From f84414d5ca03b6e5ec0bf9582cdaf62a5cc7efa4 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 12:14:49 -0800 Subject: [PATCH 01/24] refactor: modularize claude-executor and extract shared utilities - Extract message handling into src/ai/message-handlers.ts with pure functions - Extract output formatting into src/ai/output-formatters.ts - Extract progress management into src/ai/progress-manager.ts - Add audit-logger.ts with Null Object pattern for optional logging - Add shared utilities: formatting.ts, file-io.ts, functional.ts - Consolidate getPromptNameForAgent into src/types/agents.ts --- src/ai/audit-logger.ts | 79 ++++ src/ai/claude-executor.ts | 732 ++++++++++++----------------------- src/ai/message-handlers.ts | 244 ++++++++++++ src/ai/output-formatters.ts | 169 ++++++++ src/ai/progress-manager.ts | 76 ++++ src/ai/types.ts | 134 +++++++ src/audit/audit-session.ts | 5 +- src/audit/logger.ts | 17 +- src/audit/metrics-tracker.ts | 61 +-- src/error-handling.ts | 133 +++---- src/phases/pre-recon.ts | 123 +++--- src/queue-validation.ts | 75 ++-- src/session-manager.ts | 20 + src/shannon.ts | 385 +++++++----------- src/types/agents.ts | 27 +- src/utils/concurrency.ts | 5 +- src/utils/file-io.ts | 73 ++++ src/utils/formatting.ts | 60 +++ src/utils/functional.ts | 29 ++ src/utils/git-manager.ts | 276 +++++++------ src/utils/metrics.ts | 2 +- 21 files changed, 1627 insertions(+), 1098 deletions(-) create mode 100644 src/ai/audit-logger.ts create mode 100644 src/ai/message-handlers.ts create mode 100644 src/ai/output-formatters.ts create mode 100644 src/ai/progress-manager.ts create mode 100644 src/ai/types.ts create mode 100644 src/utils/file-io.ts create mode 100644 src/utils/formatting.ts create mode 100644 src/utils/functional.ts diff --git a/src/ai/audit-logger.ts b/src/ai/audit-logger.ts new file mode 100644 index 00000000..e7d44912 --- /dev/null +++ b/src/ai/audit-logger.ts @@ -0,0 +1,79 @@ +// 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. + +// Null Object pattern for audit logging - callers never check for null + +import type { AuditSession } from '../audit/index.js'; +import { formatTimestamp } from '../utils/formatting.js'; + +export interface AuditLogger { + logLlmResponse(turn: number, content: string): Promise; + logToolStart(toolName: string, parameters: unknown): Promise; + logToolEnd(result: unknown): Promise; + logError(error: Error, duration: number, turns: number): Promise; +} + +class RealAuditLogger implements AuditLogger { + private auditSession: AuditSession; + + constructor(auditSession: AuditSession) { + this.auditSession = auditSession; + } + + async logLlmResponse(turn: number, content: string): Promise { + await this.auditSession.logEvent('llm_response', { + turn, + content, + timestamp: formatTimestamp(), + }); + } + + async logToolStart(toolName: string, parameters: unknown): Promise { + await this.auditSession.logEvent('tool_start', { + toolName, + parameters, + timestamp: formatTimestamp(), + }); + } + + async logToolEnd(result: unknown): Promise { + await this.auditSession.logEvent('tool_end', { + result, + timestamp: formatTimestamp(), + }); + } + + async logError(error: Error, duration: number, turns: number): Promise { + await this.auditSession.logEvent('error', { + message: error.message, + errorType: error.constructor.name, + stack: error.stack, + duration, + turns, + timestamp: formatTimestamp(), + }); + } +} + +/** Null Object implementation - all methods are safe no-ops */ +class NullAuditLogger implements AuditLogger { + async logLlmResponse(_turn: number, _content: string): Promise {} + + async logToolStart(_toolName: string, _parameters: unknown): Promise {} + + async logToolEnd(_result: unknown): Promise {} + + async logError(_error: Error, _duration: number, _turns: number): Promise {} +} + +// Returns no-op when auditSession is null +export function createAuditLogger(auditSession: AuditSession | null): AuditLogger { + if (auditSession) { + return new RealAuditLogger(auditSession); + } + + return new NullAuditLogger(); +} diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index c8fa5f2b..d6763793 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -4,34 +4,33 @@ // it under the terms of the GNU Affero General Public License version 3 // as published by the Free Software Foundation. -import { $, fs, path } from 'zx'; +// Production Claude agent execution with retry, git checkpoints, and audit logging + +import { fs, path } from 'zx'; import chalk, { type ChalkInstance } from 'chalk'; import { query } from '@anthropic-ai/claude-agent-sdk'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; import { isRetryableError, getRetryDelay, PentestError } from '../error-handling.js'; -import { ProgressIndicator } from '../progress-indicator.js'; -import { timingResults, costResults, Timer } from '../utils/metrics.js'; -import { formatDuration } from '../audit/utils.js'; -import { createGitCheckpoint, commitGitSuccess, rollbackGitWorkspace } from '../utils/git-manager.js'; +import { timingResults, Timer } from '../utils/metrics.js'; +import { formatTimestamp } from '../utils/formatting.js'; +import { createGitCheckpoint, commitGitSuccess, rollbackGitWorkspace, getGitCommitHash } from '../utils/git-manager.js'; import { AGENT_VALIDATORS, MCP_AGENT_MAPPING } from '../constants.js'; -import { filterJsonToolCalls, getAgentPrefix } from '../utils/output-formatter.js'; import { generateSessionLogPath } from '../session-manager.js'; import { AuditSession } from '../audit/index.js'; import { createShannonHelperServer } from '../../mcp-server/dist/index.js'; import type { SessionMetadata } from '../audit/utils.js'; -import type { PromptName } from '../types/index.js'; +import { getPromptNameForAgent } from '../types/agents.js'; +import type { AgentName } from '../types/index.js'; + +import { dispatchMessage } from './message-handlers.js'; +import { detectExecutionContext, formatErrorOutput, formatCompletionMessage } from './output-formatters.js'; +import { createProgressManager } from './progress-manager.js'; +import { createAuditLogger } from './audit-logger.js'; -// Extend global for loader flag declare global { var SHANNON_DISABLE_LOADER: boolean | undefined; } -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Result types interface ClaudePromptResult { result?: string | null; success: boolean; @@ -47,7 +46,6 @@ interface ClaudePromptResult { retryable?: boolean; } -// MCP Server types interface StdioMcpServer { type: 'stdio'; command: string; @@ -57,43 +55,110 @@ interface StdioMcpServer { type McpServer = ReturnType | StdioMcpServer; -/** - * Convert agent name to prompt name for MCP_AGENT_MAPPING lookup - */ -function agentNameToPromptName(agentName: string): PromptName { - // Special cases - if (agentName === 'pre-recon') return 'pre-recon-code'; - if (agentName === 'report') return 'report-executive'; - if (agentName === 'recon') return 'recon'; - - // Pattern: {type}-vuln → vuln-{type} - const vulnMatch = agentName.match(/^(.+)-vuln$/); - if (vulnMatch) { - return `vuln-${vulnMatch[1]}` as PromptName; +// Configures MCP servers for agent execution, with Docker-specific Chromium handling +function buildMcpServers( + sourceDir: string, + agentName: string | null +): Record { + const shannonHelperServer = createShannonHelperServer(sourceDir); + + const mcpServers: Record = { + 'shannon-helper': shannonHelperServer, + }; + + if (agentName) { + const promptName = getPromptNameForAgent(agentName as AgentName); + const playwrightMcpName = MCP_AGENT_MAPPING[promptName as keyof typeof MCP_AGENT_MAPPING] || null; + + if (playwrightMcpName) { + console.log(chalk.gray(` Assigned ${agentName} -> ${playwrightMcpName}`)); + + const userDataDir = `/tmp/${playwrightMcpName}`; + + // Docker uses system Chromium; local dev uses Playwright's bundled browsers + const isDocker = process.env.SHANNON_DOCKER === 'true'; + + const mcpArgs: string[] = [ + '@playwright/mcp@latest', + '--isolated', + '--user-data-dir', userDataDir, + ]; + + // Docker: Use system Chromium; Local: Use Playwright's bundled browsers + if (isDocker) { + mcpArgs.push('--executable-path', '/usr/bin/chromium-browser'); + mcpArgs.push('--browser', 'chromium'); + } + + const envVars: Record = Object.fromEntries( + Object.entries({ + ...process.env, + PLAYWRIGHT_HEADLESS: 'true', + ...(isDocker && { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }), + }).filter((entry): entry is [string, string] => entry[1] !== undefined) + ); + + mcpServers[playwrightMcpName] = { + type: 'stdio' as const, + command: 'npx', + args: mcpArgs, + env: envVars, + }; + } } - // Pattern: {type}-exploit → exploit-{type} - const exploitMatch = agentName.match(/^(.+)-exploit$/); - if (exploitMatch) { - return `exploit-${exploitMatch[1]}` as PromptName; + return mcpServers; +} + +function outputLines(lines: string[]): void { + for (const line of lines) { + console.log(line); } +} - // Default: return as-is - return agentName as PromptName; +async function writeErrorLog( + err: Error & { code?: string; status?: number }, + sourceDir: string, + fullPrompt: string, + duration: number +): Promise { + try { + const errorLog = { + timestamp: formatTimestamp(), + agent: 'claude-executor', + error: { + name: err.constructor.name, + message: err.message, + code: err.code, + status: err.status, + stack: err.stack + }, + context: { + sourceDir, + prompt: fullPrompt.slice(0, 200) + '...', + retryable: isRetryableError(err) + }, + duration + }; + const logPath = path.join(sourceDir, 'error.log'); + await fs.appendFile(logPath, JSON.stringify(errorLog) + '\n'); + } catch (logError) { + const logErrMsg = logError instanceof Error ? logError.message : String(logError); + console.log(chalk.gray(` (Failed to write error log: ${logErrMsg})`)); + } } -// Simplified validation using direct agent name mapping async function validateAgentOutput( result: ClaudePromptResult, agentName: string | null, sourceDir: string ): Promise { - console.log(chalk.blue(` 🔍 Validating ${agentName} agent output`)); + console.log(chalk.blue(` Validating ${agentName} agent output`)); try { // Check if agent completed successfully if (!result.success || !result.result) { - console.log(chalk.red(` ❌ Validation failed: Agent execution was unsuccessful`)); + console.log(chalk.red(` Validation failed: Agent execution was unsuccessful`)); return false; } @@ -101,38 +166,36 @@ async function validateAgentOutput( const validator = agentName ? AGENT_VALIDATORS[agentName as keyof typeof AGENT_VALIDATORS] : undefined; if (!validator) { - console.log(chalk.yellow(` âš ī¸ No validator found for agent "${agentName}" - assuming success`)); - console.log(chalk.green(` ✅ Validation passed: Unknown agent with successful result`)); + console.log(chalk.yellow(` No validator found for agent "${agentName}" - assuming success`)); + console.log(chalk.green(` Validation passed: Unknown agent with successful result`)); return true; } - console.log(chalk.blue(` 📋 Using validator for agent: ${agentName}`)); - console.log(chalk.blue(` 📂 Source directory: ${sourceDir}`)); + console.log(chalk.blue(` Using validator for agent: ${agentName}`)); + console.log(chalk.blue(` Source directory: ${sourceDir}`)); // Apply validation function const validationResult = await validator(sourceDir); if (validationResult) { - console.log(chalk.green(` ✅ Validation passed: Required files/structure present`)); + console.log(chalk.green(` Validation passed: Required files/structure present`)); } else { - console.log(chalk.red(` ❌ Validation failed: Missing required deliverable files`)); + console.log(chalk.red(` Validation failed: Missing required deliverable files`)); } return validationResult; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.red(` ❌ Validation failed with error: ${errMsg}`)); - return false; // Assume invalid on validation error + console.log(chalk.red(` Validation failed with error: ${errMsg}`)); + return false; } } -// Pure function: Run Claude Code with SDK - Maximum Autonomy -// WARNING: This is a low-level function. Use runClaudePromptWithRetry() for agent execution +// Low-level SDK execution. Handles message streaming, progress, and audit logging. async function runClaudePrompt( prompt: string, sourceDir: string, - _allowedTools: string = 'Read', context: string = '', description: string = 'Claude analysis', agentName: string | null = null, @@ -143,349 +206,68 @@ async function runClaudePrompt( ): Promise { const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`); const fullPrompt = context ? `${context}\n\n${prompt}` : prompt; - let totalCost = 0; - let partialCost = 0; // Track partial cost for crash safety - - // Auto-detect execution mode to adjust logging behavior - const isParallelExecution = description.includes('vuln agent') || description.includes('exploit agent'); - const useCleanOutput = description.includes('Pre-recon agent') || - description.includes('Recon agent') || - description.includes('Executive Summary and Report Cleanup') || - description.includes('vuln agent') || - description.includes('exploit agent'); - - // Disable status manager - using simple JSON filtering for all agents now - const statusManager = null; - - // Setup progress indicator for clean output agents (unless disabled via flag) - let progressIndicator: ProgressIndicator | null = null; - if (useCleanOutput && !global.SHANNON_DISABLE_LOADER) { - const agentType = description.includes('Pre-recon') ? 'pre-reconnaissance' : - description.includes('Recon') ? 'reconnaissance' : - description.includes('Report') ? 'report generation' : 'analysis'; - progressIndicator = new ProgressIndicator(`Running ${agentType}...`); + + const execContext = detectExecutionContext(description); + const progress = createProgressManager( + { description, useCleanOutput: execContext.useCleanOutput }, + global.SHANNON_DISABLE_LOADER ?? false + ); + const auditLogger = createAuditLogger(auditSession); + + const logFilePath = buildLogFilePath(sessionMetadata, execContext.agentKey, attemptNumber); + if (!logFilePath) { + console.log(chalk.blue(` Running Claude Code: ${description}...`)); } - // NOTE: Logging now handled by AuditSession (append-only, crash-safe) - let logFilePath: string | null = null; - if (sessionMetadata && sessionMetadata.webUrl && sessionMetadata.id) { - const timestamp = new Date().toISOString().replace(/T/, '_').replace(/[:.]/g, '-').slice(0, 19); - const agentKey = description.toLowerCase().replace(/\s+/g, '-'); - const logDir = generateSessionLogPath(sessionMetadata.webUrl, sessionMetadata.id); - logFilePath = path.join(logDir, `${timestamp}_${agentKey}_attempt-${attemptNumber}.log`); - } else { - console.log(chalk.blue(` 🤖 Running Claude Code: ${description}...`)); + const mcpServers = buildMcpServers(sourceDir, agentName); + const options = { + model: 'claude-sonnet-4-5-20250929', + maxTurns: 10_000, + cwd: sourceDir, + permissionMode: 'bypassPermissions' as const, + mcpServers, + }; + + if (!execContext.useCleanOutput) { + console.log(chalk.gray(` SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=BYPASS`)); } - // Declare variables that need to be accessible in both try and catch blocks let turnCount = 0; + let result: string | null = null; + let apiErrorDetected = false; + let totalCost = 0; - try { - // Create MCP server with target directory context - const shannonHelperServer = createShannonHelperServer(sourceDir); - - // Look up agent's assigned Playwright MCP server - let playwrightMcpName: string | null = null; - if (agentName) { - const promptName = agentNameToPromptName(agentName); - playwrightMcpName = MCP_AGENT_MAPPING[promptName as keyof typeof MCP_AGENT_MAPPING] || null; - - if (playwrightMcpName) { - console.log(chalk.gray(` 🎭 Assigned ${agentName} → ${playwrightMcpName}`)); - } - } - - // Configure MCP servers: shannon-helper (SDK) + playwright-agentN (stdio) - const mcpServers: Record = { - 'shannon-helper': shannonHelperServer, - }; - - // Add Playwright MCP server if this agent needs browser automation - if (playwrightMcpName) { - const userDataDir = `/tmp/${playwrightMcpName}`; - - // Detect if running in Docker via explicit environment variable - const isDocker = process.env.SHANNON_DOCKER === 'true'; - - // Build args array - conditionally add --executable-path for Docker - const mcpArgs: string[] = [ - '@playwright/mcp@latest', - '--isolated', - '--user-data-dir', userDataDir, - ]; - - // Docker: Use system Chromium; Local: Use Playwright's bundled browsers - if (isDocker) { - mcpArgs.push('--executable-path', '/usr/bin/chromium-browser'); - mcpArgs.push('--browser', 'chromium'); - } - - // Filter out undefined env values for type safety - const envVars: Record = Object.fromEntries( - Object.entries({ - ...process.env, - PLAYWRIGHT_HEADLESS: 'true', - ...(isDocker && { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }), - }).filter((entry): entry is [string, string] => entry[1] !== undefined) - ); - - mcpServers[playwrightMcpName] = { - type: 'stdio' as const, - command: 'npx', - args: mcpArgs, - env: envVars, - }; - } - - const options = { - model: 'claude-sonnet-4-5-20250929', // Use latest Claude 4.5 Sonnet - maxTurns: 10_000, // Maximum turns for autonomous work - cwd: sourceDir, // Set working directory using SDK option - permissionMode: 'bypassPermissions' as const, // Bypass all permission checks for pentesting - mcpServers, - }; - - // SDK Options only shown for verbose agents (not clean output) - if (!useCleanOutput) { - console.log(chalk.gray(` SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=BYPASS`)); - } - - let result: string | null = null; - const messages: string[] = []; - let apiErrorDetected = false; - - // Start progress indicator for clean output agents - if (progressIndicator) { - progressIndicator.start(); - } - - let lastHeartbeat = Date.now(); - const HEARTBEAT_INTERVAL = 30000; // 30 seconds - - try { - for await (const message of query({ prompt: fullPrompt, options })) { - // Periodic heartbeat for long-running agents (only when loader is disabled) - const now = Date.now(); - if (global.SHANNON_DISABLE_LOADER && now - lastHeartbeat > HEARTBEAT_INTERVAL) { - console.log(chalk.blue(` âąī¸ [${Math.floor((now - timer.startTime) / 1000)}s] ${description} running... (Turn ${turnCount})`)); - lastHeartbeat = now; - } - - if (message.type === "assistant") { - turnCount++; - - const messageContent = message.message as { content: unknown }; - const content = Array.isArray(messageContent.content) - ? messageContent.content.map((c: { text?: string }) => c.text || JSON.stringify(c)).join('\n') - : String(messageContent.content); - - if (statusManager) { - // Smart status updates for parallel execution - disabled - } else if (useCleanOutput) { - // Clean output for all agents: filter JSON tool calls but show meaningful text - const cleanedContent = filterJsonToolCalls(content); - if (cleanedContent.trim()) { - // Temporarily stop progress indicator to show output - if (progressIndicator) { - progressIndicator.stop(); - } - - if (isParallelExecution) { - // Compact output for parallel agents with prefixes - const prefix = getAgentPrefix(description); - console.log(colorFn(`${prefix} ${cleanedContent}`)); - } else { - // Full turn output for single agents - console.log(colorFn(`\n 🤖 Turn ${turnCount} (${description}):`)); - console.log(colorFn(` ${cleanedContent}`)); - } - - // Restart progress indicator after output - if (progressIndicator) { - progressIndicator.start(); - } - } - } else { - // Full streaming output - show complete messages with specialist color - console.log(colorFn(`\n 🤖 Turn ${turnCount} (${description}):`)); - console.log(colorFn(` ${content}`)); - } - - // Log to audit system (crash-safe, append-only) - if (auditSession) { - await auditSession.logEvent('llm_response', { - turn: turnCount, - content, - timestamp: new Date().toISOString() - }); - } - - messages.push(content); - - // Check for API error patterns in assistant message content - if (content && typeof content === 'string') { - const lowerContent = content.toLowerCase(); - if (lowerContent.includes('session limit reached')) { - throw new PentestError('Session limit reached', 'billing', false); - } - if (lowerContent.includes('api error') || lowerContent.includes('terminated')) { - apiErrorDetected = true; - console.log(chalk.red(` âš ī¸ API Error detected in assistant response: ${content.trim()}`)); - } - } - - } else if (message.type === "system" && (message as { subtype?: string }).subtype === "init") { - // Show useful system info only for verbose agents - if (!useCleanOutput) { - const initMsg = message as { model?: string; permissionMode?: string; mcp_servers?: Array<{ name: string; status: string }> }; - console.log(chalk.blue(` â„šī¸ Model: ${initMsg.model}, Permission: ${initMsg.permissionMode}`)); - if (initMsg.mcp_servers && initMsg.mcp_servers.length > 0) { - const mcpStatus = initMsg.mcp_servers.map(s => `${s.name}(${s.status})`).join(', '); - console.log(chalk.blue(` đŸ“Ļ MCP: ${mcpStatus}`)); - } - } - - } else if (message.type === "user") { - // Skip user messages (these are our own inputs echoed back) - continue; - - } else if ((message.type as string) === "tool_use") { - const toolMsg = message as unknown as { name: string; input?: Record }; - console.log(chalk.yellow(`\n 🔧 Using Tool: ${toolMsg.name}`)); - if (toolMsg.input && Object.keys(toolMsg.input).length > 0) { - console.log(chalk.gray(` Input: ${JSON.stringify(toolMsg.input, null, 2)}`)); - } - - // Log tool start event - if (auditSession) { - await auditSession.logEvent('tool_start', { - toolName: toolMsg.name, - parameters: toolMsg.input, - timestamp: new Date().toISOString() - }); - } - } else if ((message.type as string) === "tool_result") { - const resultMsg = message as unknown as { content?: unknown }; - console.log(chalk.green(` ✅ Tool Result:`)); - if (resultMsg.content) { - // Show tool results but truncate if too long - const resultStr = typeof resultMsg.content === 'string' ? resultMsg.content : JSON.stringify(resultMsg.content, null, 2); - if (resultStr.length > 500) { - console.log(chalk.gray(` ${resultStr.slice(0, 500)}...\n [Result truncated - ${resultStr.length} total chars]`)); - } else { - console.log(chalk.gray(` ${resultStr}`)); - } - } - - // Log tool end event - if (auditSession) { - await auditSession.logEvent('tool_end', { - result: resultMsg.content, - timestamp: new Date().toISOString() - }); - } - } else if (message.type === "result") { - const resultMessage = message as { - result?: string; - total_cost_usd?: number; - duration_ms?: number; - subtype?: string; - permission_denials?: unknown[]; - }; - result = resultMessage.result || null; - - if (!statusManager) { - if (useCleanOutput) { - // Clean completion output - just duration and cost - console.log(chalk.magenta(`\n 🏁 COMPLETED:`)); - const cost = resultMessage.total_cost_usd || 0; - console.log(chalk.gray(` âąī¸ Duration: ${((resultMessage.duration_ms || 0)/1000).toFixed(1)}s, Cost: $${cost.toFixed(4)}`)); - - if (resultMessage.subtype === "error_max_turns") { - console.log(chalk.red(` âš ī¸ Stopped: Hit maximum turns limit`)); - } else if (resultMessage.subtype === "error_during_execution") { - console.log(chalk.red(` ❌ Stopped: Execution error`)); - } - - if (resultMessage.permission_denials && resultMessage.permission_denials.length > 0) { - console.log(chalk.yellow(` đŸšĢ ${resultMessage.permission_denials.length} permission denials`)); - } - } else { - // Full completion output for agents without clean output - console.log(chalk.magenta(`\n 🏁 COMPLETED:`)); - const cost = resultMessage.total_cost_usd || 0; - console.log(chalk.gray(` âąī¸ Duration: ${((resultMessage.duration_ms || 0)/1000).toFixed(1)}s, Cost: $${cost.toFixed(4)}`)); - - if (resultMessage.subtype === "error_max_turns") { - console.log(chalk.red(` âš ī¸ Stopped: Hit maximum turns limit`)); - } else if (resultMessage.subtype === "error_during_execution") { - console.log(chalk.red(` ❌ Stopped: Execution error`)); - } - - if (resultMessage.permission_denials && resultMessage.permission_denials.length > 0) { - console.log(chalk.yellow(` đŸšĢ ${resultMessage.permission_denials.length} permission denials`)); - } - - // Show result content (if it's reasonable length) - if (result && typeof result === 'string') { - if (result.length > 1000) { - console.log(chalk.magenta(` 📄 ${result.slice(0, 1000)}... [${result.length} total chars]`)); - } else { - console.log(chalk.magenta(` 📄 ${result}`)); - } - } - } - } - - // Track cost for all agents - const cost = resultMessage.total_cost_usd || 0; - const agentKey = description.toLowerCase().replace(/\s+/g, '-'); - costResults.agents[agentKey] = cost; - costResults.total += cost; + progress.start(); - // Store cost for return value and partial tracking - totalCost = cost; - partialCost = cost; - break; - } else { - // Log any other message types we might not be handling - console.log(chalk.gray(` đŸ’Ŧ ${message.type}: ${JSON.stringify(message, null, 2)}`)); - } - } - } catch (queryError) { - throw queryError; // Re-throw to outer catch - } + try { + const messageLoopResult = await processMessageStream( + fullPrompt, + options, + { execContext, description, colorFn, progress, auditLogger }, + timer + ); + + turnCount = messageLoopResult.turnCount; + result = messageLoopResult.result; + apiErrorDetected = messageLoopResult.apiErrorDetected; + totalCost = messageLoopResult.cost; const duration = timer.stop(); - const agentKey = description.toLowerCase().replace(/\s+/g, '-'); - timingResults.agents[agentKey] = duration; + timingResults.agents[execContext.agentKey] = duration; - // API error detection is logged but not immediately failed if (apiErrorDetected) { - console.log(chalk.yellow(` âš ī¸ API Error detected in ${description} - will validate deliverables before failing`)); + console.log(chalk.yellow(` API Error detected in ${description} - will validate deliverables before failing`)); } - // Show completion messages based on agent type - if (progressIndicator) { - const agentType = description.includes('Pre-recon') ? 'Pre-recon analysis' : - description.includes('Recon') ? 'Reconnaissance' : - description.includes('Report') ? 'Report generation' : 'Analysis'; - progressIndicator.finish(`${agentType} complete! (${turnCount} turns, ${formatDuration(duration)})`); - } else if (isParallelExecution) { - const prefix = getAgentPrefix(description); - console.log(chalk.green(`${prefix} ✅ Complete (${turnCount} turns, ${formatDuration(duration)})`)); - } else if (!useCleanOutput) { - console.log(chalk.green(` ✅ Claude Code completed: ${description} (${turnCount} turns) in ${formatDuration(duration)}`)); - } + progress.finish(formatCompletionMessage(execContext, description, turnCount, duration)); - // Return result with log file path for all agents const returnData: ClaudePromptResult = { result, success: true, duration, turns: turnCount, cost: totalCost, - partialCost, + partialCost: totalCost, apiErrorDetected }; if (logFilePath) { @@ -495,76 +277,14 @@ async function runClaudePrompt( } catch (error) { const duration = timer.stop(); - const agentKey = description.toLowerCase().replace(/\s+/g, '-'); - timingResults.agents[agentKey] = duration; - - const err = error as Error & { code?: string; status?: number; duration?: number; cost?: number }; - - // Log error to audit system - if (auditSession) { - await auditSession.logEvent('error', { - message: err.message, - errorType: err.constructor.name, - stack: err.stack, - duration, - turns: turnCount, - timestamp: new Date().toISOString() - }); - } - - // Show error messages based on agent type - if (progressIndicator) { - progressIndicator.stop(); - const agentType = description.includes('Pre-recon') ? 'Pre-recon analysis' : - description.includes('Recon') ? 'Reconnaissance' : - description.includes('Report') ? 'Report generation' : 'Analysis'; - console.log(chalk.red(`❌ ${agentType} failed (${formatDuration(duration)})`)); - } else if (isParallelExecution) { - const prefix = getAgentPrefix(description); - console.log(chalk.red(`${prefix} ❌ Failed (${formatDuration(duration)})`)); - } else if (!useCleanOutput) { - console.log(chalk.red(` ❌ Claude Code failed: ${description} (${formatDuration(duration)})`)); - } - console.log(chalk.red(` Error Type: ${err.constructor.name}`)); - console.log(chalk.red(` Message: ${err.message}`)); - console.log(chalk.gray(` Agent: ${description}`)); - console.log(chalk.gray(` Working Directory: ${sourceDir}`)); - console.log(chalk.gray(` Retryable: ${isRetryableError(err) ? 'Yes' : 'No'}`)); - - // Log additional context if available - if (err.code) { - console.log(chalk.gray(` Error Code: ${err.code}`)); - } - if (err.status) { - console.log(chalk.gray(` HTTP Status: ${err.status}`)); - } + timingResults.agents[execContext.agentKey] = duration; - // Save detailed error to log file for debugging - try { - const errorLog = { - timestamp: new Date().toISOString(), - agent: description, - error: { - name: err.constructor.name, - message: err.message, - code: err.code, - status: err.status, - stack: err.stack - }, - context: { - sourceDir, - prompt: fullPrompt.slice(0, 200) + '...', - retryable: isRetryableError(err) - }, - duration - }; + const err = error as Error & { code?: string; status?: number }; - const logPath = path.join(sourceDir, 'error.log'); - await fs.appendFile(logPath, JSON.stringify(errorLog) + '\n'); - } catch (logError) { - const logErrMsg = logError instanceof Error ? logError.message : String(logError); - console.log(chalk.gray(` (Failed to write error log: ${logErrMsg})`)); - } + await auditLogger.logError(err, duration, turnCount); + progress.stop(); + outputLines(formatErrorOutput(err, execContext, description, duration, sourceDir, isRetryableError(err))); + await writeErrorLog(err, sourceDir, fullPrompt, duration); return { error: err.message, @@ -572,17 +292,97 @@ async function runClaudePrompt( prompt: fullPrompt.slice(0, 100) + '...', success: false, duration, - cost: partialCost, + cost: totalCost, retryable: isRetryableError(err) }; } } -// PREFERRED: Production-ready Claude agent execution with full orchestration +function buildLogFilePath( + sessionMetadata: SessionMetadata | null, + agentKey: string, + attemptNumber: number +): string | null { + if (!sessionMetadata || !sessionMetadata.webUrl || !sessionMetadata.id) { + return null; + } + const timestamp = formatTimestamp().replace(/T/, '_').replace(/[:.]/g, '-').slice(0, 19); + const logDir = generateSessionLogPath(sessionMetadata.webUrl, sessionMetadata.id); + return path.join(logDir, `${timestamp}_${agentKey}_attempt-${attemptNumber}.log`); +} + +interface MessageLoopResult { + turnCount: number; + result: string | null; + apiErrorDetected: boolean; + cost: number; +} + +interface MessageLoopDeps { + execContext: ReturnType; + description: string; + colorFn: ChalkInstance; + progress: ReturnType; + auditLogger: ReturnType; +} + +async function processMessageStream( + fullPrompt: string, + options: NonNullable[0]['options']>, + deps: MessageLoopDeps, + timer: Timer +): Promise { + const { execContext, description, colorFn, progress, auditLogger } = deps; + const HEARTBEAT_INTERVAL = 30000; + + let turnCount = 0; + let result: string | null = null; + let apiErrorDetected = false; + let cost = 0; + let lastHeartbeat = Date.now(); + + for await (const message of query({ prompt: fullPrompt, options })) { + // Heartbeat logging when loader is disabled + const now = Date.now(); + if (global.SHANNON_DISABLE_LOADER && now - lastHeartbeat > HEARTBEAT_INTERVAL) { + console.log(chalk.blue(` [${Math.floor((now - timer.startTime) / 1000)}s] ${description} running... (Turn ${turnCount})`)); + lastHeartbeat = now; + } + + // Increment turn count for assistant messages + if (message.type === 'assistant') { + turnCount++; + } + + const dispatchResult = await dispatchMessage( + message as { type: string; subtype?: string }, + turnCount, + { execContext, description, colorFn, progress, auditLogger } + ); + + if (dispatchResult.type === 'throw') { + throw dispatchResult.error; + } + + if (dispatchResult.type === 'complete') { + result = dispatchResult.result; + cost = dispatchResult.cost; + break; + } + + if (dispatchResult.type === 'continue' && dispatchResult.apiErrorDetected) { + apiErrorDetected = true; + } + } + + return { turnCount, result, apiErrorDetected, cost }; +} + +// Main entry point for agent execution. Handles retries, git checkpoints, and validation. export async function runClaudePromptWithRetry( prompt: string, sourceDir: string, - allowedTools: string = 'Read', + _allowedTools: string = 'Read', context: string = '', description: string = 'Claude analysis', agentName: string | null = null, @@ -593,9 +393,8 @@ export async function runClaudePromptWithRetry( let lastError: Error | undefined; let retryContext = context; - console.log(chalk.cyan(`🚀 Starting ${description} with ${maxRetries} max attempts`)); + console.log(chalk.cyan(`Starting ${description} with ${maxRetries} max attempts`)); - // Initialize audit session (crash-safe logging) let auditSession: AuditSession | null = null; if (sessionMetadata && agentName) { auditSession = new AuditSession(sessionMetadata); @@ -603,29 +402,27 @@ export async function runClaudePromptWithRetry( } for (let attempt = 1; attempt <= maxRetries; attempt++) { - // Create checkpoint before each attempt await createGitCheckpoint(sourceDir, description, attempt); - // Start agent tracking in audit system (saves prompt snapshot automatically) if (auditSession && agentName) { const fullPrompt = retryContext ? `${retryContext}\n\n${prompt}` : prompt; await auditSession.startAgent(agentName, fullPrompt, attempt); } try { - const result = await runClaudePrompt(prompt, sourceDir, allowedTools, retryContext, description, agentName, colorFn, sessionMetadata, auditSession, attempt); + const result = await runClaudePrompt( + prompt, sourceDir, retryContext, + description, agentName, colorFn, sessionMetadata, auditSession, attempt + ); - // Validate output after successful run if (result.success) { const validationPassed = await validateAgentOutput(result, agentName, sourceDir); if (validationPassed) { - // Check if API error was detected but validation passed if (result.apiErrorDetected) { - console.log(chalk.yellow(`📋 Validation: Ready for exploitation despite API error warnings`)); + console.log(chalk.yellow(`Validation: Ready for exploitation despite API error warnings`)); } - // Record successful attempt in audit system if (auditSession && agentName) { const commitHash = await getGitCommitHash(sourceDir); const endResult: { @@ -646,15 +443,13 @@ export async function runClaudePromptWithRetry( await auditSession.endAgent(agentName, endResult); } - // Commit successful changes (will include the snapshot) await commitGitSuccess(sourceDir, description); - console.log(chalk.green.bold(`🎉 ${description} completed successfully on attempt ${attempt}/${maxRetries}`)); + console.log(chalk.green.bold(`${description} completed successfully on attempt ${attempt}/${maxRetries}`)); return result; + // Validation failure is retryable - agent might succeed on retry with cleaner workspace } else { - // Agent completed but output validation failed - console.log(chalk.yellow(`âš ī¸ ${description} completed but output validation failed`)); + console.log(chalk.yellow(`${description} completed but output validation failed`)); - // Record failed validation attempt in audit system if (auditSession && agentName) { await auditSession.endAgent(agentName, { attemptNumber: attempt, @@ -666,20 +461,17 @@ export async function runClaudePromptWithRetry( }); } - // If API error detected AND validation failed, this is a retryable error if (result.apiErrorDetected) { - console.log(chalk.yellow(`âš ī¸ API Error detected with validation failure - treating as retryable`)); + console.log(chalk.yellow(`API Error detected with validation failure - treating as retryable`)); lastError = new Error('API Error: terminated with validation failure'); } else { lastError = new Error('Output validation failed'); } if (attempt < maxRetries) { - // Rollback contaminated workspace await rollbackGitWorkspace(sourceDir, 'validation failure'); continue; } else { - // FAIL FAST - Don't continue with broken pipeline throw new PentestError( `Agent ${description} failed output validation after ${maxRetries} attempts. Required deliverable files were not created.`, 'validation', @@ -694,7 +486,6 @@ export async function runClaudePromptWithRetry( const err = error as Error & { duration?: number; cost?: number; partialResults?: unknown }; lastError = err; - // Record failed attempt in audit system if (auditSession && agentName) { await auditSession.endAgent(agentName, { attemptNumber: attempt, @@ -706,24 +497,21 @@ export async function runClaudePromptWithRetry( }); } - // Check if error is retryable if (!isRetryableError(err)) { - console.log(chalk.red(`❌ ${description} failed with non-retryable error: ${err.message}`)); + console.log(chalk.red(`${description} failed with non-retryable error: ${err.message}`)); await rollbackGitWorkspace(sourceDir, 'non-retryable error cleanup'); throw err; } if (attempt < maxRetries) { - // Rollback for clean retry await rollbackGitWorkspace(sourceDir, 'retryable error cleanup'); const delay = getRetryDelay(err, attempt); const delaySeconds = (delay / 1000).toFixed(1); - console.log(chalk.yellow(`âš ī¸ ${description} failed (attempt ${attempt}/${maxRetries})`)); + console.log(chalk.yellow(`${description} failed (attempt ${attempt}/${maxRetries})`)); console.log(chalk.gray(` Error: ${err.message}`)); console.log(chalk.gray(` Workspace rolled back, retrying in ${delaySeconds}s...`)); - // Preserve any partial results for next retry if (err.partialResults) { retryContext = `${context}\n\nPrevious partial results: ${JSON.stringify(err.partialResults)}`; } @@ -731,7 +519,7 @@ export async function runClaudePromptWithRetry( await new Promise(resolve => setTimeout(resolve, delay)); } else { await rollbackGitWorkspace(sourceDir, 'final failure cleanup'); - console.log(chalk.red(`❌ ${description} failed after ${maxRetries} attempts`)); + console.log(chalk.red(`${description} failed after ${maxRetries} attempts`)); console.log(chalk.red(` Final error: ${err.message}`)); } } @@ -739,13 +527,3 @@ export async function runClaudePromptWithRetry( throw lastError; } - -// Helper function to get git commit hash -async function getGitCommitHash(sourceDir: string): Promise { - try { - const result = await $`cd ${sourceDir} && git rev-parse HEAD`; - return result.stdout.trim(); - } catch { - return null; - } -} diff --git a/src/ai/message-handlers.ts b/src/ai/message-handlers.ts new file mode 100644 index 00000000..098d2391 --- /dev/null +++ b/src/ai/message-handlers.ts @@ -0,0 +1,244 @@ +// 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. + +// Pure functions for processing SDK message types + +import { PentestError } from '../error-handling.js'; +import { filterJsonToolCalls } from '../utils/output-formatter.js'; +import { formatTimestamp } from '../utils/formatting.js'; +import chalk from 'chalk'; +import { + formatAssistantOutput, + formatResultOutput, + formatToolUseOutput, + formatToolResultOutput, +} from './output-formatters.js'; +import { costResults } from '../utils/metrics.js'; +import type { AuditLogger } from './audit-logger.js'; +import type { ProgressManager } from './progress-manager.js'; +import type { + AssistantMessage, + ResultMessage, + ToolUseMessage, + ToolResultMessage, + AssistantResult, + ResultData, + ToolUseData, + ToolResultData, + ApiErrorDetection, + ContentBlock, + SystemInitMessage, + ExecutionContext, +} from './types.js'; +import type { ChalkInstance } from 'chalk'; + +// Handles both array and string content formats from SDK +export function extractMessageContent(message: AssistantMessage): string { + const messageContent = message.message; + + if (Array.isArray(messageContent.content)) { + return messageContent.content + .map((c: ContentBlock) => c.text || JSON.stringify(c)) + .join('\n'); + } + + return String(messageContent.content); +} + +export function detectApiError(content: string): ApiErrorDetection { + if (!content || typeof content !== 'string') { + return { detected: false }; + } + + const lowerContent = content.toLowerCase(); + + // Fatal error - should throw immediately + if (lowerContent.includes('session limit reached')) { + return { + detected: true, + shouldThrow: new PentestError('Session limit reached', 'billing', false), + }; + } + + // Non-fatal API errors - detected but continue + if (lowerContent.includes('api error') || lowerContent.includes('terminated')) { + return { detected: true }; + } + + return { detected: false }; +} + +export function handleAssistantMessage( + message: AssistantMessage, + turnCount: number +): AssistantResult { + const content = extractMessageContent(message); + const cleanedContent = filterJsonToolCalls(content); + const errorDetection = detectApiError(content); + + const result: AssistantResult = { + content, + cleanedContent, + apiErrorDetected: errorDetection.detected, + logData: { + turn: turnCount, + content, + timestamp: formatTimestamp(), + }, + }; + + // Only add shouldThrow if it exists (exactOptionalPropertyTypes compliance) + if (errorDetection.shouldThrow) { + result.shouldThrow = errorDetection.shouldThrow; + } + + return result; +} + +// Final message of a query with cost/duration info +export function handleResultMessage(message: ResultMessage): ResultData { + const result: ResultData = { + result: message.result || null, + cost: message.total_cost_usd || 0, + duration_ms: message.duration_ms || 0, + permissionDenials: message.permission_denials?.length || 0, + }; + + // Only add subtype if it exists (exactOptionalPropertyTypes compliance) + if (message.subtype) { + result.subtype = message.subtype; + } + + return result; +} + +export function handleToolUseMessage(message: ToolUseMessage): ToolUseData { + return { + toolName: message.name, + parameters: message.input || {}, + timestamp: formatTimestamp(), + }; +} + +// Truncates long results for display (500 char limit), preserves full content for logging +export function handleToolResultMessage(message: ToolResultMessage): ToolResultData { + const content = message.content; + const contentStr = + typeof content === 'string' ? content : JSON.stringify(content, null, 2); + + const displayContent = + contentStr.length > 500 + ? `${contentStr.slice(0, 500)}...\n[Result truncated - ${contentStr.length} total chars]` + : contentStr; + + return { + content, + displayContent, + timestamp: formatTimestamp(), + }; +} + +// Output helper for console logging +function outputLines(lines: string[]): void { + for (const line of lines) { + console.log(line); + } +} + +// Message dispatch result types +export type MessageDispatchAction = + | { type: 'continue'; apiErrorDetected?: boolean } + | { type: 'complete'; result: string | null; cost: number } + | { type: 'throw'; error: Error }; + +export interface MessageDispatchDeps { + execContext: ExecutionContext; + description: string; + colorFn: ChalkInstance; + progress: ProgressManager; + auditLogger: AuditLogger; +} + +// Dispatches SDK messages to appropriate handlers and formatters +export async function dispatchMessage( + message: { type: string; subtype?: string }, + turnCount: number, + deps: MessageDispatchDeps +): Promise { + const { execContext, description, colorFn, progress, auditLogger } = deps; + + switch (message.type) { + case 'assistant': { + const assistantResult = handleAssistantMessage(message as AssistantMessage, turnCount); + + if (assistantResult.shouldThrow) { + return { type: 'throw', error: assistantResult.shouldThrow }; + } + + if (assistantResult.cleanedContent.trim()) { + progress.stop(); + outputLines(formatAssistantOutput( + assistantResult.cleanedContent, + execContext, + turnCount, + description, + colorFn + )); + progress.start(); + } + + await auditLogger.logLlmResponse(turnCount, assistantResult.content); + + if (assistantResult.apiErrorDetected) { + console.log(chalk.red(` API Error detected in assistant response`)); + return { type: 'continue', apiErrorDetected: true }; + } + + return { type: 'continue' }; + } + + case 'system': { + if (message.subtype === 'init' && !execContext.useCleanOutput) { + const initMsg = message as SystemInitMessage; + console.log(chalk.blue(` Model: ${initMsg.model}, Permission: ${initMsg.permissionMode}`)); + if (initMsg.mcp_servers && initMsg.mcp_servers.length > 0) { + const mcpStatus = initMsg.mcp_servers.map(s => `${s.name}(${s.status})`).join(', '); + console.log(chalk.blue(` MCP: ${mcpStatus}`)); + } + } + return { type: 'continue' }; + } + + case 'user': + return { type: 'continue' }; + + case 'tool_use': { + const toolData = handleToolUseMessage(message as unknown as ToolUseMessage); + outputLines(formatToolUseOutput(toolData.toolName, toolData.parameters)); + await auditLogger.logToolStart(toolData.toolName, toolData.parameters); + return { type: 'continue' }; + } + + case 'tool_result': { + const toolResultData = handleToolResultMessage(message as unknown as ToolResultMessage); + outputLines(formatToolResultOutput(toolResultData.displayContent)); + await auditLogger.logToolEnd(toolResultData.content); + return { type: 'continue' }; + } + + case 'result': { + const resultData = handleResultMessage(message as ResultMessage); + outputLines(formatResultOutput(resultData, !execContext.useCleanOutput)); + costResults.agents[execContext.agentKey] = resultData.cost; + costResults.total += resultData.cost; + return { type: 'complete', result: resultData.result, cost: resultData.cost }; + } + + default: + console.log(chalk.gray(` ${message.type}: ${JSON.stringify(message, null, 2)}`)); + return { type: 'continue' }; + } +} diff --git a/src/ai/output-formatters.ts b/src/ai/output-formatters.ts new file mode 100644 index 00000000..833c71ca --- /dev/null +++ b/src/ai/output-formatters.ts @@ -0,0 +1,169 @@ +// 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. + +// Pure functions for formatting console output + +import chalk from 'chalk'; +import { extractAgentType, formatDuration } from '../utils/formatting.js'; +import { getAgentPrefix } from '../utils/output-formatter.js'; +import type { ExecutionContext, ResultData } from './types.js'; + +export function detectExecutionContext(description: string): ExecutionContext { + const isParallelExecution = + description.includes('vuln agent') || description.includes('exploit agent'); + + const useCleanOutput = + description.includes('Pre-recon agent') || + description.includes('Recon agent') || + description.includes('Executive Summary and Report Cleanup') || + description.includes('vuln agent') || + description.includes('exploit agent'); + + const agentType = extractAgentType(description); + + const agentKey = description.toLowerCase().replace(/\s+/g, '-'); + + return { isParallelExecution, useCleanOutput, agentType, agentKey }; +} + +export function formatAssistantOutput( + cleanedContent: string, + context: ExecutionContext, + turnCount: number, + description: string, + colorFn: typeof chalk.cyan = chalk.cyan +): string[] { + if (!cleanedContent.trim()) { + return []; + } + + const lines: string[] = []; + + if (context.isParallelExecution) { + // Compact output for parallel agents with prefixes + const prefix = getAgentPrefix(description); + lines.push(colorFn(`${prefix} ${cleanedContent}`)); + } else { + // Full turn output for sequential agents + lines.push(colorFn(`\n Turn ${turnCount} (${description}):`)); + lines.push(colorFn(` ${cleanedContent}`)); + } + + return lines; +} + +export function formatResultOutput(data: ResultData, showFullResult: boolean): string[] { + const lines: string[] = []; + + lines.push(chalk.magenta(`\n COMPLETED:`)); + lines.push( + chalk.gray( + ` Duration: ${(data.duration_ms / 1000).toFixed(1)}s, Cost: $${data.cost.toFixed(4)}` + ) + ); + + if (data.subtype === 'error_max_turns') { + lines.push(chalk.red(` Stopped: Hit maximum turns limit`)); + } else if (data.subtype === 'error_during_execution') { + lines.push(chalk.red(` Stopped: Execution error`)); + } + + if (data.permissionDenials > 0) { + lines.push(chalk.yellow(` ${data.permissionDenials} permission denials`)); + } + + if (showFullResult && data.result && typeof data.result === 'string') { + if (data.result.length > 1000) { + lines.push(chalk.magenta(` ${data.result.slice(0, 1000)}... [${data.result.length} total chars]`)); + } else { + lines.push(chalk.magenta(` ${data.result}`)); + } + } + + return lines; +} + +export function formatErrorOutput( + error: Error & { code?: string; status?: number }, + context: ExecutionContext, + description: string, + duration: number, + sourceDir: string, + isRetryable: boolean +): string[] { + const lines: string[] = []; + + if (context.isParallelExecution) { + const prefix = getAgentPrefix(description); + lines.push(chalk.red(`${prefix} Failed (${formatDuration(duration)})`)); + } else if (context.useCleanOutput) { + lines.push(chalk.red(`${context.agentType} failed (${formatDuration(duration)})`)); + } else { + lines.push(chalk.red(` Claude Code failed: ${description} (${formatDuration(duration)})`)); + } + + lines.push(chalk.red(` Error Type: ${error.constructor.name}`)); + lines.push(chalk.red(` Message: ${error.message}`)); + lines.push(chalk.gray(` Agent: ${description}`)); + lines.push(chalk.gray(` Working Directory: ${sourceDir}`)); + lines.push(chalk.gray(` Retryable: ${isRetryable ? 'Yes' : 'No'}`)); + + if (error.code) { + lines.push(chalk.gray(` Error Code: ${error.code}`)); + } + if (error.status) { + lines.push(chalk.gray(` HTTP Status: ${error.status}`)); + } + + return lines; +} + +export function formatCompletionMessage( + context: ExecutionContext, + description: string, + turnCount: number, + duration: number +): string { + if (context.isParallelExecution) { + const prefix = getAgentPrefix(description); + return chalk.green(`${prefix} Complete (${turnCount} turns, ${formatDuration(duration)})`); + } + + if (context.useCleanOutput) { + return chalk.green( + `${context.agentType.charAt(0).toUpperCase() + context.agentType.slice(1)} complete! (${turnCount} turns, ${formatDuration(duration)})` + ); + } + + return chalk.green( + ` Claude Code completed: ${description} (${turnCount} turns) in ${formatDuration(duration)}` + ); +} + +export function formatToolUseOutput( + toolName: string, + input: Record | undefined +): string[] { + const lines: string[] = []; + + lines.push(chalk.yellow(`\n Using Tool: ${toolName}`)); + if (input && Object.keys(input).length > 0) { + lines.push(chalk.gray(` Input: ${JSON.stringify(input, null, 2)}`)); + } + + return lines; +} + +export function formatToolResultOutput(displayContent: string): string[] { + const lines: string[] = []; + + lines.push(chalk.green(` Tool Result:`)); + if (displayContent) { + lines.push(chalk.gray(` ${displayContent}`)); + } + + return lines; +} diff --git a/src/ai/progress-manager.ts b/src/ai/progress-manager.ts new file mode 100644 index 00000000..ceee32dd --- /dev/null +++ b/src/ai/progress-manager.ts @@ -0,0 +1,76 @@ +// 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. + +// Null Object pattern for progress indicator - callers never check for null + +import { ProgressIndicator } from '../progress-indicator.js'; +import { extractAgentType } from '../utils/formatting.js'; + +export interface ProgressContext { + description: string; + useCleanOutput: boolean; +} + +export interface ProgressManager { + start(): void; + stop(): void; + finish(message: string): void; + isActive(): boolean; +} + +class RealProgressManager implements ProgressManager { + private indicator: ProgressIndicator; + private active: boolean = false; + + constructor(message: string) { + this.indicator = new ProgressIndicator(message); + } + + start(): void { + this.indicator.start(); + this.active = true; + } + + stop(): void { + this.indicator.stop(); + this.active = false; + } + + finish(message: string): void { + this.indicator.finish(message); + this.active = false; + } + + isActive(): boolean { + return this.active; + } +} + +/** Null Object implementation - all methods are safe no-ops */ +class NullProgressManager implements ProgressManager { + start(): void {} + + stop(): void {} + + finish(_message: string): void {} + + isActive(): boolean { + return false; + } +} + +// Returns no-op when disabled +export function createProgressManager( + context: ProgressContext, + disableLoader: boolean +): ProgressManager { + if (!context.useCleanOutput || disableLoader) { + return new NullProgressManager(); + } + + const agentType = extractAgentType(context.description); + return new RealProgressManager(`Running ${agentType}...`); +} diff --git a/src/ai/types.ts b/src/ai/types.ts new file mode 100644 index 00000000..b754d0c9 --- /dev/null +++ b/src/ai/types.ts @@ -0,0 +1,134 @@ +// 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. + +// Type definitions for Claude executor message processing pipeline + +export interface ExecutionContext { + isParallelExecution: boolean; + useCleanOutput: boolean; + agentType: string; + agentKey: string; +} + +export interface ProcessingState { + turnCount: number; + result: string | null; + apiErrorDetected: boolean; + totalCost: number; + partialCost: number; + lastHeartbeat: number; +} + +export interface ProcessingResult { + result: string | null; + turnCount: number; + apiErrorDetected: boolean; + totalCost: number; +} + +export interface AssistantResult { + content: string; + cleanedContent: string; + apiErrorDetected: boolean; + shouldThrow?: Error; + logData: { + turn: number; + content: string; + timestamp: string; + }; +} + +export interface ResultData { + result: string | null; + cost: number; + duration_ms: number; + subtype?: string; + permissionDenials: number; +} + +export interface ToolUseData { + toolName: string; + parameters: Record; + timestamp: string; +} + +export interface ToolResultData { + content: unknown; + displayContent: string; + timestamp: string; +} + +export interface ContentBlock { + type?: string; + text?: string; +} + +export interface AssistantMessage { + type: 'assistant'; + message: { + content: ContentBlock[] | string; + }; +} + +export interface ResultMessage { + type: 'result'; + result?: string; + total_cost_usd?: number; + duration_ms?: number; + subtype?: string; + permission_denials?: unknown[]; +} + +export interface ToolUseMessage { + type: 'tool_use'; + name: string; + input?: Record; +} + +export interface ToolResultMessage { + type: 'tool_result'; + content?: unknown; +} + +export interface ApiErrorDetection { + detected: boolean; + shouldThrow?: Error; +} + +// Message types from SDK stream +export type SdkMessage = + | AssistantMessage + | ResultMessage + | ToolUseMessage + | ToolResultMessage + | SystemInitMessage + | UserMessage; + +export interface SystemInitMessage { + type: 'system'; + subtype: 'init'; + model?: string; + permissionMode?: string; + mcp_servers?: Array<{ name: string; status: string }>; +} + +export interface UserMessage { + type: 'user'; +} + +// Dispatch result types for message processing +export type MessageDispatchResult = + | { action: 'continue' } + | { action: 'break'; result: string | null; cost: number } + | { action: 'throw'; error: Error }; + +export interface MessageDispatchContext { + turnCount: number; + execContext: ExecutionContext; + description: string; + colorFn: (text: string) => string; + useCleanOutput: boolean; +} diff --git a/src/audit/audit-session.ts b/src/audit/audit-session.ts index b3540a7a..ccb66363 100644 --- a/src/audit/audit-session.ts +++ b/src/audit/audit-session.ts @@ -13,7 +13,8 @@ import { AgentLogger } from './logger.js'; import { MetricsTracker } from './metrics-tracker.js'; -import { initializeAuditStructure, formatTimestamp, type SessionMetadata } from './utils.js'; +import { initializeAuditStructure, type SessionMetadata } from './utils.js'; +import { formatTimestamp } from '../utils/formatting.js'; import { SessionMutex } from '../utils/concurrency.js'; // Global mutex instance @@ -145,7 +146,7 @@ export class AuditSession { // Mutex-protected update to session.json const unlock = await sessionMutex.lock(this.sessionId); try { - // Reload metrics (in case of parallel updates) + // Reload inside mutex to prevent lost updates during parallel exploitation phase await this.metricsTracker.reload(); // Update metrics diff --git a/src/audit/logger.ts b/src/audit/logger.ts index 281563a4..c8e902da 100644 --- a/src/audit/logger.ts +++ b/src/audit/logger.ts @@ -15,10 +15,10 @@ import fs from 'fs'; import { generateLogPath, generatePromptPath, - atomicWrite, - formatTimestamp, type SessionMetadata, } from './utils.js'; +import { atomicWrite } from '../utils/file-io.js'; +import { formatTimestamp } from '../utils/formatting.js'; interface LogEvent { type: string; @@ -96,22 +96,13 @@ export class AgentLogger { return; } - // Write and flush immediately (crash-safe) const needsDrain = !this.stream.write(text, 'utf8', (error) => { - if (error) { - reject(error); - } + if (error) reject(error); }); if (needsDrain) { - // Buffer is full, wait for drain - const drainHandler = (): void => { - this.stream!.removeListener('drain', drainHandler); - resolve(); - }; - this.stream.once('drain', drainHandler); + this.stream.once('drain', resolve); } else { - // Buffer has space, resolve immediately resolve(); } }); diff --git a/src/audit/metrics-tracker.ts b/src/audit/metrics-tracker.ts index 54ec9737..3e552ef5 100644 --- a/src/audit/metrics-tracker.ts +++ b/src/audit/metrics-tracker.ts @@ -13,13 +13,12 @@ import { generateSessionJsonPath, - atomicWrite, - readJson, - fileExists, - formatTimestamp, - calculatePercentage, type SessionMetadata, } from './utils.js'; +import { atomicWrite, readJson, fileExists } from '../utils/file-io.js'; +import { formatTimestamp, calculatePercentage } from '../utils/formatting.js'; +import { AGENT_PHASE_MAP, type PhaseName } from '../session-manager.js'; +import type { AgentName } from '../types/index.js'; interface AttemptData { attempt_number: number; @@ -152,16 +151,14 @@ export class MetricsTracker { } // Initialize agent metrics if not exists - if (!this.data.metrics.agents[agentName]) { - this.data.metrics.agents[agentName] = { - status: 'in-progress', - attempts: [], - final_duration_ms: 0, - total_cost_usd: 0, - }; - } - - const agent = this.data.metrics.agents[agentName]!; + const existingAgent = this.data.metrics.agents[agentName]; + const agent = existingAgent ?? { + status: 'in-progress' as const, + attempts: [], + final_duration_ms: 0, + total_cost_usd: 0, + }; + this.data.metrics.agents[agentName] = agent; // Add attempt to array const attempt: AttemptData = { @@ -255,36 +252,19 @@ export class MetricsTracker { private calculatePhaseMetrics( successfulAgents: Array<[string, AgentMetrics]> ): Record { - const phases: Record = { + const phases: Record = { 'pre-recon': [], - recon: [], + 'recon': [], 'vulnerability-analysis': [], - exploitation: [], - reporting: [], - }; - - // Map agents to phases - const agentPhaseMap: Record = { - 'pre-recon': 'pre-recon', - recon: 'recon', - 'injection-vuln': 'vulnerability-analysis', - 'xss-vuln': 'vulnerability-analysis', - 'auth-vuln': 'vulnerability-analysis', - 'authz-vuln': 'vulnerability-analysis', - 'ssrf-vuln': 'vulnerability-analysis', - 'injection-exploit': 'exploitation', - 'xss-exploit': 'exploitation', - 'auth-exploit': 'exploitation', - 'authz-exploit': 'exploitation', - 'ssrf-exploit': 'exploitation', - report: 'reporting', + 'exploitation': [], + 'reporting': [], }; - // Group agents by phase + // Group agents by phase using imported AGENT_PHASE_MAP for (const [agentName, agentData] of successfulAgents) { - const phase = agentPhaseMap[agentName]; - if (phase && phases[phase]) { - phases[phase]!.push(agentData); + const phase = AGENT_PHASE_MAP[agentName as AgentName]; + if (phase) { + phases[phase].push(agentData); } } @@ -296,7 +276,6 @@ export class MetricsTracker { if (agentList.length === 0) continue; const phaseDuration = agentList.reduce((sum, agent) => sum + agent.final_duration_ms, 0); - const phaseCost = agentList.reduce((sum, agent) => sum + agent.total_cost_usd, 0); phaseMetrics[phaseName] = { diff --git a/src/error-handling.ts b/src/error-handling.ts index c2b57666..9dd68316 100644 --- a/src/error-handling.ts +++ b/src/error-handling.ts @@ -37,11 +37,11 @@ export class PentestError extends Error { } // Centralized error logging function -export const logError = async ( +export async function logError( error: Error & { type?: PentestErrorType; retryable?: boolean; context?: PentestErrorContext }, contextMsg: string, sourceDir: string | null = null -): Promise => { +): Promise { const timestamp = new Date().toISOString(); const logEntry: LogEntry = { timestamp, @@ -80,13 +80,13 @@ export const logError = async ( } return logEntry; -}; +} // Handle tool execution errors -export const handleToolError = ( +export function handleToolError( toolName: string, error: Error & { code?: string } -): ToolErrorResult => { +): ToolErrorResult { const isRetryable = error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || @@ -105,13 +105,13 @@ export const handleToolError = ( { toolName, originalError: error.message, errorCode: error.code } ), }; -}; +} // Handle prompt loading errors -export const handlePromptError = ( +export function handlePromptError( promptName: string, error: Error -): PromptErrorResult => { +): PromptErrorResult { return { success: false, error: new PentestError( @@ -121,78 +121,63 @@ export const handlePromptError = ( { promptName, originalError: error.message } ), }; -}; +} -// Check if an error should trigger a retry for Claude agents -export const isRetryableError = (error: Error): boolean => { +// Patterns that indicate retryable errors +const RETRYABLE_PATTERNS = [ + // Network and connection errors + 'network', + 'connection', + 'timeout', + 'econnreset', + 'enotfound', + 'econnrefused', + // Rate limiting + 'rate limit', + '429', + 'too many requests', + // Server errors + 'server error', + '5xx', + 'internal server error', + 'service unavailable', + 'bad gateway', + // Claude API errors + 'mcp server', + 'model unavailable', + 'service temporarily unavailable', + 'api error', + 'terminated', + // Max turns + 'max turns', + 'maximum turns', +]; + +// Patterns that indicate non-retryable errors (checked before default) +const NON_RETRYABLE_PATTERNS = [ + 'authentication', + 'invalid prompt', + 'out of memory', + 'permission denied', + 'session limit reached', + 'invalid api key', +]; + +// Conservative retry classification - unknown errors don't retry (fail-safe default) +export function isRetryableError(error: Error): boolean { const message = error.message.toLowerCase(); - // Network and connection errors - always retryable - if ( - message.includes('network') || - message.includes('connection') || - message.includes('timeout') || - message.includes('econnreset') || - message.includes('enotfound') || - message.includes('econnrefused') - ) { - return true; - } - - // Rate limiting - retryable with longer backoff - if ( - message.includes('rate limit') || - message.includes('429') || - message.includes('too many requests') - ) { - return true; - } - - // Server errors - retryable - if ( - message.includes('server error') || - message.includes('5xx') || - message.includes('internal server error') || - message.includes('service unavailable') || - message.includes('bad gateway') - ) { - return true; - } - - // Claude API specific errors - retryable - if ( - message.includes('mcp server') || - message.includes('model unavailable') || - message.includes('service temporarily unavailable') || - message.includes('api error') || - message.includes('terminated') - ) { - return true; - } - - // Max turns without completion - retryable once - if (message.includes('max turns') || message.includes('maximum turns')) { - return true; - } - - // Non-retryable errors - if ( - message.includes('authentication') || - message.includes('invalid prompt') || - message.includes('out of memory') || - message.includes('permission denied') || - message.includes('session limit reached') || - message.includes('invalid api key') - ) { + // Check for explicit non-retryable patterns first + if (NON_RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern))) { return false; } - // Default to non-retryable for unknown errors - return false; -}; + // Check for retryable patterns + return RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern)); +} -// Get retry delay based on error type and attempt number -export const getRetryDelay = (error: Error, attempt: number): number => { +// Rate limit errors get longer base delay (30s) vs standard exponential backoff (2s) +export function getRetryDelay(error: Error, attempt: number): number { const message = error.message.toLowerCase(); // Rate limiting gets longer delays @@ -204,4 +189,4 @@ export const getRetryDelay = (error: Error, attempt: number): number => { const baseDelay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s const jitter = Math.random() * 1000; // 0-1s random return Math.min(baseDelay + jitter, 30000); // Max 30s -}; +} diff --git a/src/phases/pre-recon.ts b/src/phases/pre-recon.ts index 34f1580f..54300290 100644 --- a/src/phases/pre-recon.ts +++ b/src/phases/pre-recon.ts @@ -7,7 +7,7 @@ import { $, fs, path } from 'zx'; import chalk from 'chalk'; import { Timer } from '../utils/metrics.js'; -import { formatDuration } from '../audit/utils.js'; +import { formatDuration } from '../utils/formatting.js'; import { handleToolError, PentestError } from '../error-handling.js'; import { AGENTS } from '../session-manager.js'; import { runClaudePromptWithRetry } from '../ai/claude-executor.js'; @@ -40,11 +40,17 @@ interface PromptVariables { repoPath: string; } +// Discriminated union for Wave1 tool results - clearer than loose union types +type Wave1ToolResult = + | { kind: 'scan'; result: TerminalScanResult } + | { kind: 'skipped'; message: string } + | { kind: 'agent'; result: AgentResult }; + interface Wave1Results { - nmap: TerminalScanResult | string | AgentResult; - subfinder: TerminalScanResult | string | AgentResult; - whatweb: TerminalScanResult | string | AgentResult; - naabu?: TerminalScanResult | string | AgentResult; + nmap: Wave1ToolResult; + subfinder: Wave1ToolResult; + whatweb: Wave1ToolResult; + naabu?: Wave1ToolResult; codeAnalysis: AgentResult; } @@ -57,7 +63,7 @@ interface PreReconResult { report: string; } -// Pure function: Run terminal scanning tools +// Runs external security tools (nmap, whatweb, etc). Schemathesis requires schemas from code analysis. async function runTerminalScan(tool: ToolName, target: string, sourceDir: string | null = null): Promise { const timer = new Timer(`command-${tool}`); try { @@ -89,7 +95,7 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string return { tool: 'whatweb', output: result.stdout, status: 'success', duration: whatwebDuration }; } case 'schemathesis': { - // Only run if API schemas found + // Schemathesis depends on code analysis output - skip if no schemas found const schemasDir = path.join(sourceDir || '.', 'outputs', 'schemas'); if (await fs.pathExists(schemasDir)) { const schemaFiles = await fs.readdir(schemasDir) as string[]; @@ -146,6 +152,8 @@ async function runPreReconWave1( const operations: Promise[] = []; + const skippedResult = (message: string): Wave1ToolResult => ({ kind: 'skipped', message }); + // Skip external commands in pipeline testing mode if (pipelineTestingMode) { console.log(chalk.gray(' â­ī¸ Skipping external tools (pipeline testing mode)')); @@ -163,9 +171,9 @@ async function runPreReconWave1( ); const [codeAnalysis] = await Promise.all(operations); return { - nmap: 'Skipped (pipeline testing mode)', - subfinder: 'Skipped (pipeline testing mode)', - whatweb: 'Skipped (pipeline testing mode)', + nmap: skippedResult('Skipped (pipeline testing mode)'), + subfinder: skippedResult('Skipped (pipeline testing mode)'), + whatweb: skippedResult('Skipped (pipeline testing mode)'), codeAnalysis: codeAnalysis as AgentResult }; } else { @@ -192,9 +200,9 @@ async function runPreReconWave1( const [nmap, subfinder, whatweb, codeAnalysis] = await Promise.all(operations); return { - nmap: nmap as TerminalScanResult, - subfinder: subfinder as TerminalScanResult, - whatweb: whatweb as TerminalScanResult, + nmap: { kind: 'scan', result: nmap as TerminalScanResult }, + subfinder: { kind: 'scan', result: subfinder as TerminalScanResult }, + whatweb: { kind: 'scan', result: whatweb as TerminalScanResult }, codeAnalysis: codeAnalysis as AgentResult }; } @@ -250,17 +258,21 @@ async function runPreReconWave2( return response; } -// Helper type for stitching results -interface StitchableResult { - status?: string; - output?: string; - tool?: string; +// Extracts status and output from a Wave1 tool result +function extractResult(r: Wave1ToolResult | undefined): { status: string; output: string } { + if (!r) return { status: 'Skipped', output: 'No output' }; + switch (r.kind) { + case 'scan': + return { status: r.result.status || 'Skipped', output: r.result.output || 'No output' }; + case 'skipped': + return { status: 'Skipped', output: r.message }; + case 'agent': + return { status: r.result.success ? 'success' : 'error', output: 'See agent output' }; + } } -// Pure function: Stitch together pre-recon outputs and save to file -async function stitchPreReconOutputs(outputs: (StitchableResult | string | undefined)[], sourceDir: string): Promise { - const [nmap, subfinder, whatweb, naabu, codeAnalysis, ...additionalScans] = outputs; - +// Combines tool outputs into single deliverable. Falls back to reference if file missing. +async function stitchPreReconOutputs(wave1: Wave1Results, additionalScans: TerminalScanResult[], sourceDir: string): Promise { // Try to read the code analysis deliverable file let codeAnalysisContent = 'No analysis available'; try { @@ -269,62 +281,45 @@ async function stitchPreReconOutputs(outputs: (StitchableResult | string | undef } catch (error) { const err = error as Error; console.log(chalk.yellow(`âš ī¸ Could not read code analysis deliverable: ${err.message}`)); - // Fallback message if file doesn't exist codeAnalysisContent = 'Analysis located in deliverables/code_analysis_deliverable.md'; } - // Build additional scans section let additionalSection = ''; - if (additionalScans && additionalScans.length > 0) { + if (additionalScans.length > 0) { additionalSection = '\n## Authenticated Scans\n'; - additionalScans.forEach(scan => { - const s = scan as StitchableResult; - if (s && s.tool) { - additionalSection += ` -### ${s.tool.toUpperCase()} -Status: ${s.status} -${s.output} + for (const scan of additionalScans) { + additionalSection += ` +### ${scan.tool.toUpperCase()} +Status: ${scan.status} +${scan.output} `; - } - }); + } } - const nmapResult = nmap as StitchableResult | string | undefined; - const subfinderResult = subfinder as StitchableResult | string | undefined; - const whatwebResult = whatweb as StitchableResult | string | undefined; - const naabuResult = naabu as StitchableResult | string | undefined; - - const getStatus = (r: StitchableResult | string | undefined): string => { - if (!r) return 'Skipped'; - if (typeof r === 'string') return 'Skipped'; - return r.status || 'Skipped'; - }; - - const getOutput = (r: StitchableResult | string | undefined): string => { - if (!r) return 'No output'; - if (typeof r === 'string') return r; - return r.output || 'No output'; - }; + const nmap = extractResult(wave1.nmap); + const subfinder = extractResult(wave1.subfinder); + const whatweb = extractResult(wave1.whatweb); + const naabu = extractResult(wave1.naabu); const report = ` # Pre-Reconnaissance Report ## Port Discovery (naabu) -Status: ${getStatus(naabuResult)} -${getOutput(naabuResult)} +Status: ${naabu.status} +${naabu.output} ## Network Scanning (nmap) -Status: ${getStatus(nmapResult)} -${getOutput(nmapResult)} +Status: ${nmap.status} +${nmap.output} ## Subdomain Discovery (subfinder) -Status: ${getStatus(subfinderResult)} -${getOutput(subfinderResult)} +Status: ${subfinder.status} +${subfinder.output} ## Technology Detection (whatweb) -Status: ${getStatus(whatwebResult)} -${getOutput(whatwebResult)} +Status: ${whatweb.status} +${whatweb.output} ## Code Analysis ${codeAnalysisContent} ${additionalSection} @@ -375,16 +370,8 @@ export async function executePreReconPhase( console.log(chalk.green(' ✅ Wave 2 operations completed')); console.log(chalk.blue('📝 Stitching pre-recon outputs...')); - // Combine wave 1 and wave 2 results for stitching - const allResults: (StitchableResult | string | undefined)[] = [ - wave1Results.nmap as StitchableResult | string, - wave1Results.subfinder as StitchableResult | string, - wave1Results.whatweb as StitchableResult | string, - wave1Results.naabu as StitchableResult | string | undefined, - wave1Results.codeAnalysis as unknown as StitchableResult, - ...(wave2Results.schemathesis ? [wave2Results.schemathesis as StitchableResult] : []) - ]; - const preReconReport = await stitchPreReconOutputs(allResults, sourceDir); + const additionalScans = wave2Results.schemathesis ? [wave2Results.schemathesis] : []; + const preReconReport = await stitchPreReconOutputs(wave1Results, additionalScans, sourceDir); const duration = timer.stop(); console.log(chalk.green(`✅ Pre-reconnaissance complete in ${formatDuration(duration)}`)); diff --git a/src/queue-validation.ts b/src/queue-validation.ts index 1f84a1e1..ce21e1d5 100644 --- a/src/queue-validation.ts +++ b/src/queue-validation.ts @@ -6,6 +6,7 @@ import { fs, path } from 'zx'; import { PentestError } from './error-handling.js'; +import { asyncPipe } from './utils/functional.js'; export type VulnType = 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz'; @@ -16,9 +17,11 @@ interface VulnTypeConfigItem { type VulnTypeConfig = Record; +type ErrorMessageResolver = string | ((existence: FileExistence) => string); + interface ValidationRule { predicate: (existence: FileExistence) => boolean; - errorMessage: string; + errorMessage: ErrorMessageResolver; retryable: boolean; } @@ -94,40 +97,36 @@ const VULN_TYPE_CONFIG: VulnTypeConfig = Object.freeze({ }), }) as VulnTypeConfig; -// Functional composition utilities - async pipe for promise chain -type PipeFunction = (x: any) => any | Promise; - -const pipe = - (...fns: PipeFunction[]) => - (x: any): Promise => - fns.reduce(async (v, f) => f(await v), Promise.resolve(x)); - // Pure function to create validation rule -const createValidationRule = ( +function createValidationRule( predicate: (existence: FileExistence) => boolean, - errorMessage: string, + errorMessage: ErrorMessageResolver, retryable: boolean = true -): ValidationRule => Object.freeze({ predicate, errorMessage, retryable }); +): ValidationRule { + return Object.freeze({ predicate, errorMessage, retryable }); +} -// Validation rules for file existence (following QUEUE_VALIDATION_FLOW.md) +// Symmetric deliverable rules: queue and deliverable must exist together (prevents partial analysis from triggering exploitation) const fileExistenceRules: readonly ValidationRule[] = Object.freeze([ - // Rule 1: Neither deliverable nor queue exists - createValidationRule( - ({ deliverableExists, queueExists }) => deliverableExists || queueExists, - 'Analysis failed: Neither deliverable nor queue file exists. Analysis agent must create both files.' - ), - // Rule 2: Queue doesn't exist but deliverable exists - createValidationRule( - ({ deliverableExists, queueExists }) => !(!queueExists && deliverableExists), - 'Analysis incomplete: Deliverable exists but queue file missing. Analysis agent must create both files.' - ), - // Rule 3: Queue exists but deliverable doesn't exist createValidationRule( - ({ deliverableExists, queueExists }) => !(queueExists && !deliverableExists), - 'Analysis incomplete: Queue exists but deliverable file missing. Analysis agent must create both files.' + ({ deliverableExists, queueExists }) => deliverableExists && queueExists, + getExistenceErrorMessage ), ]); +// Generate appropriate error message based on which files are missing +function getExistenceErrorMessage(existence: FileExistence): string { + const { deliverableExists, queueExists } = existence; + + if (!deliverableExists && !queueExists) { + return 'Analysis failed: Neither deliverable nor queue file exists. Analysis agent must create both files.'; + } + if (!queueExists) { + return 'Analysis incomplete: Deliverable exists but queue file missing. Analysis agent must create both files.'; + } + return 'Analysis incomplete: Queue exists but deliverable file missing. Analysis agent must create both files.'; +} + // Pure function to create file paths const createPaths = ( vulnType: VulnType, @@ -170,7 +169,7 @@ const checkFileExistence = async ( }); }; -// Pure function to validate existence rules +// Validates deliverable/queue symmetry - both must exist or neither const validateExistenceRules = ( pathsWithExistence: PathsWithExistence | PathsWithError ): PathsWithExistence | PathsWithError => { @@ -182,9 +181,14 @@ const validateExistenceRules = ( const failedRule = fileExistenceRules.find((rule) => !rule.predicate(existence)); if (failedRule) { + const message = + typeof failedRule.errorMessage === 'function' + ? failedRule.errorMessage(existence) + : failedRule.errorMessage; + return { error: new PentestError( - `${failedRule.errorMessage} (${vulnType})`, + `${message} (${vulnType})`, 'validation', failedRule.retryable, { @@ -224,7 +228,7 @@ const validateQueueStructure = (content: string): QueueValidationResult => { } }; -// Pure function to read and validate queue content +// Queue parse failures are retryable - agent can fix malformed JSON on retry const validateQueueContent = async ( pathsWithExistence: PathsWithExistence | PathsWithError ): Promise => { @@ -273,7 +277,7 @@ const validateQueueContent = async ( } }; -// Pure function to determine exploitation decision +// Final decision: skip if queue says no vulns, proceed if vulns found, error otherwise const determineExploitationDecision = ( validatedData: PathsWithQueue | PathsWithError ): ExploitationDecision => { @@ -294,17 +298,18 @@ const determineExploitationDecision = ( }; // Main functional validation pipeline -export const validateQueueAndDeliverable = async ( +export async function validateQueueAndDeliverable( vulnType: VulnType, sourceDir: string -): Promise => - (await pipe( - () => createPaths(vulnType, sourceDir), +): Promise { + return asyncPipe( + createPaths(vulnType, sourceDir), checkFileExistence, validateExistenceRules, validateQueueContent, determineExploitationDecision - )(() => createPaths(vulnType, sourceDir))) as ExploitationDecision; + ); +} // Pure function to safely validate (returns result instead of throwing) export const safeValidateQueueAndDeliverable = async ( diff --git a/src/session-manager.ts b/src/session-manager.ts index fbf6d4d5..7a31d1a8 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -106,6 +106,26 @@ export const getParallelGroups = (): Readonly<{ vuln: AgentName[]; exploit: Agen exploit: ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit'] }); +// Phase names for metrics aggregation +export type PhaseName = 'pre-recon' | 'recon' | 'vulnerability-analysis' | 'exploitation' | 'reporting'; + +// Map agents to their corresponding phases (single source of truth) +export const AGENT_PHASE_MAP: Readonly> = Object.freeze({ + 'pre-recon': 'pre-recon', + 'recon': 'recon', + 'injection-vuln': 'vulnerability-analysis', + 'xss-vuln': 'vulnerability-analysis', + 'auth-vuln': 'vulnerability-analysis', + 'authz-vuln': 'vulnerability-analysis', + 'ssrf-vuln': 'vulnerability-analysis', + 'injection-exploit': 'exploitation', + 'xss-exploit': 'exploitation', + 'auth-exploit': 'exploitation', + 'authz-exploit': 'exploitation', + 'ssrf-exploit': 'exploitation', + 'report': 'reporting', +}); + // Generate a session-based log folder path (used by claude-executor.ts) export const generateSessionLogPath = (webUrl: string, sessionId: string): string => { const hostname = new URL(webUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-'); diff --git a/src/shannon.ts b/src/shannon.ts index e493047e..8eddcdb3 100644 --- a/src/shannon.ts +++ b/src/shannon.ts @@ -17,6 +17,7 @@ import { checkToolAvailability, handleMissingTools } from './tool-checker.js'; // Session import { AGENTS, getParallelGroups } from './session-manager.js'; +import { getPromptNameForAgent } from './types/agents.js'; import type { AgentName, PromptName } from './types/index.js'; // Setup and Deliverables @@ -32,7 +33,8 @@ import { assembleFinalReport } from './phases/reporting.js'; // Utils import { timingResults, displayTimingSummary, Timer } from './utils/metrics.js'; -import { formatDuration, generateAuditPath } from './audit/utils.js'; +import { formatDuration } from './utils/formatting.js'; +import { generateAuditPath } from './audit/utils.js'; import type { SessionMetadata } from './audit/utils.js'; import { AuditSession } from './audit/audit-session.js'; @@ -86,6 +88,7 @@ async function saveSessions(store: SessionStore): Promise { await fs.writeJson(STORE_PATH, store, { spaces: 2 }); } +// Session prevents concurrent runs on same repo - different repos can run in parallel async function createSession(webUrl: string, repoPath: string): Promise { const store = await loadSessions(); @@ -155,32 +158,26 @@ interface ParallelAgentResult { error?: string | undefined; } -// Configure zx to disable timeouts (let tools run as long as needed) -$.timeout = 0; +type VulnType = 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz'; -// Helper function to get prompt name from agent name -const getPromptName = (agentName: AgentName): PromptName => { - const mappings: Record = { - 'pre-recon': 'pre-recon-code', - 'recon': 'recon', - 'injection-vuln': 'vuln-injection', - 'xss-vuln': 'vuln-xss', - 'auth-vuln': 'vuln-auth', - 'ssrf-vuln': 'vuln-ssrf', - 'authz-vuln': 'vuln-authz', - 'injection-exploit': 'exploit-injection', - 'xss-exploit': 'exploit-xss', - 'auth-exploit': 'exploit-auth', - 'ssrf-exploit': 'exploit-ssrf', - 'authz-exploit': 'exploit-authz', - 'report': 'report-executive' - }; +interface ParallelAgentConfig { + phaseType: 'vuln' | 'exploit'; + headerText: string; + specialistLabel: string; +} - return mappings[agentName] || agentName as PromptName; -}; +interface AgentExecutionContext { + sourceDir: string; + variables: PromptVariables; + distributedConfig: DistributedConfig | null; + pipelineTestingMode: boolean; + sessionMetadata: SessionMetadata; +} + +// Configure zx to disable timeouts (let tools run as long as needed) +$.timeout = 0; -// Get color function for agent -const getAgentColor = (agentName: AgentName): ChalkInstance => { +function getAgentColor(agentName: AgentName): ChalkInstance { const colorMap: Partial> = { 'injection-vuln': chalk.red, 'injection-exploit': chalk.red, @@ -194,11 +191,9 @@ const getAgentColor = (agentName: AgentName): ChalkInstance => { 'authz-exploit': chalk.green }; return colorMap[agentName] || chalk.cyan; -}; +} -/** - * Consolidate deliverables from target repo into the session folder - */ +// Non-fatal copy - failure logs warning but doesn't halt pipeline async function consolidateOutputs(sourceDir: string, sessionPath: string): Promise { const srcDeliverables = path.join(sourceDir, 'deliverables'); const destDeliverables = path.join(sessionPath, 'deliverables'); @@ -228,7 +223,7 @@ async function runAgent( sessionMetadata: SessionMetadata ): Promise { const agent = AGENTS[agentName]; - const promptName = getPromptName(agentName); + const promptName = getPromptNameForAgent(agentName); const prompt = await loadPrompt(promptName, variables, distributedConfig, pipelineTestingMode); return await runClaudePromptWithRetry( @@ -244,85 +239,68 @@ async function runAgent( } /** - * Run vulnerability agents in parallel + * Execute a single agent with retry logic */ -async function runParallelVuln( - sourceDir: string, - variables: PromptVariables, - distributedConfig: DistributedConfig | null, - pipelineTestingMode: boolean, - sessionMetadata: SessionMetadata -): Promise { - const { vuln: vulnAgents } = getParallelGroups(); - - console.log(chalk.cyan(`\nStarting ${vulnAgents.length} vulnerability analysis specialists in parallel...`)); - console.log(chalk.gray(' Specialists: ' + vulnAgents.join(', '))); - console.log(); - - const startTime = Date.now(); - - const results = await Promise.allSettled( - vulnAgents.map(async (agentName, index) => { - // Add 2-second stagger to prevent API overwhelm - await new Promise(resolve => setTimeout(resolve, index * 2000)); - - let lastError: Error | undefined; - let attempts = 0; - const maxAttempts = 3; - - while (attempts < maxAttempts) { - attempts++; - try { - const result = await runAgent( - agentName, - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); - - // Validate vulnerability analysis results - const vulnType = agentName.replace('-vuln', ''); - try { - const validation = await safeValidateQueueAndDeliverable(vulnType as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz', sourceDir); - - if (validation.success && validation.data) { - console.log(chalk.blue(`${agentName}: ${validation.data.shouldExploit ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` : 'No vulnerabilities found'}`)); - } - } catch { - // Validation failure is non-critical - } +async function executeAgentWithRetry( + agentName: AgentName, + context: AgentExecutionContext, + onSuccess?: (agentName: AgentName) => Promise +): Promise { + const { sourceDir, variables, distributedConfig, pipelineTestingMode, sessionMetadata } = context; + const maxAttempts = 3; + let lastError: Error | undefined; + let attempts = 0; + + while (attempts < maxAttempts) { + attempts++; + try { + const result = await runAgent( + agentName, + sourceDir, + variables, + distributedConfig, + pipelineTestingMode, + sessionMetadata + ); - return { - agentName, - success: result.success, - timing: result.duration, - cost: result.cost, - attempts - }; - } catch (error) { - lastError = error as Error; - if (attempts < maxAttempts) { - console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); - await new Promise(resolve => setTimeout(resolve, 5000)); - } - } + if (onSuccess) { + await onSuccess(agentName); } return { agentName, - success: false, - attempts, - error: lastError?.message || 'Unknown error' + success: result.success, + timing: result.duration, + cost: result.cost, + attempts }; - }) - ); + } catch (error) { + lastError = error as Error; + if (attempts < maxAttempts) { + console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } + } - const totalDuration = Date.now() - startTime; + return { + agentName, + success: false, + attempts, + error: lastError?.message || 'Unknown error' + }; +} - // Process and display results - console.log(chalk.cyan('\nVulnerability Analysis Results')); +/** + * Display results table for parallel agent execution + */ +function displayParallelResults( + results: PromiseSettledResult[], + agents: AgentName[], + headerText: string, + totalDuration: number +): ParallelAgentResult[] { + console.log(chalk.cyan(`\n${headerText}`)); console.log(chalk.gray('-'.repeat(80))); console.log(chalk.bold('Agent Status Attempt Duration Cost')); console.log(chalk.gray('-'.repeat(80))); @@ -330,7 +308,7 @@ async function runParallelVuln( const processedResults: ParallelAgentResult[] = []; results.forEach((result, index) => { - const agentName = vulnAgents[index]!; + const agentName = agents[index]!; const agentDisplay = agentName.padEnd(22); if (result.status === 'fulfilled') { @@ -371,159 +349,90 @@ async function runParallelVuln( console.log(chalk.gray('-'.repeat(80))); const successCount = processedResults.filter(r => r.success).length; - console.log(chalk.cyan(`Summary: ${successCount}/${vulnAgents.length} succeeded in ${formatDuration(totalDuration)}`)); + console.log(chalk.cyan(`Summary: ${successCount}/${agents.length} succeeded in ${formatDuration(totalDuration)}`)); return processedResults; } /** - * Run exploitation agents in parallel + * Run agents in parallel with retry logic and result display */ -async function runParallelExploit( - sourceDir: string, - variables: PromptVariables, - distributedConfig: DistributedConfig | null, - pipelineTestingMode: boolean, - sessionMetadata: SessionMetadata +async function runParallelAgents( + context: AgentExecutionContext, + config: ParallelAgentConfig ): Promise { - const { exploit: exploitAgents, vuln: vulnAgents } = getParallelGroups(); - - // Load validation module - const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js'); - - // Check eligibility - const eligibilityChecks = await Promise.all( - exploitAgents.map(async (agentName) => { - const vulnAgentName = agentName.replace('-exploit', '-vuln') as AgentName; - const vulnType = vulnAgentName.replace('-vuln', '') as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz'; - - const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir); - - if (!validation.success || !validation.data?.shouldExploit) { - console.log(chalk.gray(`Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`)); - return { agentName, eligible: false }; - } + const { sourceDir } = context; + const { phaseType, headerText, specialistLabel } = config; + const parallelGroups = getParallelGroups(); + const allAgents = parallelGroups[phaseType]; + + // For exploit phase, filter to only eligible agents + let agents: AgentName[]; + if (phaseType === 'exploit') { + const eligibilityChecks = await Promise.all( + allAgents.map(async (agentName) => { + const vulnAgentName = agentName.replace('-exploit', '-vuln') as AgentName; + const vulnType = vulnAgentName.replace('-vuln', '') as VulnType; + + const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir); + + if (!validation.success || !validation.data?.shouldExploit) { + console.log(chalk.gray(`Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`)); + return { agentName, eligible: false }; + } - console.log(chalk.blue(`${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`)); - return { agentName, eligible: true }; - }) - ); + console.log(chalk.blue(`${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`)); + return { agentName, eligible: true }; + }) + ); - const eligibleAgents = eligibilityChecks - .filter(check => check.eligible) - .map(check => check.agentName); + agents = eligibilityChecks + .filter(check => check.eligible) + .map(check => check.agentName); - if (eligibleAgents.length === 0) { - console.log(chalk.gray('No exploitation agents eligible (no vulnerabilities found)')); - return []; + if (agents.length === 0) { + console.log(chalk.gray('No exploitation agents eligible (no vulnerabilities found)')); + return []; + } + } else { + agents = allAgents; } - console.log(chalk.cyan(`\nStarting ${eligibleAgents.length} exploitation specialists in parallel...`)); - console.log(chalk.gray(' Specialists: ' + eligibleAgents.join(', '))); + console.log(chalk.cyan(`\nStarting ${agents.length} ${specialistLabel} in parallel...`)); + console.log(chalk.gray(' Specialists: ' + agents.join(', '))); console.log(); const startTime = Date.now(); - const results = await Promise.allSettled( - eligibleAgents.map(async (agentName, index) => { - await new Promise(resolve => setTimeout(resolve, index * 2000)); - - let lastError: Error | undefined; - let attempts = 0; - const maxAttempts = 3; - - while (attempts < maxAttempts) { - attempts++; + // Build onSuccess callback for vuln phase (validation logging) + const onSuccess = phaseType === 'vuln' + ? async (agentName: AgentName): Promise => { + const vulnType = agentName.replace('-vuln', '') as VulnType; try { - const result = await runAgent( - agentName, - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); - - return { - agentName, - success: result.success, - timing: result.duration, - cost: result.cost, - attempts - }; - } catch (error) { - lastError = error as Error; - if (attempts < maxAttempts) { - console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); - await new Promise(resolve => setTimeout(resolve, 5000)); + const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir); + if (validation.success && validation.data) { + const message = validation.data.shouldExploit + ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` + : 'No vulnerabilities found'; + console.log(chalk.blue(`${agentName}: ${message}`)); } + } catch { + // Validation failure is non-critical } } + : undefined; - return { - agentName, - success: false, - attempts, - error: lastError?.message || 'Unknown error' - }; + const results = await Promise.allSettled( + agents.map(async (agentName, index) => { + // Add 2-second stagger to prevent API overwhelm + await new Promise(resolve => setTimeout(resolve, index * 2000)); + return executeAgentWithRetry(agentName, context, onSuccess); }) ); const totalDuration = Date.now() - startTime; - // Process and display results - console.log(chalk.cyan('\nExploitation Results')); - console.log(chalk.gray('-'.repeat(80))); - console.log(chalk.bold('Agent Status Attempt Duration Cost')); - console.log(chalk.gray('-'.repeat(80))); - - const processedResults: ParallelAgentResult[] = []; - - results.forEach((result, index) => { - const agentName = eligibleAgents[index]!; - const agentDisplay = agentName.padEnd(22); - - if (result.status === 'fulfilled') { - const data = result.value; - processedResults.push(data); - - if (data.success) { - const duration = formatDuration(data.timing || 0); - const cost = `$${(data.cost || 0).toFixed(4)}`; - - console.log( - `${chalk.green(agentDisplay)} ${chalk.green('Success')} ` + - `${data.attempts}/3 ${duration.padEnd(11)} ${cost}` - ); - } else { - console.log( - `${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` + - `${data.attempts}/3 - -` - ); - if (data.error) { - console.log(chalk.gray(` Error: ${data.error.substring(0, 60)}...`)); - } - } - } else { - processedResults.push({ - agentName, - success: false, - attempts: 3, - error: String(result.reason) - }); - - console.log( - `${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` + - `3/3 - -` - ); - } - }); - - console.log(chalk.gray('-'.repeat(80))); - const successCount = processedResults.filter(r => r.success).length; - console.log(chalk.cyan(`Summary: ${successCount}/${eligibleAgents.length} succeeded in ${formatDuration(totalDuration)}`)); - - return processedResults; + return displayParallelResults(results, agents, headerText, totalDuration); } // Setup graceful cleanup on process signals @@ -677,13 +586,19 @@ async function main( const vulnTimer = new Timer('phase-3-vulnerability-analysis'); console.log(chalk.red.bold('\n🚨 PHASE 3: VULNERABILITY ANALYSIS')); - const vulnResults = await runParallelVuln( + const executionContext: AgentExecutionContext = { sourceDir, variables, distributedConfig, pipelineTestingMode, sessionMetadata - ); + }; + + const vulnResults = await runParallelAgents(executionContext, { + phaseType: 'vuln', + headerText: 'Vulnerability Analysis Results', + specialistLabel: 'vulnerability analysis specialists' + }); const vulnDuration = vulnTimer.stop(); console.log(chalk.green(`✅ Vulnerability analysis phase complete in ${formatDuration(vulnDuration)}`)); @@ -692,13 +607,11 @@ async function main( const exploitTimer = new Timer('phase-4-exploitation'); console.log(chalk.red.bold('\nđŸ’Ĩ PHASE 4: EXPLOITATION')); - const exploitResults = await runParallelExploit( - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); + const exploitResults = await runParallelAgents(executionContext, { + phaseType: 'exploit', + headerText: 'Exploitation Results', + specialistLabel: 'exploitation specialists' + }); const exploitDuration = exploitTimer.stop(); console.log(chalk.green(`✅ Exploitation phase complete in ${formatDuration(exploitDuration)}`)); diff --git a/src/types/agents.ts b/src/types/agents.ts index d3580958..a47256f6 100644 --- a/src/types/agents.ts +++ b/src/types/agents.ts @@ -47,10 +47,6 @@ export type PlaywrightAgent = export type AgentValidator = (sourceDir: string) => Promise; -export type AgentValidatorMap = Record; - -export type McpAgentMapping = Record; - export type AgentStatus = | 'pending' | 'in_progress' @@ -63,3 +59,26 @@ export interface AgentDefinition { displayName: string; prerequisites: AgentName[]; } + +/** + * Maps an agent name to its corresponding prompt file name. + */ +export function getPromptNameForAgent(agentName: AgentName): PromptName { + const mappings: Record = { + 'pre-recon': 'pre-recon-code', + 'recon': 'recon', + 'injection-vuln': 'vuln-injection', + 'xss-vuln': 'vuln-xss', + 'auth-vuln': 'vuln-auth', + 'ssrf-vuln': 'vuln-ssrf', + 'authz-vuln': 'vuln-authz', + 'injection-exploit': 'exploit-injection', + 'xss-exploit': 'exploit-xss', + 'auth-exploit': 'exploit-auth', + 'ssrf-exploit': 'exploit-ssrf', + 'authz-exploit': 'exploit-authz', + 'report': 'report-executive', + }; + + return mappings[agentName]; +} diff --git a/src/utils/concurrency.ts b/src/utils/concurrency.ts index e10de458..1edf03b7 100644 --- a/src/utils/concurrency.ts +++ b/src/utils/concurrency.ts @@ -31,13 +31,12 @@ type UnlockFunction = () => void; * } * ``` */ +// Promise-based mutex with queue semantics - safe for parallel agents on same session export class SessionMutex { // Map of sessionId -> Promise (represents active lock) private locks: Map> = new Map(); - /** - * Acquire lock for a session - */ + // Wait for existing lock, then acquire. Queue ensures FIFO ordering. async lock(sessionId: string): Promise { if (this.locks.has(sessionId)) { // Wait for existing lock to be released diff --git a/src/utils/file-io.ts b/src/utils/file-io.ts new file mode 100644 index 00000000..0f35c83f --- /dev/null +++ b/src/utils/file-io.ts @@ -0,0 +1,73 @@ +// 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. + +/** + * File I/O Utilities + * + * Core utility functions for file operations including atomic writes, + * directory creation, and JSON file handling. + */ + +import fs from 'fs/promises'; + +/** + * Ensure directory exists (idempotent, race-safe) + */ +export async function ensureDirectory(dirPath: string): Promise { + try { + await fs.mkdir(dirPath, { recursive: true }); + } catch (error) { + // Ignore EEXIST errors (race condition safe) + if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { + throw error; + } + } +} + +/** + * Atomic write using temp file + rename pattern + * Guarantees no partial writes or corruption on crash + */ +export async function atomicWrite(filePath: string, data: object | string): Promise { + const tempPath = `${filePath}.tmp`; + const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); + + try { + // Write to temp file + await fs.writeFile(tempPath, content, 'utf8'); + + // Atomic rename (POSIX guarantee: atomic on same filesystem) + await fs.rename(tempPath, filePath); + } catch (error) { + // Clean up temp file on failure + try { + await fs.unlink(tempPath); + } catch { + // Ignore cleanup errors + } + throw error; + } +} + +/** + * Read and parse JSON file + */ +export async function readJson(filePath: string): Promise { + const content = await fs.readFile(filePath, 'utf8'); + return JSON.parse(content) as T; +} + +/** + * Check if file exists + */ +export async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts new file mode 100644 index 00000000..3f60d20a --- /dev/null +++ b/src/utils/formatting.ts @@ -0,0 +1,60 @@ +// 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. + +/** + * Formatting Utilities + * + * Generic formatting functions for durations, timestamps, and percentages. + */ + +/** + * Format duration in milliseconds to human-readable string + */ +export function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms}ms`; + } + + const seconds = ms / 1000; + if (seconds < 60) { + return `${seconds.toFixed(1)}s`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; +} + +/** + * Format timestamp to ISO 8601 string + */ +export function formatTimestamp(timestamp: number = Date.now()): string { + return new Date(timestamp).toISOString(); +} + +/** + * Calculate percentage + */ +export function calculatePercentage(part: number, total: number): number { + if (total === 0) return 0; + return (part / total) * 100; +} + +/** + * Extract agent type from description string for display purposes + */ +export function extractAgentType(description: string): string { + if (description.includes('Pre-recon')) { + return 'pre-reconnaissance'; + } + if (description.includes('Recon')) { + return 'reconnaissance'; + } + if (description.includes('Report')) { + return 'report generation'; + } + return 'analysis'; +} diff --git a/src/utils/functional.ts b/src/utils/functional.ts new file mode 100644 index 00000000..ee1dac74 --- /dev/null +++ b/src/utils/functional.ts @@ -0,0 +1,29 @@ +// 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. + +/** + * Functional Programming Utilities + * + * Generic functional composition patterns for async operations. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type PipelineFunction = (x: any) => any | Promise; + +/** + * Async pipeline that passes result through a series of functions. + * Clearer than reduce-based pipe and easier to debug. + */ +export async function asyncPipe( + initial: unknown, + ...fns: PipelineFunction[] +): Promise { + let result = initial; + for (const fn of fns) { + result = await fn(result); + } + return result as TResult; +} diff --git a/src/utils/git-manager.ts b/src/utils/git-manager.ts index b48ad96c..969e8111 100644 --- a/src/utils/git-manager.ts +++ b/src/utils/git-manager.ts @@ -13,7 +13,57 @@ interface GitOperationResult { error?: Error; } -// Global git operations semaphore to prevent index.lock conflicts during parallel execution +/** + * Get list of changed files from git status --porcelain output + */ +async function getChangedFiles( + sourceDir: string, + operationDescription: string +): Promise { + const status = await executeGitCommandWithRetry( + ['git', 'status', '--porcelain'], + sourceDir, + operationDescription + ); + return status.stdout + .trim() + .split('\n') + .filter((line) => line.length > 0); +} + +/** + * Log a summary of changed files with truncation for long lists + */ +function logChangeSummary( + changes: string[], + messageWithChanges: string, + messageWithoutChanges: string, + color: typeof chalk.green, + maxToShow: number = 5 +): void { + if (changes.length > 0) { + console.log(color(messageWithChanges.replace('{count}', String(changes.length)))); + changes.slice(0, maxToShow).forEach((change) => console.log(chalk.gray(` ${change}`))); + if (changes.length > maxToShow) { + console.log(chalk.gray(` ... and ${changes.length - maxToShow} more files`)); + } + } else { + console.log(color(messageWithoutChanges)); + } +} + +/** + * Convert unknown error to GitOperationResult + */ +function toErrorResult(error: unknown): GitOperationResult { + const errMsg = error instanceof Error ? error.message : String(error); + return { + success: false, + error: error instanceof Error ? error : new Error(errMsg), + }; +} + +// Serializes git operations to prevent index.lock conflicts during parallel agent execution class GitSemaphore { private queue: Array<() => void> = []; private running: boolean = false; @@ -41,33 +91,38 @@ class GitSemaphore { const gitSemaphore = new GitSemaphore(); -// Execute git commands with retry logic for index.lock conflicts -export const executeGitCommandWithRetry = async ( +const GIT_LOCK_ERROR_PATTERNS = [ + 'index.lock', + 'unable to lock', + 'Another git process', + 'fatal: Unable to create', + 'fatal: index file', +]; + +function isGitLockError(errorMessage: string): boolean { + return GIT_LOCK_ERROR_PATTERNS.some((pattern) => errorMessage.includes(pattern)); +} + +// Retries git commands on lock conflicts with exponential backoff +export async function executeGitCommandWithRetry( commandArgs: string[], sourceDir: string, description: string, maxRetries: number = 5 -): Promise<{ stdout: string; stderr: string }> => { +): Promise<{ stdout: string; stderr: string }> { await gitSemaphore.acquire(); try { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - // For arrays like ['git', 'status', '--porcelain'], execute parts separately const [cmd, ...args] = commandArgs; const result = await $`cd ${sourceDir} && ${cmd} ${args}`; return result; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); - const isLockError = - errMsg.includes('index.lock') || - errMsg.includes('unable to lock') || - errMsg.includes('Another git process') || - errMsg.includes('fatal: Unable to create') || - errMsg.includes('fatal: index file'); - if (isLockError && attempt < maxRetries) { - const delay = Math.pow(2, attempt - 1) * 1000; // Exponential backoff: 1s, 2s, 4s, 8s, 16s + if (isGitLockError(errMsg) && attempt < maxRetries) { + const delay = Math.pow(2, attempt - 1) * 1000; console.log( chalk.yellow( ` âš ī¸ Git lock conflict during ${description} (attempt ${attempt}/${maxRetries}). Retrying in ${delay}ms...` @@ -80,84 +135,69 @@ export const executeGitCommandWithRetry = async ( throw error; } } - // Should never reach here but TypeScript needs a return throw new Error(`Git command failed after ${maxRetries} retries`); } finally { gitSemaphore.release(); } -}; +} -// Pure functions for Git workspace management -const cleanWorkspace = async ( +// Two-phase reset: hard reset (tracked files) + clean (untracked files) +export async function rollbackGitWorkspace( sourceDir: string, - reason: string = 'clean start' -): Promise => { - console.log(chalk.blue(` 🧹 Cleaning workspace for ${reason}`)); + reason: string = 'retry preparation' +): Promise { + console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`)); try { - // Check for uncommitted changes - const status = await $`cd ${sourceDir} && git status --porcelain`; - const hasChanges = status.stdout.trim().length > 0; - - if (hasChanges) { - // Show what we're about to remove - const changes = status.stdout - .trim() - .split('\n') - .filter((line) => line.length > 0); - console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`)); + const changes = await getChangedFiles(sourceDir, 'status check for rollback'); - await $`cd ${sourceDir} && git reset --hard HEAD`; - await $`cd ${sourceDir} && git clean -fd`; + await executeGitCommandWithRetry( + ['git', 'reset', '--hard', 'HEAD'], + sourceDir, + 'hard reset for rollback' + ); + await executeGitCommandWithRetry( + ['git', 'clean', '-fd'], + sourceDir, + 'cleaning untracked files for rollback' + ); - console.log( - chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`) - ); - changes.slice(0, 3).forEach((change) => console.log(chalk.gray(` ${change}`))); - if (changes.length > 3) { - console.log(chalk.gray(` ... and ${changes.length - 3} more files`)); - } - } else { - console.log(chalk.blue(` ✅ Workspace already clean (no changes to remove)`)); - } - return { success: true, hadChanges: hasChanges }; + logChangeSummary( + changes, + ' ✅ Rollback completed - removed {count} contaminated changes:', + ' ✅ Rollback completed - no changes to remove', + chalk.yellow, + 3 + ); + return { success: true }; } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.yellow(` âš ī¸ Workspace cleanup failed: ${errMsg}`)); - return { success: false, error: error instanceof Error ? error : new Error(errMsg) }; + const result = toErrorResult(error); + console.log(chalk.red(` ❌ Rollback failed after retries: ${result.error?.message}`)); + return result; } -}; +} -export const createGitCheckpoint = async ( +// Creates checkpoint before each attempt. First attempt preserves workspace; retries clean it. +export async function createGitCheckpoint( sourceDir: string, description: string, attempt: number -): Promise => { +): Promise { console.log(chalk.blue(` 📍 Creating checkpoint for ${description} (attempt ${attempt})`)); try { - // Only clean workspace on retry attempts (attempt > 1), not on first attempts - // This preserves deliverables between agents while still cleaning on actual retries + // First attempt: preserve existing deliverables. Retries: clean workspace to prevent pollution if (attempt > 1) { - const cleanResult = await cleanWorkspace(sourceDir, `${description} (retry cleanup)`); + const cleanResult = await rollbackGitWorkspace(sourceDir, `${description} (retry cleanup)`); if (!cleanResult.success) { - const errMsg = cleanResult.error?.message || 'Unknown error'; console.log( - chalk.yellow(` âš ī¸ Workspace cleanup failed, continuing anyway: ${errMsg}`) + chalk.yellow(` âš ī¸ Workspace cleanup failed, continuing anyway: ${cleanResult.error?.message}`) ); } } - // Check for uncommitted changes with retry logic - const status = await executeGitCommandWithRetry( - ['git', 'status', '--porcelain'], - sourceDir, - 'status check' - ); - const hasChanges = status.stdout.trim().length > 0; + const changes = await getChangedFiles(sourceDir, 'status check'); + const hasChanges = changes.length > 0; - // Stage changes with retry logic await executeGitCommandWithRetry(['git', 'add', '-A'], sourceDir, 'staging changes'); - - // Create commit with retry logic await executeGitCommandWithRetry( ['git', 'commit', '-m', `📍 Checkpoint: ${description} (attempt ${attempt})`, '--allow-empty'], sourceDir, @@ -171,106 +211,54 @@ export const createGitCheckpoint = async ( } return { success: true }; } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.yellow(` âš ī¸ Checkpoint creation failed after retries: ${errMsg}`)); - return { success: false, error: error instanceof Error ? error : new Error(errMsg) }; + const result = toErrorResult(error); + console.log(chalk.yellow(` âš ī¸ Checkpoint creation failed after retries: ${result.error?.message}`)); + return result; } -}; +} -export const commitGitSuccess = async ( +export async function commitGitSuccess( sourceDir: string, description: string -): Promise => { +): Promise { console.log(chalk.green(` 💾 Committing successful results for ${description}`)); try { - // Check what we're about to commit with retry logic - const status = await executeGitCommandWithRetry( - ['git', 'status', '--porcelain'], - sourceDir, - 'status check for success commit' - ); - const changes = status.stdout - .trim() - .split('\n') - .filter((line) => line.length > 0); + const changes = await getChangedFiles(sourceDir, 'status check for success commit'); - // Stage changes with retry logic await executeGitCommandWithRetry( ['git', 'add', '-A'], sourceDir, 'staging changes for success commit' ); - - // Create success commit with retry logic await executeGitCommandWithRetry( ['git', 'commit', '-m', `✅ ${description}: completed successfully`, '--allow-empty'], sourceDir, 'creating success commit' ); - if (changes.length > 0) { - console.log(chalk.green(` ✅ Success commit created with ${changes.length} file changes:`)); - changes.slice(0, 5).forEach((change) => console.log(chalk.gray(` ${change}`))); - if (changes.length > 5) { - console.log(chalk.gray(` ... and ${changes.length - 5} more files`)); - } - } else { - console.log(chalk.green(` ✅ Empty success commit created (agent made no file changes)`)); - } + logChangeSummary( + changes, + ' ✅ Success commit created with {count} file changes:', + ' ✅ Empty success commit created (agent made no file changes)', + chalk.green, + 5 + ); return { success: true }; } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.yellow(` âš ī¸ Success commit failed after retries: ${errMsg}`)); - return { success: false, error: error instanceof Error ? error : new Error(errMsg) }; + const result = toErrorResult(error); + console.log(chalk.yellow(` âš ī¸ Success commit failed after retries: ${result.error?.message}`)); + return result; } -}; +} -export const rollbackGitWorkspace = async ( - sourceDir: string, - reason: string = 'retry preparation' -): Promise => { - console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`)); +/** + * Get current git commit hash + */ +export async function getGitCommitHash(sourceDir: string): Promise { try { - // Show what we're about to remove with retry logic - const status = await executeGitCommandWithRetry( - ['git', 'status', '--porcelain'], - sourceDir, - 'status check for rollback' - ); - const changes = status.stdout - .trim() - .split('\n') - .filter((line) => line.length > 0); - - // Reset to HEAD with retry logic - await executeGitCommandWithRetry( - ['git', 'reset', '--hard', 'HEAD'], - sourceDir, - 'hard reset for rollback' - ); - - // Clean untracked files with retry logic - await executeGitCommandWithRetry( - ['git', 'clean', '-fd'], - sourceDir, - 'cleaning untracked files for rollback' - ); - - if (changes.length > 0) { - console.log( - chalk.yellow(` ✅ Rollback completed - removed ${changes.length} contaminated changes:`) - ); - changes.slice(0, 3).forEach((change) => console.log(chalk.gray(` ${change}`))); - if (changes.length > 3) { - console.log(chalk.gray(` ... and ${changes.length - 3} more files`)); - } - } else { - console.log(chalk.yellow(` ✅ Rollback completed - no changes to remove`)); - } - return { success: true }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.red(` ❌ Rollback failed after retries: ${errMsg}`)); - return { success: false, error: error instanceof Error ? error : new Error(errMsg) }; + const result = await $`cd ${sourceDir} && git rev-parse HEAD`; + return result.stdout.trim(); + } catch { + return null; } -}; +} diff --git a/src/utils/metrics.ts b/src/utils/metrics.ts index 93ec4565..01cf79c5 100644 --- a/src/utils/metrics.ts +++ b/src/utils/metrics.ts @@ -5,7 +5,7 @@ // as published by the Free Software Foundation. import chalk from 'chalk'; -import { formatDuration } from '../audit/utils.js'; +import { formatDuration } from './formatting.js'; // Timing utilities From 49e53b9e0cbabf4987be23568d849a59fccbbdc4 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 12:15:41 -0800 Subject: [PATCH 02/24] feat: add Claude Code custom commands for debug and review --- .claude/commands/debug.md | 148 +++++++++++++++++++++++++++++++++++++ .claude/commands/review.md | 120 ++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 .claude/commands/debug.md create mode 100644 .claude/commands/review.md diff --git a/.claude/commands/debug.md b/.claude/commands/debug.md new file mode 100644 index 00000000..e69da00b --- /dev/null +++ b/.claude/commands/debug.md @@ -0,0 +1,148 @@ +--- +description: Systematically debug errors using context analysis and structured recovery +--- + +You are debugging an issue. Follow this structured approach to avoid spinning in circles. + +## Step 1: Capture Error Context +- Read the full error message and stack trace +- Identify the layer where the error originated: + - **CLI/Args** - Input validation, path resolution + - **Config Parsing** - YAML parsing, JSON Schema validation + - **Session Management** - Mutex, session.json, lock files + - **Audit System** - Logging, metrics tracking, atomic writes + - **Claude SDK** - Agent execution, MCP servers, turn handling + - **Git Operations** - Checkpoints, rollback, commit + - **Tool Execution** - nmap, subfinder, whatweb + - **Validation** - Deliverable checks, queue validation + +## Step 2: Check Relevant Logs + +**Session audit logs:** +```bash +# Find most recent session +ls -lt audit-logs/ | head -5 + +# Check session metrics and errors +cat audit-logs//session.json | jq '.errors, .agentMetrics' + +# Check agent execution logs +ls -lt audit-logs//agents/ +cat audit-logs//agents/.log +``` + +**Check for lock file issues:** +```bash +# Session lock file (prevents concurrent runs) +cat .shannon-store.json + +# Remove if stale (no active session) +rm .shannon-store.json +``` + +## Step 3: Trace the Call Path + +For Shannon, trace through these layers: + +1. **CLI Entry** → `src/shannon.ts` - Argument parsing, session setup +2. **Config** → `src/config-parser.ts` - YAML loading, schema validation +3. **Session** → `src/session-manager.ts` - Agent definitions, execution order +4. **Audit** → `src/audit/audit-session.ts` - Logging facade, metrics tracking +5. **Executor** → `src/ai/claude-executor.ts` - SDK calls, MCP setup, retry logic +6. **Phases** → `src/phases/pre-recon.ts`, `reporting.ts` - Phase-specific logic +7. **Validation** → `src/queue-validation.ts` - Deliverable checks + +## Step 4: Identify Root Cause + +**Common Shannon-specific issues:** + +| Symptom | Likely Cause | Fix | +|---------|--------------|-----| +| "A session is already running" | Stale `.shannon-store.json` | Delete the lock file | +| Agent hangs indefinitely | MCP server crashed, Playwright timeout | Check Playwright logs in `/tmp/playwright-*` | +| "Validation failed: Missing deliverable" | Agent didn't create expected file | Check `deliverables/` dir, review prompt | +| Git checkpoint fails | Uncommitted changes, git lock | Run `git status`, remove `.git/index.lock` | +| "Session limit reached" | Claude API billing limit | Not retryable - check API usage | +| Parallel agents all fail | Shared resource contention | Check mutex usage, stagger startup timing | +| Cost/timing not tracked | Metrics not reloaded before update | Add `metricsTracker.reload()` before updates | +| session.json corrupted | Partial write during crash | Delete and restart, or restore from backup | +| YAML config rejected | Invalid schema or unsafe content | Run through AJV validator manually | +| Prompt variable not replaced | Missing `{{VARIABLE}}` in context | Check `prompt-manager.ts` interpolation | + +**MCP Server Issues:** +```bash +# Check if Playwright browsers are installed +npx playwright install chromium + +# Check MCP server startup (look for connection errors) +grep -i "mcp\|playwright" audit-logs//agents/*.log +``` + +**Git State Issues:** +```bash +# Check for uncommitted changes +git status + +# Check for git locks +ls -la .git/*.lock + +# View recent git operations from Shannon +git reflog | head -10 +``` + +## Step 5: Apply Fix with Retry Limit + +- **CRITICAL**: Track consecutive failed attempts +- After **3 consecutive failures** on the same issue, STOP and: + - Summarize what was tried + - Explain what's blocking progress + - Ask the user for guidance or additional context +- After a successful fix, reset the failure counter + +## Step 6: Validate the Fix + +**For code changes:** +```bash +# Compile TypeScript +npx tsc --noEmit + +# Quick validation run +shannon --pipeline-testing +``` + +**For audit/session issues:** +- Verify `session.json` is valid JSON after fix +- Check that atomic writes complete without errors +- Confirm mutex release in `finally` blocks + +**For agent issues:** +- Verify deliverable files are created in correct location +- Check that validation functions return expected results +- Confirm retry logic triggers on appropriate errors + +## Anti-Patterns to Avoid + +- Don't delete `session.json` without checking if session is active +- Don't modify git state while an agent is running +- Don't retry billing/quota errors (they're not retryable) +- Don't ignore PentestError type - it indicates the error category +- Don't make random changes hoping something works +- Don't fix symptoms without understanding root cause +- Don't bypass mutex protection for "quick fixes" + +## Quick Reference: Error Types + +| PentestError Type | Meaning | Retryable? | +|-------------------|---------|------------| +| `config` | Configuration file issues | No | +| `network` | Connection/timeout issues | Yes | +| `tool` | External tool (nmap, etc.) failed | Yes | +| `prompt` | Claude SDK/API issues | Sometimes | +| `filesystem` | File read/write errors | Sometimes | +| `validation` | Deliverable validation failed | Yes (via retry) | +| `billing` | API quota/billing limit | No | +| `unknown` | Unexpected error | Depends | + +--- + +Now analyze the error and begin debugging systematically. diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 00000000..31b60a47 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,120 @@ +--- +description: Review code changes for Shannon-specific patterns, security, and common mistakes +--- + +Review the current changes (staged or working directory) with focus on Shannon-specific patterns and common mistakes. + +## Step 1: Gather Changes +Run these commands to understand the scope: +```bash +git diff --stat HEAD +git diff HEAD +``` + +## Step 2: Check Shannon-Specific Patterns + +### Error Handling (CRITICAL) +- [ ] **All errors use PentestError** - Never use raw `Error`. Use `new PentestError(message, type, retryable, context)` +- [ ] **Error type is appropriate** - Use correct type: 'config', 'network', 'tool', 'prompt', 'filesystem', 'validation', 'billing', 'unknown' +- [ ] **Retryable flag matches behavior** - If error will be retried, set `retryable: true` +- [ ] **Context includes debugging info** - Add relevant paths, tool names, error codes to context object +- [ ] **Never swallow errors silently** - Always log or propagate errors + +### Audit System & Concurrency (CRITICAL) +- [ ] **Mutex protection for parallel operations** - Use `sessionMutex.lock()` when updating `session.json` during parallel agent execution +- [ ] **Reload before modify** - Always call `this.metricsTracker.reload()` before updating metrics in mutex block +- [ ] **Atomic writes for session.json** - Use `atomicWrite()` for session metadata, never `fs.writeFile()` directly +- [ ] **Stream drain handling** - Log writes must wait for buffer drain before resolving +- [ ] **Semaphore release in finally** - Git semaphore must be released in `finally` block + +### Claude SDK Integration (CRITICAL) +- [ ] **MCP server configuration** - Verify Playwright MCP uses `--isolated` and unique `--user-data-dir` +- [ ] **Prompt variable interpolation** - Check all `{{VARIABLE}}` placeholders are replaced +- [ ] **Turn counting** - Increment `turnCount` on assistant messages, not tool calls +- [ ] **Cost tracking** - Extract cost from final `result` message, track even on failure +- [ ] **API error detection** - Check for "session limit reached" (fatal) vs other errors + +### Configuration & Validation (CRITICAL) +- [ ] **FAILSAFE_SCHEMA for YAML** - Never use default schema (prevents code execution) +- [ ] **Security pattern detection** - Check for path traversal (`../`), HTML injection (`<>`), JavaScript URLs +- [ ] **Rule conflict detection** - Rules cannot appear in both `avoid` AND `focus` +- [ ] **Duplicate rule detection** - Same `type:url_path` cannot appear twice +- [ ] **JSON Schema validation before use** - Config must pass AJV validation + +### Session & Agent Management (CRITICAL) +- [ ] **Deliverable dependencies respected** - Exploitation agents only run if vulnerability queue exists AND has items +- [ ] **Queue validation before exploitation** - Use `safeValidateQueueAndDeliverable()` to check eligibility +- [ ] **Git checkpoint before agent run** - Create checkpoint for rollback on failure +- [ ] **Git rollback on retry** - Call `rollbackGitWorkspace()` before each retry attempt +- [ ] **Agent prerequisites checked** - Verify prerequisite agents completed before running dependent agent + +### Parallel Execution +- [ ] **Promise.allSettled for parallel agents** - Never use `Promise.all` (partial failures should not crash batch) +- [ ] **Staggered startup** - 2-second delay between parallel agent starts to prevent API throttle +- [ ] **Individual retry loops** - Each agent retries independently (3 attempts max) +- [ ] **Results aggregated correctly** - Handle both 'fulfilled' and 'rejected' results from `Promise.allSettled` + +## Step 3: TypeScript Safety + +### Type Assertions (WARNING) +- [ ] **No double casting** - Never use `as unknown as SomeType` (bypasses type safety) +- [ ] **Validate before casting** - JSON parsed data should be validated (JSON Schema) before `as Type` +- [ ] **Prefer type guards** - Use `instanceof` or property checks instead of assertions where possible + +### Null/Undefined Handling +- [ ] **Explicit null checks** - Use `if (x === null || x === undefined)` not truthy checks for critical paths +- [ ] **Nullish coalescing** - Use `??` for null/undefined, not `||` which also catches empty string/0 +- [ ] **Optional chaining** - Use `?.` for nested property access on potentially undefined objects + +### Imports & Types +- [ ] **Type imports** - Use `import type { ... }` for type-only imports +- [ ] **No implicit any** - All function parameters and returns must have explicit types +- [ ] **Readonly for constants** - Use `Object.freeze()` and `Readonly<>` for immutable data + +## Step 4: Security Review + +### Defensive Tool Security +- [ ] **No credentials in logs** - Check that passwords, tokens, TOTP secrets are not logged to audit files +- [ ] **Config file size limit** - Ensure 1MB max for config files (DoS prevention) +- [ ] **Safe shell execution** - Command arguments must be escaped/sanitized + +### Code Injection Prevention +- [ ] **YAML safe parsing** - FAILSAFE_SCHEMA only +- [ ] **No eval/Function** - Never use dynamic code evaluation +- [ ] **Input validation at boundaries** - URLs, paths validated before use + +## Step 5: Common Mistakes to Avoid + +### Anti-Patterns Found in Codebase +- [ ] **Catch + re-throw without context** - Don't just `throw error`, wrap with additional context +- [ ] **Silent failures in session loading** - Corrupted session files should warn user, not silently reset +- [ ] **Duplicate retry logic** - Don't implement retry at both caller and callee level +- [ ] **Hardcoded error message matching** - Prefer error codes over regex on error.message +- [ ] **Missing timeout on long operations** - Git operations and API calls should have timeouts + +### Code Quality +- [ ] **No dead code added** - Remove unused imports, functions, variables +- [ ] **No over-engineering** - Don't add abstractions for single-use operations +- [ ] **Comments only where needed** - Self-documenting code preferred over excessive comments +- [ ] **Consistent file naming** - kebab-case for files (e.g., `queue-validation.ts`) + +## Step 6: Provide Feedback + +For each issue found: +1. **Location**: File and line number +2. **Issue**: What's wrong and why it matters +3. **Fix**: How to correct it (with code example if helpful) +4. **Severity**: Critical / Warning / Suggestion + +### Severity Definitions +- **Critical**: Will cause bugs, crashes, data loss, or security issues +- **Warning**: Code smell, inconsistent pattern, or potential future issue +- **Suggestion**: Style improvement or minor enhancement + +Summarize with: +- Total issues by severity +- Overall assessment (Ready to commit / Needs fixes / Needs discussion) + +--- + +Now review the current changes. From 6fdfdcb96aa783ffe925c9a1d5b996e88819329e Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 13:25:55 -0800 Subject: [PATCH 03/24] feat: add Temporal integration foundation (phase 1-2) - Add Temporal SDK dependencies (@temporalio/client, worker, workflow, activity) - Add shared types for pipeline state, metrics, and progress queries - Add classifyErrorForTemporal() for retry behavior classification - Add docker-compose for Temporal server with SQLite persistence --- docker/docker-compose.temporal.yml | 20 + package-lock.json | 2292 +++++++++++++++++++++++++--- package.json | 4 + src/error-handling.ts | 111 ++ src/temporal/shared.ts | 41 + 5 files changed, 2249 insertions(+), 219 deletions(-) create mode 100644 docker/docker-compose.temporal.yml create mode 100644 src/temporal/shared.ts diff --git a/docker/docker-compose.temporal.yml b/docker/docker-compose.temporal.yml new file mode 100644 index 00000000..b5688305 --- /dev/null +++ b/docker/docker-compose.temporal.yml @@ -0,0 +1,20 @@ +services: + temporal: + image: temporalio/auto-setup:latest + environment: + - DB=sqlite + - SQLITE_DB_PATH=/var/lib/temporal/temporal.db + ports: + - "7233:7233" # gRPC + - "8233:8233" # Web UI + volumes: + - temporal-data:/var/lib/temporal + healthcheck: + test: ["CMD", "temporal", "operator", "cluster", "health", "--address", "localhost:7233"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + +volumes: + temporal-data: diff --git a/package-lock.json b/package-lock.json index 412c0b41..63e6d715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "1.0.0", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", + "@temporalio/activity": "^1.11.0", + "@temporalio/client": "^1.11.0", + "@temporalio/worker": "^1.11.0", + "@temporalio/workflow": "^1.11.0", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "boxen": "^8.0.1", @@ -49,6 +53,37 @@ "zod": "^3.24.1" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -258,321 +293,1803 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@types/tinycolor2": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", - "license": "MIT" - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", + "node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } }, - "node_modules/boxen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", - "license": "MIT", + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^8.0.0", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "string-width": "^7.2.0", - "type-fest": "^4.21.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0" + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { - "node": ">=18" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "license": "MIT", + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, "engines": { - "node": ">=16" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", - "license": "MIT", + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@swc/core": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz", + "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.8", + "@swc/core-darwin-x64": "1.15.8", + "@swc/core-linux-arm-gnueabihf": "1.15.8", + "@swc/core-linux-arm64-gnu": "1.15.8", + "@swc/core-linux-arm64-musl": "1.15.8", + "@swc/core-linux-x64-gnu": "1.15.8", + "@swc/core-linux-x64-musl": "1.15.8", + "@swc/core-win32-arm64-msvc": "1.15.8", + "@swc/core-win32-ia32-msvc": "1.15.8", + "@swc/core-win32-x64-msvc": "1.15.8" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } } }, - "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", - "license": "MIT", + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz", + "integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=20" + "node": ">=10" } }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz", + "integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=10" } }, - "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz", + "integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz", + "integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz", + "integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz", + "integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz", + "integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz", + "integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz", + "integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz", + "integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@temporalio/activity": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.14.1.tgz", + "integrity": "sha512-wG2fTNgomhcKOzPY7mqhKqe8scawm4BvUYdgX1HJouHmVNRgtZurf2xQWJZQOTxWrsXfdoYqzohZLzxlNtcC5A==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.14.1", + "@temporalio/common": "1.14.1", + "abort-controller": "^3.0.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/client": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.14.1.tgz", + "integrity": "sha512-AfWSA0LYzBvDLFiFgrPWqTGGq1NGnF3d4xKnxf0PGxSmv5SLb/aqQ9lzHg4DJ5UNkHO4M/NwzdxzzoaR1J5F8Q==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.14.1", + "@temporalio/proto": "1.14.1", + "abort-controller": "^3.0.0", + "long": "^5.2.3", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.14.1.tgz", + "integrity": "sha512-y49wOm3AIEKZufIQ/QU5JhTSaHJIEkiUt5bGB0/uSzCg8P4g8Cz0XoVPSbDwuCix533O9cOKcliYq7Gzjt/sIA==", + "license": "MIT", + "dependencies": { + "@temporalio/proto": "1.14.1", + "long": "^5.2.3", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.1", + "proto3-json-serializer": "^2.0.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/core-bridge": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.14.1.tgz", + "integrity": "sha512-mrXXIFK5yNvsSZsTejLnL64JMuMliQjFKktSGITm2Ci7cWZ/ZTOVN6u+hCsUKfadYYv83jSuOC9Xe3z3RK273w==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.14.1" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/nexus": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.14.1.tgz", + "integrity": "sha512-51oTeJ8nntAMF8boFSlzVdHlyC7y/LaLQPZMjEEOV2pi8O9yOI7GZvYDIAHhY8Z8AcDVgbXb8x0BbkjkwNiUiQ==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.14.1", + "@temporalio/common": "1.14.1", + "@temporalio/proto": "1.14.1", + "long": "^5.2.3", + "nexus-rpc": "^0.0.1" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.14.1.tgz", + "integrity": "sha512-mCsUommDPXbXbBu60p1g4jpSqVb+GNR67yR0uKTU8ARb4qVZQo7SQnOUaneoxDERDXuR/yIjVCektMm+7Myb+A==", + "license": "MIT", + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/worker": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.14.1.tgz", + "integrity": "sha512-wFfN5gc03eq1bYAuJNsG9a1iWBG6hL9zAfYbxiJdshPhpHa82BtHGvXD447oT2BX3zqI+Jf2b0m/N0wgkW6wyQ==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@swc/core": "^1.3.102", + "@temporalio/activity": "1.14.1", + "@temporalio/client": "1.14.1", + "@temporalio/common": "1.14.1", + "@temporalio/core-bridge": "1.14.1", + "@temporalio/nexus": "1.14.1", + "@temporalio/proto": "1.14.1", + "@temporalio/workflow": "1.14.1", + "abort-controller": "^3.0.0", + "heap-js": "^2.6.0", + "memfs": "^4.6.0", + "nexus-rpc": "^0.0.1", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "^7.2.5", + "rxjs": "^7.8.1", + "source-map": "^0.7.4", + "source-map-loader": "^4.0.2", + "supports-color": "^8.1.1", + "swc-loader": "^0.2.3", + "unionfs": "^4.5.1", + "webpack": "^5.94.0" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@temporalio/workflow": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.14.1.tgz", + "integrity": "sha512-MzshcoRo8zjQYa9WHrv3XC8LVvpRNSVaW3kOSTmHuTYDh/7be48WODOgs5yUpbnkpsw6rjVCDCgtB/K02cQwDg==", + "license": "MIT", + "dependencies": { + "@temporalio/common": "1.14.1", + "@temporalio/proto": "1.14.1", + "nexus-rpc": "^0.0.1" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "license": "MIT" }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/figlet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.3.tgz", + "integrity": "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gradient-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", + "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/memfs": { + "version": "4.51.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", + "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nexus-rpc": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.1.tgz", + "integrity": "sha512-hAWn8Hh2eewpB5McXR5EW81R3pR/ziuGhKCF3wFyUVCklanPqrIgMNr7jKCbzXeNVad0nUDfWpFRqh2u+zxQtw==", + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/fastify" + "url": "https://github.com/sponsors/feross" }, { - "type": "opencollective", - "url": "https://opencollective.com/fastify" + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "MIT" }, - "node_modules/figlet": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.3.tgz", - "integrity": "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { - "commander": "^14.0.0" - }, - "bin": { - "figlet": "bin/index.js" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 17.0.0" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "license": "MIT", - "engines": { - "node": ">=18" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/gradient-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", - "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", - "license": "MIT", + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", "dependencies": { - "chalk": "^5.3.0", - "tinygradient": "^1.1.5" - }, + "randombytes": "^2.1.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">=14" + "node": ">= 12" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/source-map-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.2.tgz", + "integrity": "sha512-oYwAqCuL0OZhBoSgmdrLa7mv9MjommVMiQIWgcztf+eS4+8BfcUee6nenFnDhKOhzAVnk5gpZdfnz1iiBv+5sg==", "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -609,6 +2126,121 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", + "integrity": "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -625,6 +2257,29 @@ "tinycolor2": "^1.0.0" } }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -655,9 +2310,130 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, + "node_modules/unionfs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.6.0.tgz", + "integrity": "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==", + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/watchpack": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz", + "integrity": "sha512-e6vZvY6xboSwLz2GD36c16+O/2Z6fKvIf4pOXptw2rY9MVwE/TXc6RGqxD3I3x0a28lwBY7DE+76uTPSsBrrCA==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -690,11 +2466,89 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index c01d110f..0b43f0cb 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", + "@temporalio/activity": "^1.11.0", + "@temporalio/client": "^1.11.0", + "@temporalio/worker": "^1.11.0", + "@temporalio/workflow": "^1.11.0", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "boxen": "^8.0.1", diff --git a/src/error-handling.ts b/src/error-handling.ts index 9dd68316..2b837baa 100644 --- a/src/error-handling.ts +++ b/src/error-handling.ts @@ -14,6 +14,12 @@ import type { PromptErrorResult, } from './types/errors.js'; +// Temporal error classification for ApplicationFailure wrapping +export interface TemporalErrorClassification { + type: string; + retryable: boolean; +} + // Custom error class for pentest operations export class PentestError extends Error { name = 'PentestError' as const; @@ -190,3 +196,108 @@ export function getRetryDelay(error: Error, attempt: number): number { const jitter = Math.random() * 1000; // 0-1s random return Math.min(baseDelay + jitter, 30000); // Max 30s } + +/** + * Classifies errors for Temporal workflow retry behavior. + * Returns error type and whether Temporal should retry. + * + * Used by activities to wrap errors in ApplicationFailure: + * - Retryable errors: Temporal retries with configured backoff + * - Non-retryable errors: Temporal fails immediately + */ +export function classifyErrorForTemporal(error: unknown): TemporalErrorClassification { + const message = (error instanceof Error ? error.message : String(error)).toLowerCase(); + + // === BILLING ERRORS (Retryable with long backoff) === + // Anthropic returns billing as 400 invalid_request_error + // Human can add credits, so retry with 5-30 min backoff + if ( + message.includes('billing_error') || + message.includes('credit balance is too low') || + message.includes('insufficient credits') || + message.includes('usage is blocked due to insufficient credits') || + message.includes('please visit plans & billing') || + message.includes('please visit plans and billing') || + message.includes('usage limit reached') || + message.includes('quota exceeded') || + message.includes('daily rate limit') || + message.includes('limit will reset') + ) { + return { type: 'BillingError', retryable: true }; + } + + // === PERMANENT ERRORS (Non-retryable) === + + // Authentication (401) - bad API key won't fix itself + if ( + message.includes('authentication') || + message.includes('api key') || + message.includes('401') || + message.includes('authentication_error') + ) { + return { type: 'AuthenticationError', retryable: false }; + } + + // Permission (403) - access won't be granted + if ( + message.includes('permission') || + message.includes('forbidden') || + message.includes('403') + ) { + return { type: 'PermissionError', retryable: false }; + } + + // Invalid Request (400) - malformed request is permanent + // Note: Checked AFTER billing since Anthropic billing is 400 + if ( + message.includes('invalid_request_error') || + message.includes('malformed') || + message.includes('validation') + ) { + return { type: 'InvalidRequestError', retryable: false }; + } + + // Request Too Large (413) - won't fit no matter how many retries + if ( + message.includes('request_too_large') || + message.includes('too large') || + message.includes('413') + ) { + return { type: 'RequestTooLargeError', retryable: false }; + } + + // Configuration errors - missing files need manual fix + if ( + message.includes('enoent') || + message.includes('no such file') || + message.includes('cli not installed') + ) { + return { type: 'ConfigurationError', retryable: false }; + } + + // Execution limits - max turns/budget reached + if ( + message.includes('max turns') || + message.includes('budget') || + message.includes('execution limit') || + message.includes('error_max_turns') || + message.includes('error_max_budget') + ) { + return { type: 'ExecutionLimitError', retryable: false }; + } + + // Invalid target URL - bad URL format won't fix itself + if ( + message.includes('invalid url') || + message.includes('invalid target') || + message.includes('malformed url') || + message.includes('invalid uri') + ) { + return { type: 'InvalidTargetError', retryable: false }; + } + + // === TRANSIENT ERRORS (Retryable) === + // Rate limits (429), server errors (5xx), network issues + // Let Temporal retry with configured backoff + return { type: 'TransientError', retryable: true }; +} diff --git a/src/temporal/shared.ts b/src/temporal/shared.ts new file mode 100644 index 00000000..578b3a35 --- /dev/null +++ b/src/temporal/shared.ts @@ -0,0 +1,41 @@ +import { defineQuery } from '@temporalio/workflow'; + +// === Types === + +export interface PipelineInput { + webUrl: string; + repoPath: string; + configPath?: string; + outputPath?: string; + pipelineTestingMode?: boolean; + workflowId?: string; // Added by client, used for audit correlation +} + +export interface AgentMetrics { + durationMs: number; + inputTokens: number | null; + outputTokens: number | null; + costUsd: number | null; + numTurns: number | null; +} + +export interface PipelineState { + status: 'running' | 'completed' | 'failed'; + currentPhase: string | null; + currentAgent: string | null; + completedAgents: string[]; + failedAgent: string | null; + error: string | null; + startTime: number; + agentMetrics: Record; +} + +// Extended state returned by getProgress query (includes computed fields) +export interface PipelineProgress extends PipelineState { + workflowId: string; + elapsedMs: number; +} + +// === Queries === + +export const getProgress = defineQuery('getProgress'); From 322e427c38e423e27eb9cf8a45aa312faa6990b4 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 13:35:17 -0800 Subject: [PATCH 04/24] feat: add Temporal activities for agent execution (phase 3) - Add activities.ts with heartbeat loop, git checkpoint/rollback, and error classification - Export runClaudePrompt, validateAgentOutput, ClaudePromptResult for Temporal use - Track attempt number via Temporal Context for accurate audit logging - Rollback git workspace before retry to ensure clean state --- src/ai/claude-executor.ts | 7 +- src/temporal/activities.ts | 278 +++++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 src/temporal/activities.ts diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index d6763793..91ef8330 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -31,7 +31,7 @@ declare global { var SHANNON_DISABLE_LOADER: boolean | undefined; } -interface ClaudePromptResult { +export interface ClaudePromptResult { result?: string | null; success: boolean; duration: number; @@ -148,7 +148,7 @@ async function writeErrorLog( } } -async function validateAgentOutput( +export async function validateAgentOutput( result: ClaudePromptResult, agentName: string | null, sourceDir: string @@ -193,7 +193,8 @@ async function validateAgentOutput( } // Low-level SDK execution. Handles message streaming, progress, and audit logging. -async function runClaudePrompt( +// Exported for Temporal activities to call single-attempt execution. +export async function runClaudePrompt( prompt: string, sourceDir: string, context: string = '', diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts new file mode 100644 index 00000000..0b631a79 --- /dev/null +++ b/src/temporal/activities.ts @@ -0,0 +1,278 @@ +// 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. + +/** + * Temporal activities for Shannon agent execution. + * + * Each activity wraps a single agent execution with: + * - Heartbeat loop (2s interval) to signal worker liveness + * - Git checkpoint/rollback/commit per attempt + * - Error classification for Temporal retry behavior + * - Audit session logging + * + * Temporal handles retries based on error classification: + * - Retryable: BillingError, TransientError (429, 5xx, network) + * - Non-retryable: AuthenticationError, PermissionError, ConfigurationError, etc. + */ + +import { heartbeat, ApplicationFailure, Context } from '@temporalio/activity'; +import chalk from 'chalk'; + +import { + runClaudePrompt, + validateAgentOutput, + type ClaudePromptResult, +} from '../ai/claude-executor.js'; +import { loadPrompt } from '../prompts/prompt-manager.js'; +import { parseConfig, distributeConfig } from '../config-parser.js'; +import { classifyErrorForTemporal } from '../error-handling.js'; +import { + createGitCheckpoint, + commitGitSuccess, + rollbackGitWorkspace, + getGitCommitHash, +} from '../utils/git-manager.js'; +import { getPromptNameForAgent } from '../types/agents.js'; +import { AuditSession } from '../audit/index.js'; +import type { AgentName } from '../types/agents.js'; +import type { AgentMetrics } from './shared.js'; +import type { DistributedConfig } from '../types/config.js'; +import type { SessionMetadata } from '../audit/utils.js'; + +const HEARTBEAT_INTERVAL_MS = 2000; // Must be < heartbeatTimeout (30s) + +/** + * Input for all agent activities. + * Matches PipelineInput but with required workflowId for audit correlation. + */ +export interface ActivityInput { + webUrl: string; + repoPath: string; + configPath?: string; + outputPath?: string; + pipelineTestingMode?: boolean; + workflowId: string; +} + +/** + * Core activity implementation. + * + * Executes a single agent with: + * 1. Heartbeat loop for worker liveness + * 2. Config loading (if configPath provided) + * 3. Audit session initialization + * 4. Prompt loading + * 5. Git checkpoint before execution + * 6. Agent execution (single attempt) + * 7. Output validation + * 8. Git commit on success, rollback on failure + * 9. Error classification for Temporal retry + */ +async function runAgentActivity( + agentName: AgentName, + input: ActivityInput +): Promise { + const { + webUrl, + repoPath, + configPath, + outputPath, + pipelineTestingMode = false, + workflowId, + } = input; + + const startTime = Date.now(); + + // Get attempt number from Temporal context (tracks retries automatically) + const attemptNumber = Context.current().info.attempt; + + // Heartbeat loop - signals worker is alive to Temporal server + const heartbeatInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + heartbeat({ agent: agentName, elapsedSeconds: elapsed, attempt: attemptNumber }); + }, HEARTBEAT_INTERVAL_MS); + + try { + // 1. Load config (if provided) + let distributedConfig: DistributedConfig | null = null; + if (configPath) { + try { + const config = await parseConfig(configPath); + distributedConfig = distributeConfig(config); + } catch (err) { + throw new Error(`Failed to load config ${configPath}: ${err instanceof Error ? err.message : String(err)}`); + } + } + + // 2. Build session metadata for audit + const sessionMetadata: SessionMetadata = { + id: workflowId, + webUrl, + repoPath, + ...(outputPath && { outputPath }), + }; + + // 3. Initialize audit session (idempotent, safe across retries) + const auditSession = new AuditSession(sessionMetadata); + await auditSession.initialize(); + + // 4. Load prompt + const promptName = getPromptNameForAgent(agentName); + const prompt = await loadPrompt( + promptName, + { webUrl, repoPath }, + distributedConfig, + pipelineTestingMode + ); + + // 5. Create git checkpoint before execution + await createGitCheckpoint(repoPath, agentName, attemptNumber); + await auditSession.startAgent(agentName, prompt, attemptNumber); + + // 6. Execute agent (single attempt - Temporal handles retries) + const result: ClaudePromptResult = await runClaudePrompt( + prompt, + repoPath, + '', // context + agentName, // description + agentName, + chalk.cyan, + sessionMetadata, + auditSession, + attemptNumber + ); + + // 7. Handle execution failure + if (!result.success) { + await rollbackGitWorkspace(repoPath, 'execution failure'); + await auditSession.endAgent(agentName, { + attemptNumber, + duration_ms: result.duration, + cost_usd: result.cost || 0, + success: false, + error: result.error || 'Execution failed', + }); + throw new Error(result.error || 'Agent execution failed'); + } + + // 8. Validate output + const validationPassed = await validateAgentOutput(result, agentName, repoPath); + if (!validationPassed) { + await rollbackGitWorkspace(repoPath, 'validation failure'); + await auditSession.endAgent(agentName, { + attemptNumber, + duration_ms: result.duration, + cost_usd: result.cost || 0, + success: false, + error: 'Output validation failed', + }); + throw new Error(`Agent ${agentName} failed output validation`); + } + + // 9. Success - commit and log + const commitHash = await getGitCommitHash(repoPath); + await auditSession.endAgent(agentName, { + attemptNumber, + duration_ms: result.duration, + cost_usd: result.cost || 0, + success: true, + ...(commitHash && { checkpoint: commitHash }), + }); + await commitGitSuccess(repoPath, agentName); + + // 10. Return metrics + return { + durationMs: Date.now() - startTime, + inputTokens: null, // Not currently exposed by SDK wrapper + outputTokens: null, + costUsd: result.cost ?? null, + numTurns: result.turns ?? null, + }; + } catch (error) { + // Rollback git workspace before Temporal retry to ensure clean state + try { + await rollbackGitWorkspace(repoPath, 'error recovery'); + } catch (rollbackErr) { + // Log but don't fail - rollback is best-effort + console.error(`Failed to rollback git workspace for ${agentName}:`, rollbackErr); + } + + // Classify error for Temporal retry behavior + const classified = classifyErrorForTemporal(error); + const message = error instanceof Error ? error.message : String(error); + + if (classified.retryable) { + // Temporal will retry with configured backoff + throw ApplicationFailure.create({ + message, + type: classified.type, + details: [{ agentName, attemptNumber, elapsed: Date.now() - startTime }], + }); + } else { + // Fail immediately - no retry + throw ApplicationFailure.nonRetryable(message, classified.type, [ + { agentName, attemptNumber, elapsed: Date.now() - startTime }, + ]); + } + } finally { + clearInterval(heartbeatInterval); + } +} + +// === Individual Agent Activity Exports === +// Each function is a thin wrapper around runAgentActivity with the agent name. + +export async function runPreReconAgent(input: ActivityInput): Promise { + return runAgentActivity('pre-recon', input); +} + +export async function runReconAgent(input: ActivityInput): Promise { + return runAgentActivity('recon', input); +} + +export async function runInjectionVulnAgent(input: ActivityInput): Promise { + return runAgentActivity('injection-vuln', input); +} + +export async function runXssVulnAgent(input: ActivityInput): Promise { + return runAgentActivity('xss-vuln', input); +} + +export async function runAuthVulnAgent(input: ActivityInput): Promise { + return runAgentActivity('auth-vuln', input); +} + +export async function runSsrfVulnAgent(input: ActivityInput): Promise { + return runAgentActivity('ssrf-vuln', input); +} + +export async function runAuthzVulnAgent(input: ActivityInput): Promise { + return runAgentActivity('authz-vuln', input); +} + +export async function runInjectionExploitAgent(input: ActivityInput): Promise { + return runAgentActivity('injection-exploit', input); +} + +export async function runXssExploitAgent(input: ActivityInput): Promise { + return runAgentActivity('xss-exploit', input); +} + +export async function runAuthExploitAgent(input: ActivityInput): Promise { + return runAgentActivity('auth-exploit', input); +} + +export async function runSsrfExploitAgent(input: ActivityInput): Promise { + return runAgentActivity('ssrf-exploit', input); +} + +export async function runAuthzExploitAgent(input: ActivityInput): Promise { + return runAgentActivity('authz-exploit', input); +} + +export async function runReportAgent(input: ActivityInput): Promise { + return runAgentActivity('report', input); +} From eb7eced23f7ea523332df27e6efb1469e0d6251a Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 13:45:50 -0800 Subject: [PATCH 05/24] feat: add Temporal workflow for 5-phase pipeline orchestration (phase 4) --- src/temporal/workflows.ts | 184 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/temporal/workflows.ts diff --git a/src/temporal/workflows.ts b/src/temporal/workflows.ts new file mode 100644 index 00000000..c07289c2 --- /dev/null +++ b/src/temporal/workflows.ts @@ -0,0 +1,184 @@ +// 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. + +/** + * Temporal workflow for Shannon pentest pipeline. + * + * Orchestrates the 5-phase penetration testing workflow: + * 1. Pre-Reconnaissance (sequential) + * 2. Reconnaissance (sequential) + * 3. Vulnerability Analysis (parallel - 5 agents) + * 4. Exploitation (parallel - 5 agents) + * 5. Reporting (sequential) + * + * Features: + * - Queryable state via getProgress + * - Automatic retry with backoff for transient/billing errors + * - Non-retryable classification for permanent errors + * - Audit correlation via workflowId + */ + +import { + proxyActivities, + setHandler, + workflowInfo, +} from '@temporalio/workflow'; +import type * as activities from './activities.js'; +import type { ActivityInput } from './activities.js'; +import { + getProgress, + type PipelineInput, + type PipelineState, + type PipelineProgress, +} from './shared.js'; + +// Activity proxy with retry configuration +const acts = proxyActivities({ + startToCloseTimeout: '2 hours', + heartbeatTimeout: '30 seconds', + retry: { + initialInterval: '5 minutes', + maximumInterval: '30 minutes', + backoffCoefficient: 2, + maximumAttempts: 50, + nonRetryableErrorTypes: [ + 'AuthenticationError', + 'PermissionError', + 'InvalidRequestError', + 'RequestTooLargeError', + 'ConfigurationError', + 'InvalidTargetError', + 'ExecutionLimitError', + ], + }, +}); + +export async function pentestPipelineWorkflow( + input: PipelineInput +): Promise { + const { workflowId } = workflowInfo(); + + // Workflow state (queryable) + const state: PipelineState = { + status: 'running', + currentPhase: null, + currentAgent: null, + completedAgents: [], + failedAgent: null, + error: null, + startTime: Date.now(), + agentMetrics: {}, + }; + + // Register query handler for real-time progress inspection + setHandler(getProgress, (): PipelineProgress => ({ + ...state, + workflowId, + elapsedMs: Date.now() - state.startTime, + })); + + // Build ActivityInput with required workflowId for audit correlation + // Activities require workflowId (non-optional), PipelineInput has it optional + // Use spread to conditionally include optional properties (exactOptionalPropertyTypes) + const activityInput: ActivityInput = { + webUrl: input.webUrl, + repoPath: input.repoPath, + workflowId, + ...(input.configPath !== undefined && { configPath: input.configPath }), + ...(input.outputPath !== undefined && { outputPath: input.outputPath }), + ...(input.pipelineTestingMode !== undefined && { + pipelineTestingMode: input.pipelineTestingMode, + }), + }; + + try { + // === Phase 1: Pre-Reconnaissance === + state.currentPhase = 'pre-recon'; + state.currentAgent = 'pre-recon'; + state.agentMetrics['pre-recon'] = + await acts.runPreReconAgent(activityInput); + state.completedAgents.push('pre-recon'); + + // === Phase 2: Reconnaissance === + state.currentPhase = 'recon'; + state.currentAgent = 'recon'; + state.agentMetrics['recon'] = await acts.runReconAgent(activityInput); + state.completedAgents.push('recon'); + + // === Phase 3: Vulnerability Analysis (Parallel) === + state.currentPhase = 'vulnerability-analysis'; + state.currentAgent = 'vuln-agents'; + + const vulnResults = await Promise.all([ + acts.runInjectionVulnAgent(activityInput), + acts.runXssVulnAgent(activityInput), + acts.runAuthVulnAgent(activityInput), + acts.runSsrfVulnAgent(activityInput), + acts.runAuthzVulnAgent(activityInput), + ]); + + const vulnAgents = [ + 'injection-vuln', + 'xss-vuln', + 'auth-vuln', + 'ssrf-vuln', + 'authz-vuln', + ] as const; + for (let i = 0; i < vulnAgents.length; i++) { + const agentName = vulnAgents[i]; + const metrics = vulnResults[i]; + if (agentName && metrics) { + state.agentMetrics[agentName] = metrics; + state.completedAgents.push(agentName); + } + } + + // === Phase 4: Exploitation (Parallel) === + state.currentPhase = 'exploitation'; + state.currentAgent = 'exploit-agents'; + + const exploitResults = await Promise.all([ + acts.runInjectionExploitAgent(activityInput), + acts.runXssExploitAgent(activityInput), + acts.runAuthExploitAgent(activityInput), + acts.runSsrfExploitAgent(activityInput), + acts.runAuthzExploitAgent(activityInput), + ]); + + const exploitAgents = [ + 'injection-exploit', + 'xss-exploit', + 'auth-exploit', + 'ssrf-exploit', + 'authz-exploit', + ] as const; + for (let i = 0; i < exploitAgents.length; i++) { + const agentName = exploitAgents[i]; + const metrics = exploitResults[i]; + if (agentName && metrics) { + state.agentMetrics[agentName] = metrics; + state.completedAgents.push(agentName); + } + } + + // === Phase 5: Reporting === + state.currentPhase = 'reporting'; + state.currentAgent = 'report'; + state.agentMetrics['report'] = await acts.runReportAgent(activityInput); + state.completedAgents.push('report'); + + // === Complete === + state.status = 'completed'; + state.currentPhase = null; + state.currentAgent = null; + return state; + } catch (error) { + state.status = 'failed'; + state.failedAgent = state.currentAgent; + state.error = error instanceof Error ? error.message : String(error); + throw error; + } +} From 05f8e2382c1ae777e28946817f3d24ed49f8b21c Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 14:27:59 -0800 Subject: [PATCH 06/24] feat: add Temporal worker, client, and query tools (phase 5) - Add worker.ts with workflow bundling and graceful shutdown - Add client.ts CLI to start pipelines with progress polling - Add query.ts CLI to inspect running workflow state - Fix buffer overflow by truncating error messages and stack traces - Skip git operations gracefully on non-git repositories - Add kill.sh/start.sh dev scripts and Dockerfile.worker --- CLAUDE.md | 20 +++ docker/Dockerfile.worker | 43 ++++++ docker/docker-compose.temporal.yml | 26 +++- package.json | 7 +- src/temporal/activities.ts | 35 ++++- src/temporal/client.ts | 209 +++++++++++++++++++++++++++++ src/temporal/query.ts | 155 +++++++++++++++++++++ src/temporal/worker.ts | 79 +++++++++++ src/utils/git-manager.ts | 37 ++++- 9 files changed, 601 insertions(+), 10 deletions(-) create mode 100644 docker/Dockerfile.worker create mode 100644 src/temporal/client.ts create mode 100644 src/temporal/query.ts create mode 100644 src/temporal/worker.ts diff --git a/CLAUDE.md b/CLAUDE.md index 72a65822..0bfd78d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -167,6 +167,26 @@ For detailed design, see `docs/unified-audit-system-design.md`. ## Development Notes +### Learning from Reference Implementations + +A working POC exists at `/Users/arjunmalleswaran/Code/shannon-pocs` that demonstrates the ideal Temporal + Claude Agent SDK integration. When implementing Temporal features, agents can ask questions in the chat, and the user will relay them to another Claude Code session working in that POC directory. + +**How to use this approach:** +1. When stuck or unsure about Temporal patterns, write a specific question in the chat +2. The user will ask an agent working on the POC to answer +3. The user relays the answer (code snippets, patterns, explanations) back +4. Apply the learned patterns to Shannon's codebase + +**Example questions to ask:** +- "How does the POC structure its workflow to handle parallel activities?" +- "Show me how heartbeats are implemented in the POC's activities" +- "What retry configuration does the POC use for long-running agent activities?" +- "How does the POC integrate Claude Agent SDK calls within Temporal activities?" + +**Current reference implementations:** +- **Temporal + Claude Agent SDK**: `/Users/arjunmalleswaran/Code/shannon-pocs` - working implementation demonstrating workflows, activities, worker setup, and SDK integration +- **Implementation plan**: `docs/temporal-implementation-plan.md` - detailed plan for Shannon integration + ### Key Design Patterns - **Configuration-Driven Architecture**: YAML configs with JSON Schema validation - **Modular Error Handling**: Categorized error types with retry logic diff --git a/docker/Dockerfile.worker b/docker/Dockerfile.worker new file mode 100644 index 00000000..aef429ce --- /dev/null +++ b/docker/Dockerfile.worker @@ -0,0 +1,43 @@ +# Wolfi-based worker for Shannon AI pentester +FROM cgr.dev/chainguard/wolfi-base:latest + +# Install Node.js 22, Python 3.12, Chromium, and dependencies +RUN apk add --no-cache \ + nodejs-22 \ + npm \ + python-3.12 \ + py3.12-pip \ + chromium \ + git \ + bash \ + curl + +# Install uvx for browser-use +RUN pip install uvx --break-system-packages + +# Create non-root user +RUN adduser -D -u 1000 pentest +WORKDIR /app + +# Copy package files first for better caching +COPY package*.json ./ + +# Install dependencies +RUN npm ci --omit=dev + +# Copy application code +COPY dist/ ./dist/ +COPY prompts/ ./prompts/ + +# Set ownership +RUN chown -R pentest:pentest /app + +# Switch to non-root user +USER pentest + +# Set Chromium path for Playwright +ENV CHROME_PATH=/usr/bin/chromium-browser +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Entry point +CMD ["node", "dist/temporal/worker.js"] diff --git a/docker/docker-compose.temporal.yml b/docker/docker-compose.temporal.yml index b5688305..4d9bb455 100644 --- a/docker/docker-compose.temporal.yml +++ b/docker/docker-compose.temporal.yml @@ -1,12 +1,10 @@ services: temporal: - image: temporalio/auto-setup:latest - environment: - - DB=sqlite - - SQLITE_DB_PATH=/var/lib/temporal/temporal.db + image: temporalio/temporal:latest + command: ["server", "start-dev", "--db-filename", "/var/lib/temporal/temporal.db", "--ip", "0.0.0.0"] ports: - "7233:7233" # gRPC - - "8233:8233" # Web UI + - "8233:8233" # Web UI (built-in) volumes: - temporal-data:/var/lib/temporal healthcheck: @@ -16,5 +14,23 @@ services: retries: 10 start_period: 30s + worker: + build: + context: .. + dockerfile: docker/Dockerfile.worker + environment: + - TEMPORAL_ADDRESS=temporal:7233 + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + depends_on: + temporal: + condition: service_healthy + volumes: + - ../deliverables:/app/deliverables + - ../prompts:/app/prompts + shm_size: 2gb + ipc: host + security_opt: + - seccomp:unconfined + volumes: temporal-data: diff --git a/package.json b/package.json index 0b43f0cb..b470c720 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,12 @@ "main": "./dist/shannon.js", "scripts": { "build": "tsc", - "start": "node ./dist/shannon.js" + "start": "node ./dist/shannon.js", + "temporal:server": "docker compose -f docker/docker-compose.temporal.yml up temporal -d", + "temporal:server:stop": "docker compose -f docker/docker-compose.temporal.yml down", + "temporal:worker": "node dist/temporal/worker.js", + "temporal:start": "node dist/temporal/client.js", + "temporal:query": "node dist/temporal/query.js" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.0", diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index 0b631a79..15df9351 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -21,6 +21,29 @@ import { heartbeat, ApplicationFailure, Context } from '@temporalio/activity'; import chalk from 'chalk'; +// Max lengths to prevent Temporal protobuf buffer overflow +const MAX_ERROR_MESSAGE_LENGTH = 2000; +const MAX_STACK_TRACE_LENGTH = 1000; + +/** + * Truncate error message to prevent buffer overflow in Temporal serialization. + */ +function truncateErrorMessage(message: string): string { + if (message.length <= MAX_ERROR_MESSAGE_LENGTH) { + return message; + } + return message.slice(0, MAX_ERROR_MESSAGE_LENGTH - 20) + '\n[truncated]'; +} + +/** + * Truncate stack trace on an ApplicationFailure to prevent buffer overflow. + */ +function truncateStackTrace(failure: ApplicationFailure): void { + if (failure.stack && failure.stack.length > MAX_STACK_TRACE_LENGTH) { + failure.stack = failure.stack.slice(0, MAX_STACK_TRACE_LENGTH) + '\n[stack truncated]'; + } +} + import { runClaudePrompt, validateAgentOutput, @@ -202,20 +225,26 @@ async function runAgentActivity( // Classify error for Temporal retry behavior const classified = classifyErrorForTemporal(error); - const message = error instanceof Error ? error.message : String(error); + // Truncate message to prevent protobuf buffer overflow + const rawMessage = error instanceof Error ? error.message : String(error); + const message = truncateErrorMessage(rawMessage); if (classified.retryable) { // Temporal will retry with configured backoff - throw ApplicationFailure.create({ + const failure = ApplicationFailure.create({ message, type: classified.type, details: [{ agentName, attemptNumber, elapsed: Date.now() - startTime }], }); + truncateStackTrace(failure); + throw failure; } else { // Fail immediately - no retry - throw ApplicationFailure.nonRetryable(message, classified.type, [ + const failure = ApplicationFailure.nonRetryable(message, classified.type, [ { agentName, attemptNumber, elapsed: Date.now() - startTime }, ]); + truncateStackTrace(failure); + throw failure; } } finally { clearInterval(heartbeatInterval); diff --git a/src/temporal/client.ts b/src/temporal/client.ts new file mode 100644 index 00000000..627121b8 --- /dev/null +++ b/src/temporal/client.ts @@ -0,0 +1,209 @@ +#!/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. + +/** + * Temporal client for starting Shannon pentest pipeline workflows. + * + * Starts a workflow and optionally waits for completion with progress polling. + * + * Usage: + * npm run temporal:start -- [options] + * # or + * node dist/temporal/client.js [options] + * + * Options: + * --config Configuration file path + * --output Output directory for audit logs + * --pipeline-testing Use minimal prompts for fast testing + * --workflow-id Custom workflow ID (default: shannon-) + * --no-wait Start workflow and exit without waiting + * + * Environment: + * TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233) + */ + +import { Connection, Client } from '@temporalio/client'; +import dotenv from 'dotenv'; +import chalk from 'chalk'; +// Import types only - these don't pull in workflow runtime code +import type { PipelineInput, PipelineState, PipelineProgress } from './shared.js'; + +dotenv.config(); + +// Query name must match the one defined in workflows.ts +const PROGRESS_QUERY = 'getProgress'; + +function showUsage(): void { + console.log(chalk.cyan.bold('\nShannon Temporal Client')); + console.log(chalk.gray('Start a pentest pipeline workflow\n')); + console.log(chalk.yellow('Usage:')); + console.log( + ' node dist/temporal/client.js [options]\n' + ); + console.log(chalk.yellow('Options:')); + console.log(' --config Configuration file path'); + console.log(' --output Output directory for audit logs'); + console.log(' --pipeline-testing Use minimal prompts for fast testing'); + console.log( + ' --workflow-id Custom workflow ID (default: shannon-)' + ); + console.log(' --no-wait Start workflow and exit without waiting\n'); + console.log(chalk.yellow('Examples:')); + console.log(' node dist/temporal/client.js https://example.com /path/to/repo'); + console.log( + ' node dist/temporal/client.js https://example.com /path/to/repo --config config.yaml\n' + ); +} + +async function startPipeline(): Promise { + const args = process.argv.slice(2); + + if (args.includes('--help') || args.includes('-h') || args.length === 0) { + showUsage(); + process.exit(0); + } + + // Parse arguments + let webUrl: string | undefined; + let repoPath: string | undefined; + let configPath: string | undefined; + let outputPath: string | undefined; + let pipelineTestingMode = false; + let customWorkflowId: string | undefined; + let waitForCompletion = true; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--config') { + const nextArg = args[i + 1]; + if (nextArg && !nextArg.startsWith('-')) { + configPath = nextArg; + i++; + } + } else if (arg === '--output') { + const nextArg = args[i + 1]; + if (nextArg && !nextArg.startsWith('-')) { + outputPath = nextArg; + i++; + } + } else if (arg === '--workflow-id') { + const nextArg = args[i + 1]; + if (nextArg && !nextArg.startsWith('-')) { + customWorkflowId = nextArg; + i++; + } + } else if (arg === '--pipeline-testing') { + pipelineTestingMode = true; + } else if (arg === '--no-wait') { + waitForCompletion = false; + } else if (arg && !arg.startsWith('-')) { + if (!webUrl) { + webUrl = arg; + } else if (!repoPath) { + repoPath = arg; + } + } + } + + if (!webUrl || !repoPath) { + console.log(chalk.red('Error: webUrl and repoPath are required')); + showUsage(); + process.exit(1); + } + + const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233'; + console.log(chalk.cyan(`Connecting to Temporal at ${address}...`)); + + const connection = await Connection.connect({ address }); + const client = new Client({ connection }); + + try { + const workflowId = customWorkflowId || `shannon-${Date.now()}`; + + const input: PipelineInput = { + webUrl, + repoPath, + ...(configPath && { configPath }), + ...(outputPath && { outputPath }), + ...(pipelineTestingMode && { pipelineTestingMode }), + }; + + console.log(chalk.green(`\nStarting workflow: ${workflowId}`)); + console.log(chalk.gray(`Target: ${webUrl}`)); + console.log(chalk.gray(`Repository: ${repoPath}`)); + console.log( + chalk.blue( + `Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}\n` + ) + ); + + // Start workflow by name (not by importing the function) + const handle = await client.workflow.start<(input: PipelineInput) => Promise>( + 'pentestPipelineWorkflow', + { + taskQueue: 'shannon-pipeline', + workflowId, + args: [input], + } + ); + + if (!waitForCompletion) { + console.log( + chalk.yellow('Workflow started in background. Use query tool to check progress.') + ); + console.log(chalk.gray(` npm run temporal:query -- ${workflowId}`)); + return; + } + + // Poll for progress every 30 seconds + const progressInterval = setInterval(async () => { + try { + const progress = await handle.query(PROGRESS_QUERY); + const elapsed = Math.floor(progress.elapsedMs / 1000); + console.log( + chalk.gray(`[${elapsed}s]`), + chalk.cyan(`Phase: ${progress.currentPhase || 'unknown'}`), + chalk.gray(`| Agent: ${progress.currentAgent || 'none'}`), + chalk.gray(`| Completed: ${progress.completedAgents.length}/13`) + ); + } catch { + // Workflow may have completed + } + }, 30000); + + try { + const result = await handle.result(); + clearInterval(progressInterval); + + console.log(chalk.green.bold('\nPipeline completed successfully!')); + console.log( + chalk.gray(`Duration: ${Math.floor((Date.now() - result.startTime) / 1000)}s`) + ); + console.log(chalk.gray(`Agents completed: ${result.completedAgents.length}`)); + + // Show cost summary if available + const totalCost = Object.values(result.agentMetrics).reduce( + (sum, m) => sum + (m.costUsd ?? 0), + 0 + ); + if (totalCost > 0) { + console.log(chalk.gray(`Total cost: $${totalCost.toFixed(4)}`)); + } + } catch (error) { + clearInterval(progressInterval); + console.error(chalk.red.bold('\nPipeline failed:'), error); + process.exit(1); + } + } finally { + await connection.close(); + } +} + +startPipeline().catch((err) => { + console.error(chalk.red('Client error:'), err); + process.exit(1); +}); diff --git a/src/temporal/query.ts b/src/temporal/query.ts new file mode 100644 index 00000000..a97fe748 --- /dev/null +++ b/src/temporal/query.ts @@ -0,0 +1,155 @@ +#!/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. + +/** + * Temporal query tool for inspecting Shannon workflow progress. + * + * Queries a running or completed workflow and displays its state. + * + * Usage: + * npm run temporal:query -- + * # or + * node dist/temporal/query.js + * + * Environment: + * TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233) + */ + +import { Connection, Client } from '@temporalio/client'; +import dotenv from 'dotenv'; +import chalk from 'chalk'; + +dotenv.config(); + +// Query name must match the one defined in workflows.ts +const PROGRESS_QUERY = 'getProgress'; + +// Types duplicated from shared.ts to avoid importing workflow APIs +interface AgentMetrics { + durationMs: number; + inputTokens: number | null; + outputTokens: number | null; + costUsd: number | null; + numTurns: number | null; +} + +interface PipelineProgress { + status: 'running' | 'completed' | 'failed'; + currentPhase: string | null; + currentAgent: string | null; + completedAgents: string[]; + failedAgent: string | null; + error: string | null; + startTime: number; + agentMetrics: Record; + workflowId: string; + elapsedMs: number; +} + +function showUsage(): void { + console.log(chalk.cyan.bold('\nShannon Temporal Query Tool')); + console.log(chalk.gray('Query progress of a running workflow\n')); + console.log(chalk.yellow('Usage:')); + console.log(' node dist/temporal/query.js \n'); + console.log(chalk.yellow('Examples:')); + console.log(' node dist/temporal/query.js shannon-1704672000000\n'); +} + +function getStatusColor(status: string): string { + switch (status) { + case 'running': + return chalk.yellow(status); + case 'completed': + return chalk.green(status); + case 'failed': + return chalk.red(status); + default: + return status; + } +} + +function formatDuration(ms: number): string { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } + return `${seconds}s`; +} + +async function queryWorkflow(): Promise { + const workflowId = process.argv[2]; + + if (!workflowId || workflowId === '--help' || workflowId === '-h') { + showUsage(); + process.exit(workflowId ? 0 : 1); + } + + const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233'; + + const connection = await Connection.connect({ address }); + const client = new Client({ connection }); + + try { + const handle = client.workflow.getHandle(workflowId); + const progress = await handle.query(PROGRESS_QUERY); + + console.log(chalk.cyan.bold('\nWorkflow Progress')); + console.log(chalk.gray('\u2500'.repeat(40))); + console.log(`${chalk.white('Workflow ID:')} ${progress.workflowId}`); + console.log(`${chalk.white('Status:')} ${getStatusColor(progress.status)}`); + console.log( + `${chalk.white('Current Phase:')} ${progress.currentPhase || 'none'}` + ); + console.log( + `${chalk.white('Current Agent:')} ${progress.currentAgent || 'none'}` + ); + console.log(`${chalk.white('Elapsed:')} ${formatDuration(progress.elapsedMs)}`); + console.log( + `${chalk.white('Completed:')} ${progress.completedAgents.length}/13 agents` + ); + + if (progress.completedAgents.length > 0) { + console.log(chalk.gray('\nCompleted agents:')); + for (const agent of progress.completedAgents) { + const metrics = progress.agentMetrics[agent]; + const duration = metrics ? formatDuration(metrics.durationMs) : 'unknown'; + const cost = metrics?.costUsd ? `$${metrics.costUsd.toFixed(4)}` : ''; + console.log( + chalk.green(` - ${agent}`) + + chalk.gray(` (${duration}${cost ? ', ' + cost : ''})`) + ); + } + } + + if (progress.error) { + console.log(chalk.red(`\nError: ${progress.error}`)); + console.log(chalk.red(`Failed agent: ${progress.failedAgent}`)); + } + + console.log(); + } catch (error) { + const err = error as Error; + if (err.message?.includes('not found')) { + console.log(chalk.red(`Workflow not found: ${workflowId}`)); + } else { + console.error(chalk.red('Query failed:'), err.message); + } + process.exit(1); + } finally { + await connection.close(); + } +} + +queryWorkflow().catch((err) => { + console.error(chalk.red('Query error:'), err); + process.exit(1); +}); diff --git a/src/temporal/worker.ts b/src/temporal/worker.ts new file mode 100644 index 00000000..73462576 --- /dev/null +++ b/src/temporal/worker.ts @@ -0,0 +1,79 @@ +#!/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. + +/** + * Temporal worker for Shannon pentest pipeline. + * + * Polls the 'shannon-pipeline' task queue and executes activities. + * Handles up to 5 concurrent activities to support parallel agent execution. + * + * Usage: + * npm run temporal:worker + * # or + * node dist/temporal/worker.js + * + * Environment: + * TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233) + */ + +import { NativeConnection, Worker, bundleWorkflowCode } from '@temporalio/worker'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import dotenv from 'dotenv'; +import chalk from 'chalk'; +import * as activities from './activities.js'; + +dotenv.config(); + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function runWorker(): Promise { + const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233'; + console.log(chalk.cyan(`Connecting to Temporal at ${address}...`)); + + const connection = await NativeConnection.connect({ address }); + + // Bundle workflows for Temporal's V8 isolate + console.log(chalk.gray('Bundling workflows...')); + const workflowBundle = await bundleWorkflowCode({ + workflowsPath: path.join(__dirname, 'workflows.js'), + }); + + const worker = await Worker.create({ + connection, + namespace: 'default', + workflowBundle, + activities, + taskQueue: 'shannon-pipeline', + maxConcurrentActivityTaskExecutions: 5, // Match parallel agent count + }); + + // Graceful shutdown handling + const shutdown = async (): Promise => { + console.log(chalk.yellow('\nShutting down worker...')); + worker.shutdown(); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + console.log(chalk.green('Shannon worker started')); + console.log(chalk.gray('Task queue: shannon-pipeline')); + console.log(chalk.gray('Press Ctrl+C to stop\n')); + + try { + await worker.run(); + } finally { + await connection.close(); + console.log(chalk.gray('Worker stopped')); + } +} + +runWorker().catch((err) => { + console.error(chalk.red('Worker failed:'), err); + process.exit(1); +}); diff --git a/src/utils/git-manager.ts b/src/utils/git-manager.ts index 969e8111..780bdcd8 100644 --- a/src/utils/git-manager.ts +++ b/src/utils/git-manager.ts @@ -7,6 +7,19 @@ import { $ } from 'zx'; import chalk from 'chalk'; +/** + * Check if a directory is a git repository. + * Returns true if the directory contains a .git folder or is inside a git repo. + */ +export async function isGitRepository(dir: string): Promise { + try { + await $`cd ${dir} && git rev-parse --git-dir`.quiet(); + return true; + } catch { + return false; + } +} + interface GitOperationResult { success: boolean; hadChanges?: boolean; @@ -146,6 +159,12 @@ export async function rollbackGitWorkspace( sourceDir: string, reason: string = 'retry preparation' ): Promise { + // Skip git operations if not a git repository + if (!(await isGitRepository(sourceDir))) { + console.log(chalk.gray(` â­ī¸ Skipping git rollback (not a git repository)`)); + return { success: true }; + } + console.log(chalk.yellow(` 🔄 Rolling back workspace for ${reason}`)); try { const changes = await getChangedFiles(sourceDir, 'status check for rollback'); @@ -182,6 +201,12 @@ export async function createGitCheckpoint( description: string, attempt: number ): Promise { + // Skip git operations if not a git repository + if (!(await isGitRepository(sourceDir))) { + console.log(chalk.gray(` â­ī¸ Skipping git checkpoint (not a git repository)`)); + return { success: true }; + } + console.log(chalk.blue(` 📍 Creating checkpoint for ${description} (attempt ${attempt})`)); try { // First attempt: preserve existing deliverables. Retries: clean workspace to prevent pollution @@ -221,6 +246,12 @@ export async function commitGitSuccess( sourceDir: string, description: string ): Promise { + // Skip git operations if not a git repository + if (!(await isGitRepository(sourceDir))) { + console.log(chalk.gray(` â­ī¸ Skipping git commit (not a git repository)`)); + return { success: true }; + } + console.log(chalk.green(` 💾 Committing successful results for ${description}`)); try { const changes = await getChangedFiles(sourceDir, 'status check for success commit'); @@ -252,9 +283,13 @@ export async function commitGitSuccess( } /** - * Get current git commit hash + * Get current git commit hash. + * Returns null if not a git repository. */ export async function getGitCommitHash(sourceDir: string): Promise { + if (!(await isGitRepository(sourceDir))) { + return null; + } try { const result = await $`cd ${sourceDir} && git rev-parse HEAD`; return result.stdout.trim(); From cbb2b4acc023f5b840981ea6f75dc380ec8e9827 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 14:37:29 -0800 Subject: [PATCH 07/24] feat: fix Docker worker container setup - Install uv instead of deprecated uvx package - Add mcp-server and configs directories to container - Mount target repo dynamically via TARGET_REPO env variable --- docker/Dockerfile.worker | 6 ++++-- docker/docker-compose.temporal.yml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile.worker b/docker/Dockerfile.worker index aef429ce..31fd9f13 100644 --- a/docker/Dockerfile.worker +++ b/docker/Dockerfile.worker @@ -12,8 +12,8 @@ RUN apk add --no-cache \ bash \ curl -# Install uvx for browser-use -RUN pip install uvx --break-system-packages +# Install uv (includes uvx command) for browser-use +RUN pip install uv --break-system-packages # Create non-root user RUN adduser -D -u 1000 pentest @@ -28,6 +28,8 @@ RUN npm ci --omit=dev # Copy application code COPY dist/ ./dist/ COPY prompts/ ./prompts/ +COPY mcp-server/ ./mcp-server/ +COPY configs/ ./configs/ # Set ownership RUN chown -R pentest:pentest /app diff --git a/docker/docker-compose.temporal.yml b/docker/docker-compose.temporal.yml index 4d9bb455..82000652 100644 --- a/docker/docker-compose.temporal.yml +++ b/docker/docker-compose.temporal.yml @@ -25,8 +25,8 @@ services: temporal: condition: service_healthy volumes: - - ../deliverables:/app/deliverables - ../prompts:/app/prompts + - ${TARGET_REPO:-/tmp/target-repo}:/target-repo shm_size: 2gb ipc: host security_opt: From b26c69023df3c80a87d328d28ef0bcabd6cd3c43 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 15:40:29 -0800 Subject: [PATCH 08/24] fix: add report assembly step to Temporal workflow - Add assembleReportActivity to concatenate exploitation evidence files before report agent runs - Call assembleFinalReport in workflow Phase 5 before runReportAgent - Ensure deliverables directory exists before writing final report - Simplify pipeline-testing report prompt to just prepend header --- prompts/pipeline-testing/report-executive.txt | 32 +------------------ src/phases/reporting.ts | 5 ++- src/temporal/activities.ts | 17 ++++++++++ src/temporal/workflows.ts | 5 +++ 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/prompts/pipeline-testing/report-executive.txt b/prompts/pipeline-testing/report-executive.txt index a7350455..bd62c676 100644 --- a/prompts/pipeline-testing/report-executive.txt +++ b/prompts/pipeline-testing/report-executive.txt @@ -2,34 +2,4 @@ # This section above is metadata and not part of the prompt. === PROMPT === -Create a file at `deliverables/comprehensive_security_assessment_report.md` with this content: - -```markdown -# Injection Exploitation Evidence - -## Successfully Exploited Vulnerabilities - -### INJ-VULN-01: SQL Injection Test - -**Summary:** Test SQL injection vulnerability for pipeline testing. - -# Authentication Exploitation Evidence - -## Successfully Exploited Vulnerabilities - -### AUTH-VULN-01: Authentication Bypass Test - -**Summary:** Test authentication vulnerability for pipeline testing. -``` - -Then read and modify the file by adding these sections at the top: - -1. **Executive Summary** section with: - - Target: {{WEB_URL}} - - Assessment Date: (current date) - - Scope: Authentication, XSS, SQL and Command Injection, SSRF, Authorization testing - - Brief summary of findings (2-3 vulnerabilities found for testing) - -2. **Network Reconnaissance** section with test data for open ports and services - -Save the modified content back to `deliverables/comprehensive_security_assessment_report.md`, then say "Done". \ No newline at end of file +Read `deliverables/comprehensive_security_assessment_report.md`, prepend "# Security Assessment Report\n\n**Target:** {{WEB_URL}}\n\n" to the content, and save it back. Say "Done". diff --git a/src/phases/reporting.ts b/src/phases/reporting.ts index 0b5fc7c8..7ea28d5d 100644 --- a/src/phases/reporting.ts +++ b/src/phases/reporting.ts @@ -48,9 +48,12 @@ export async function assembleFinalReport(sourceDir: string): Promise { } const finalContent = sections.join('\n\n'); - const finalReportPath = path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md'); + const deliverablesDir = path.join(sourceDir, 'deliverables'); + const finalReportPath = path.join(deliverablesDir, 'comprehensive_security_assessment_report.md'); try { + // Ensure deliverables directory exists + await fs.ensureDir(deliverablesDir); await fs.writeFile(finalReportPath, finalContent); console.log(chalk.green(`✅ Final report assembled at ${finalReportPath}`)); } catch (error) { diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index 15df9351..c2b2dc70 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -58,6 +58,7 @@ import { rollbackGitWorkspace, getGitCommitHash, } from '../utils/git-manager.js'; +import { assembleFinalReport } from '../phases/reporting.js'; import { getPromptNameForAgent } from '../types/agents.js'; import { AuditSession } from '../audit/index.js'; import type { AgentName } from '../types/agents.js'; @@ -305,3 +306,19 @@ export async function runAuthzExploitAgent(input: ActivityInput): Promise { return runAgentActivity('report', input); } + +/** + * Assemble the final report by concatenating exploitation evidence files. + * This must be called BEFORE runReportAgent to create the file that the report agent will modify. + */ +export async function assembleReportActivity(input: ActivityInput): Promise { + const { repoPath } = input; + console.log(chalk.blue('📝 Assembling deliverables from specialist agents...')); + try { + await assembleFinalReport(repoPath); + } catch (error) { + const err = error as Error; + console.log(chalk.yellow(`âš ī¸ Error assembling final report: ${err.message}`)); + // Don't throw - the report agent can still create content even if no exploitation files exist + } +} diff --git a/src/temporal/workflows.ts b/src/temporal/workflows.ts index c07289c2..700dc577 100644 --- a/src/temporal/workflows.ts +++ b/src/temporal/workflows.ts @@ -167,6 +167,11 @@ export async function pentestPipelineWorkflow( // === Phase 5: Reporting === state.currentPhase = 'reporting'; state.currentAgent = 'report'; + + // First, assemble the concatenated report from exploitation evidence files + await acts.assembleReportActivity(activityInput); + + // Then run the report agent to add executive summary and clean up state.agentMetrics['report'] = await acts.runReportAgent(activityInput); state.completedAgents.push('report'); From 5bda6fa63453f44168ae88628eb4bed1c5d6a336 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 15:41:25 -0800 Subject: [PATCH 09/24] refactor: consolidate Docker setup to root docker-compose.yml --- ...compose.temporal.yml => docker-compose.yml | 11 ++--- docker/Dockerfile.worker | 45 ------------------- 2 files changed, 6 insertions(+), 50 deletions(-) rename docker/docker-compose.temporal.yml => docker-compose.yml (73%) delete mode 100644 docker/Dockerfile.worker diff --git a/docker/docker-compose.temporal.yml b/docker-compose.yml similarity index 73% rename from docker/docker-compose.temporal.yml rename to docker-compose.yml index 82000652..85582199 100644 --- a/docker/docker-compose.temporal.yml +++ b/docker-compose.yml @@ -15,17 +15,18 @@ services: start_period: 30s worker: - build: - context: .. - dockerfile: docker/Dockerfile.worker + build: . + entrypoint: ["node", "dist/temporal/worker.js"] environment: - TEMPORAL_ADDRESS=temporal:7233 - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} + - CLAUDE_CODE_MAX_OUTPUT_TOKENS=${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-64000} depends_on: temporal: condition: service_healthy volumes: - - ../prompts:/app/prompts + - ./prompts:/app/prompts - ${TARGET_REPO:-/tmp/target-repo}:/target-repo shm_size: 2gb ipc: host diff --git a/docker/Dockerfile.worker b/docker/Dockerfile.worker deleted file mode 100644 index 31fd9f13..00000000 --- a/docker/Dockerfile.worker +++ /dev/null @@ -1,45 +0,0 @@ -# Wolfi-based worker for Shannon AI pentester -FROM cgr.dev/chainguard/wolfi-base:latest - -# Install Node.js 22, Python 3.12, Chromium, and dependencies -RUN apk add --no-cache \ - nodejs-22 \ - npm \ - python-3.12 \ - py3.12-pip \ - chromium \ - git \ - bash \ - curl - -# Install uv (includes uvx command) for browser-use -RUN pip install uv --break-system-packages - -# Create non-root user -RUN adduser -D -u 1000 pentest -WORKDIR /app - -# Copy package files first for better caching -COPY package*.json ./ - -# Install dependencies -RUN npm ci --omit=dev - -# Copy application code -COPY dist/ ./dist/ -COPY prompts/ ./prompts/ -COPY mcp-server/ ./mcp-server/ -COPY configs/ ./configs/ - -# Set ownership -RUN chown -R pentest:pentest /app - -# Switch to non-root user -USER pentest - -# Set Chromium path for Playwright -ENV CHROME_PATH=/usr/bin/chromium-browser -ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser - -# Entry point -CMD ["node", "dist/temporal/worker.js"] From b84c1d3bb0026e66032a737b21b96d8e21f12f6f Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 16:36:48 -0800 Subject: [PATCH 10/24] feat: improve Temporal client UX and env handling - Change default to fire-and-forget (--wait flag to opt-in) - Add splash screen and improve console output formatting - Add .env to gitignore, remove from dockerignore for container access - Add Taskfile for common development commands --- .dockerignore | 1 - .gitignore | 1 + Taskfile.yml | 83 ++++++++++++++++++++++++++++++++++++++++++ src/temporal/client.ts | 44 +++++++++++++--------- 4 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 Taskfile.yml diff --git a/.dockerignore b/.dockerignore index c03a091c..deaa1cc5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,7 +18,6 @@ xben-benchmark-results/ # Development files *.md !CLAUDE.md -.env* .DS_Store Thumbs.db diff --git a/.gitignore b/.gitignore index 23d04235..b91c4cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ .shannon-store.json +.env agent-logs/ /audit-logs/ dist/ diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 00000000..57c4f789 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,83 @@ +version: '3' + +dotenv: ['.env'] + +vars: + COMPOSE_FILE: docker-compose.yml + +tasks: + default: + silent: true + cmds: [task help] + + help: + desc: Show usage information + silent: true + cmds: + - | + echo "Shannon - AI Penetration Testing Framework" + echo "" + echo "Usage:" + echo " task start URL= REPO= Start a pentest workflow" + echo " task logs View real-time worker logs" + echo " task query ID= Query workflow progress" + echo " task stop Stop all containers" + echo " task help Show this help message" + echo "" + echo "Examples:" + echo " task start URL=https://example.com REPO=/path/to/repo" + echo " task start URL=https://example.com REPO=/path/to/repo -- --pipeline-testing" + echo " task start URL=https://example.com REPO=/path/to/repo -- --config ./config.yaml" + echo " task query ID=shannon-1234567890" + echo "" + echo "Monitor workflows at http://localhost:8233" + + start: + desc: Start a pentest workflow + silent: true + requires: + vars: [URL, REPO] + cmds: + - | + if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then + echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" + exit 1 + fi + - TARGET_REPO={{.REPO}} docker compose -f {{.COMPOSE_FILE}} up -d --build + - | + for i in $(seq 1 30); do + docker compose -f {{.COMPOSE_FILE}} exec -T temporal \ + temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" && break + [ $i -eq 30 ] && echo "Timeout waiting for Temporal" && exit 1 + sleep 2 + done + - | + docker compose -f {{.COMPOSE_FILE}} exec -T worker \ + node dist/temporal/client.js "{{.URL}}" "/target-repo" {{.CLI_ARGS}} + + logs: + desc: View real-time worker logs + silent: true + cmds: + - docker compose -f {{.COMPOSE_FILE}} logs -f worker {{.CLI_ARGS}} + + query: + desc: Query workflow progress + silent: true + requires: + vars: [ID] + cmds: + - | + docker compose -f {{.COMPOSE_FILE}} exec -T worker \ + node dist/temporal/query.js "{{.ID}}" + + stop: + desc: Stop all containers + silent: true + cmds: + - | + if [ "{{.CLI_ARGS}}" = "--clean" ]; then + docker compose -f {{.COMPOSE_FILE}} down -v + else + docker compose -f {{.COMPOSE_FILE}} down + fi diff --git a/src/temporal/client.ts b/src/temporal/client.ts index 627121b8..8278e195 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -20,7 +20,7 @@ * --output Output directory for audit logs * --pipeline-testing Use minimal prompts for fast testing * --workflow-id Custom workflow ID (default: shannon-) - * --no-wait Start workflow and exit without waiting + * --wait Wait for workflow completion with progress polling * * Environment: * TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233) @@ -29,6 +29,7 @@ import { Connection, Client } from '@temporalio/client'; import dotenv from 'dotenv'; import chalk from 'chalk'; +import { displaySplashScreen } from '../splash-screen.js'; // Import types only - these don't pull in workflow runtime code import type { PipelineInput, PipelineState, PipelineProgress } from './shared.js'; @@ -51,7 +52,7 @@ function showUsage(): void { console.log( ' --workflow-id Custom workflow ID (default: shannon-)' ); - console.log(' --no-wait Start workflow and exit without waiting\n'); + console.log(' --wait Wait for workflow completion with progress polling\n'); console.log(chalk.yellow('Examples:')); console.log(' node dist/temporal/client.js https://example.com /path/to/repo'); console.log( @@ -74,7 +75,7 @@ async function startPipeline(): Promise { let outputPath: string | undefined; let pipelineTestingMode = false; let customWorkflowId: string | undefined; - let waitForCompletion = true; + let waitForCompletion = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -98,8 +99,8 @@ async function startPipeline(): Promise { } } else if (arg === '--pipeline-testing') { pipelineTestingMode = true; - } else if (arg === '--no-wait') { - waitForCompletion = false; + } else if (arg === '--wait') { + waitForCompletion = true; } else if (arg && !arg.startsWith('-')) { if (!webUrl) { webUrl = arg; @@ -115,8 +116,11 @@ async function startPipeline(): Promise { process.exit(1); } + // Display splash screen + await displaySplashScreen(); + const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233'; - console.log(chalk.cyan(`Connecting to Temporal at ${address}...`)); + console.log(chalk.gray(`Connecting to Temporal at ${address}...`)); const connection = await Connection.connect({ address }); const client = new Client({ connection }); @@ -132,14 +136,17 @@ async function startPipeline(): Promise { ...(pipelineTestingMode && { pipelineTestingMode }), }; - console.log(chalk.green(`\nStarting workflow: ${workflowId}`)); - console.log(chalk.gray(`Target: ${webUrl}`)); - console.log(chalk.gray(`Repository: ${repoPath}`)); - console.log( - chalk.blue( - `Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}\n` - ) - ); + console.log(chalk.green.bold(`✓ Workflow started: ${workflowId}`)); + console.log(); + console.log(chalk.white(' Target: ') + chalk.cyan(webUrl)); + console.log(chalk.white(' Repository: ') + chalk.cyan(repoPath)); + if (configPath) { + console.log(chalk.white(' Config: ') + chalk.cyan(configPath)); + } + if (pipelineTestingMode) { + console.log(chalk.white(' Mode: ') + chalk.yellow('Pipeline Testing')); + } + console.log(); // Start workflow by name (not by importing the function) const handle = await client.workflow.start<(input: PipelineInput) => Promise>( @@ -152,10 +159,11 @@ async function startPipeline(): Promise { ); if (!waitForCompletion) { - console.log( - chalk.yellow('Workflow started in background. Use query tool to check progress.') - ); - console.log(chalk.gray(` npm run temporal:query -- ${workflowId}`)); + console.log(chalk.bold('Monitor progress:')); + console.log(chalk.white(' Web UI: ') + chalk.blue(`http://localhost:8233/namespaces/default/workflows/${workflowId}`)); + console.log(chalk.white(' Logs: ') + chalk.gray('task logs')); + console.log(chalk.white(' Query: ') + chalk.gray(`task query ID=${workflowId}`)); + console.log(); return; } From 69f2d8ffe76bf24ca7f9a92483ebe664259fc2e7 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 17:18:28 -0800 Subject: [PATCH 11/24] refactor: simplify session ID handling and improve Taskfile options - Include hostname in workflow ID for better audit log organization - Extract sanitizeHostname utility to audit/utils.ts for reuse - Remove unused generateSessionLogPath and buildLogFilePath functions - Simplify Taskfile with CONFIG/OUTPUT/CLEAN named parameters --- Taskfile.yml | 27 +++++++++++++++++++-------- src/ai/claude-executor.ts | 25 ++----------------------- src/audit/utils.ts | 14 ++++++++++---- src/session-manager.ts | 6 ------ src/temporal/client.ts | 4 +++- 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 57c4f789..61cf6c7a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -24,11 +24,18 @@ tasks: echo " task stop Stop all containers" echo " task help Show this help message" echo "" + echo "Options for 'start':" + echo " CONFIG= Configuration file (YAML)" + echo " OUTPUT= Output directory for reports" + echo "" + echo "Options for 'stop':" + echo " CLEAN=true Remove all data including volumes" + echo "" echo "Examples:" echo " task start URL=https://example.com REPO=/path/to/repo" - echo " task start URL=https://example.com REPO=/path/to/repo -- --pipeline-testing" - echo " task start URL=https://example.com REPO=/path/to/repo -- --config ./config.yaml" + echo " task start URL=https://example.com REPO=/path/to/repo CONFIG=./config.yaml" echo " task query ID=shannon-1234567890" + echo " task stop CLEAN=true" echo "" echo "Monitor workflows at http://localhost:8233" @@ -52,8 +59,12 @@ tasks: sleep 2 done - | + ARGS="" + {{if .CONFIG}}ARGS="$ARGS --config {{.CONFIG}}"{{end}} + {{if .OUTPUT}}ARGS="$ARGS --output {{.OUTPUT}}"{{end}} + {{if eq .PIPELINE_TESTING "true"}}ARGS="$ARGS --pipeline-testing"{{end}} docker compose -f {{.COMPOSE_FILE}} exec -T worker \ - node dist/temporal/client.js "{{.URL}}" "/target-repo" {{.CLI_ARGS}} + node dist/temporal/client.js "{{.URL}}" "/target-repo" $ARGS {{.CLI_ARGS}} logs: desc: View real-time worker logs @@ -76,8 +87,8 @@ tasks: silent: true cmds: - | - if [ "{{.CLI_ARGS}}" = "--clean" ]; then - docker compose -f {{.COMPOSE_FILE}} down -v - else - docker compose -f {{.COMPOSE_FILE}} down - fi + {{if eq .CLEAN "true"}} + docker compose -f {{.COMPOSE_FILE}} down -v + {{else}} + docker compose -f {{.COMPOSE_FILE}} down + {{end}} diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index 91ef8330..86756926 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -15,7 +15,6 @@ import { timingResults, Timer } from '../utils/metrics.js'; import { formatTimestamp } from '../utils/formatting.js'; import { createGitCheckpoint, commitGitSuccess, rollbackGitWorkspace, getGitCommitHash } from '../utils/git-manager.js'; import { AGENT_VALIDATORS, MCP_AGENT_MAPPING } from '../constants.js'; -import { generateSessionLogPath } from '../session-manager.js'; import { AuditSession } from '../audit/index.js'; import { createShannonHelperServer } from '../../mcp-server/dist/index.js'; import type { SessionMetadata } from '../audit/utils.js'; @@ -39,7 +38,6 @@ export interface ClaudePromptResult { cost: number; partialCost?: number; apiErrorDetected?: boolean; - logFile?: string; error?: string; errorType?: string; prompt?: string; @@ -215,10 +213,7 @@ export async function runClaudePrompt( ); const auditLogger = createAuditLogger(auditSession); - const logFilePath = buildLogFilePath(sessionMetadata, execContext.agentKey, attemptNumber); - if (!logFilePath) { - console.log(chalk.blue(` Running Claude Code: ${description}...`)); - } + console.log(chalk.blue(` Running Claude Code: ${description}...`)); const mcpServers = buildMcpServers(sourceDir, agentName); const options = { @@ -262,7 +257,7 @@ export async function runClaudePrompt( progress.finish(formatCompletionMessage(execContext, description, turnCount, duration)); - const returnData: ClaudePromptResult = { + return { result, success: true, duration, @@ -271,10 +266,6 @@ export async function runClaudePrompt( partialCost: totalCost, apiErrorDetected }; - if (logFilePath) { - returnData.logFile = logFilePath; - } - return returnData; } catch (error) { const duration = timer.stop(); @@ -299,18 +290,6 @@ export async function runClaudePrompt( } } -function buildLogFilePath( - sessionMetadata: SessionMetadata | null, - agentKey: string, - attemptNumber: number -): string | null { - if (!sessionMetadata || !sessionMetadata.webUrl || !sessionMetadata.id) { - return null; - } - const timestamp = formatTimestamp().replace(/T/, '_').replace(/[:.]/g, '-').slice(0, 19); - const logDir = generateSessionLogPath(sessionMetadata.webUrl, sessionMetadata.id); - return path.join(logDir, `${timestamp}_${agentKey}_attempt-${attemptNumber}.log`); -} interface MessageLoopResult { turnCount: number; diff --git a/src/audit/utils.ts b/src/audit/utils.ts index b05d0d6f..18d7bd2b 100644 --- a/src/audit/utils.ts +++ b/src/audit/utils.ts @@ -31,12 +31,18 @@ export interface SessionMetadata { } /** - * Generate standardized session identifier: {hostname}_{sessionId} + * Extract and sanitize hostname from URL for use in identifiers + */ +export function sanitizeHostname(url: string): string { + return new URL(url).hostname.replace(/[^a-zA-Z0-9-]/g, '-'); +} + +/** + * Generate standardized session identifier from workflow ID + * Workflow IDs already contain hostname, so we use them directly */ export function generateSessionIdentifier(sessionMetadata: SessionMetadata): string { - const { id, webUrl } = sessionMetadata; - const hostname = new URL(webUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-'); - return `${hostname}_${id}`; + return sessionMetadata.id; } /** diff --git a/src/session-manager.ts b/src/session-manager.ts index 7a31d1a8..335a74d4 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -126,10 +126,4 @@ export const AGENT_PHASE_MAP: Readonly> = Object.fr 'report': 'reporting', }); -// Generate a session-based log folder path (used by claude-executor.ts) -export const generateSessionLogPath = (webUrl: string, sessionId: string): string => { - const hostname = new URL(webUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-'); - const sessionFolderName = `${hostname}_${sessionId}`; - return path.join(process.cwd(), 'agent-logs', sessionFolderName); -}; diff --git a/src/temporal/client.ts b/src/temporal/client.ts index 8278e195..e16e7111 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -30,6 +30,7 @@ import { Connection, Client } from '@temporalio/client'; import dotenv from 'dotenv'; import chalk from 'chalk'; import { displaySplashScreen } from '../splash-screen.js'; +import { sanitizeHostname } from '../audit/utils.js'; // Import types only - these don't pull in workflow runtime code import type { PipelineInput, PipelineState, PipelineProgress } from './shared.js'; @@ -126,7 +127,8 @@ async function startPipeline(): Promise { const client = new Client({ connection }); try { - const workflowId = customWorkflowId || `shannon-${Date.now()}`; + const hostname = sanitizeHostname(webUrl); + const workflowId = customWorkflowId || `${hostname}_shannon-${Date.now()}`; const input: PipelineInput = { webUrl, From 4de1508cb84e6959f40370f9dc75cad4ddbce876 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 17:22:53 -0800 Subject: [PATCH 12/24] chore: add .env.example and simplify .gitignore --- .env.example | 8 ++++++++ .gitignore | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..9378e66f --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Shannon Environment Configuration +# Copy this file to .env and fill in your credentials + +# Anthropic API Key (required - choose one) +ANTHROPIC_API_KEY=your-api-key-here + +# OR use OAuth token instead +# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here diff --git a/.gitignore b/.gitignore index b91c4cfd..f0e66bbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ node_modules/ .shannon-store.json .env -agent-logs/ -/audit-logs/ +audit-logs/ dist/ From 1f303b02b8f0f436df116c3b5e167e9e42da7985 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 17:23:25 -0800 Subject: [PATCH 13/24] docs: update README and CLAUDE.md for Temporal workflow usage - Replace Docker CLI instructions with Task-based commands - Add monitoring/stopping sections and workflow examples - Document Temporal orchestration layer and troubleshooting - Simplify file structure to key files overview --- CLAUDE.md | 213 +++++++++++++++++++++++++----------------------------- README.md | 174 +++++++++++++++++++------------------------- 2 files changed, 173 insertions(+), 214 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0bfd78d3..f4d0395b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,51 +8,67 @@ This is an AI-powered penetration testing agent designed for defensive security ## Commands -### Installation & Setup +### Prerequisites +- **Docker** - Container runtime +- **Task** - Task runner ([Install Task](https://taskfile.dev/installation/)) +- **Anthropic API key** - Set in `.env` file + +### Running the Penetration Testing Agent (Docker + Temporal) ```bash -npm install +# Configure credentials +cp .env.example .env +# Edit .env: +# ANTHROPIC_API_KEY=your-key +# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # Prevents token limits during long reports + +# Start a pentest workflow +task start URL= REPO= ``` -### Running the Penetration Testing Agent +Examples: ```bash -shannon [--config ] [--output ] +task start URL=https://example.com REPO=/path/to/repo +task start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml +task start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports ``` -Example: +### Monitoring Progress ```bash -shannon "https://example.com" "/path/to/local/repo" -shannon "https://juice-shop.herokuapp.com" "/home/user/juice-shop" --config juice-shop-config.yaml -shannon "https://example.com" "/path/to/repo" --output /path/to/reports +task logs # View real-time worker logs +task query ID= # Query specific workflow progress +# Temporal Web UI available at http://localhost:8233 ``` -### Alternative Execution +### Stopping Shannon ```bash -npm start --config +task stop # Stop containers (preserves workflow data) +task stop CLEAN=true # Full cleanup including volumes ``` ### Options ```bash ---config YAML configuration file for authentication and testing parameters ---output Custom output directory for session folder (default: ./audit-logs/) ---pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables) ---disable-loader Disable the animated progress loader (useful when logs interfere with spinner) ---help Show help message +CONFIG= YAML configuration file for authentication and testing parameters +OUTPUT= Custom output directory for session folder (default: ./audit-logs/) +PIPELINE_TESTING=true Use minimal prompts for fast pipeline testing ``` -### Configuration Validation +### Direct CLI (Local Development) +For development without Docker/Temporal: ```bash -# Configuration validation is built into the main script -shannon --help # Shows usage and validates config on execution +npm install +shannon [--config ] [--output ] ``` ### Generate TOTP for Authentication -TOTP generation is now handled automatically via the `generate_totp` MCP tool during authentication flows. +TOTP generation is handled automatically via the `generate_totp` MCP tool during authentication flows. ### Development Commands ```bash -# No linting or testing commands available in this project -# Development is done by running the agent in pipeline-testing mode -shannon --pipeline-testing +# Build TypeScript +npm run build + +# Run with pipeline testing mode (fast, minimal deliverables) +task start URL= REPO= PIPELINE_TESTING=true ``` ## Architecture & Components @@ -67,6 +83,21 @@ shannon --pipeline-testing - `src/session-manager.ts` - Agent definitions, execution order, and parallel groups - `src/queue-validation.ts` - Validates deliverables and agent prerequisites +### Temporal Orchestration Layer +Shannon uses Temporal for durable workflow orchestration: +- `src/temporal/shared.ts` - Types, interfaces, query definitions +- `src/temporal/workflows.ts` - Main workflow (pentestPipelineWorkflow) +- `src/temporal/activities.ts` - Activity implementations with heartbeats +- `src/temporal/worker.ts` - Worker process entry point +- `src/temporal/client.ts` - CLI client for starting workflows +- `src/temporal/query.ts` - Query tool for progress inspection + +Key features: +- **Crash recovery** - Workflows resume automatically after worker restart +- **Queryable progress** - Real-time status via `task query` or Temporal Web UI +- **Intelligent retry** - Distinguishes transient vs permanent errors +- **Parallel execution** - 5 concurrent agents in vulnerability/exploitation phases + ### Five-Phase Testing Workflow 1. **Pre-Reconnaissance** (`pre-recon`) - External tool scans (nmap, subfinder, whatweb) + source code analysis @@ -163,7 +194,6 @@ The agent implements a crash-safe audit system with the following features: - Phase-level and agent-level timing/cost aggregations - Validation results integrated with metrics -For detailed design, see `docs/unified-audit-system-design.md`. ## Development Notes @@ -183,18 +213,25 @@ A working POC exists at `/Users/arjunmalleswaran/Code/shannon-pocs` that demonst - "What retry configuration does the POC use for long-running agent activities?" - "How does the POC integrate Claude Agent SDK calls within Temporal activities?" -**Current reference implementations:** +**Reference implementation:** - **Temporal + Claude Agent SDK**: `/Users/arjunmalleswaran/Code/shannon-pocs` - working implementation demonstrating workflows, activities, worker setup, and SDK integration -- **Implementation plan**: `docs/temporal-implementation-plan.md` - detailed plan for Shannon integration + +### Adding a New Agent +1. Define the agent in `src/session-manager.ts` (add to `AGENT_QUEUE` and appropriate parallel group) +2. Create prompt template in `prompts/` (e.g., `vuln-newtype.txt` or `exploit-newtype.txt`) +3. Add activity function in `src/temporal/activities.ts` +4. Register activity in `src/temporal/workflows.ts` within the appropriate phase + +### Modifying Prompts +- Prompt templates use variable substitution: `{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`, `{{LOGIN_INSTRUCTIONS}}` +- Shared partials in `prompts/shared/` are included via `prompt-manager.ts` +- Test changes with `PIPELINE_TESTING=true` for faster iteration ### Key Design Patterns - **Configuration-Driven Architecture**: YAML configs with JSON Schema validation - **Modular Error Handling**: Categorized error types with retry logic -- **Pure Functions**: Most functionality is implemented as pure functions for testability - **SDK-First Approach**: Heavy reliance on Claude Agent SDK for autonomous AI operations - **Progressive Analysis**: Each phase builds on previous phase results -- **Local Repository Setup**: Target applications are accessed directly from user-provided local directories -- **Fire-and-Forget Execution**: Single entry point, runs all phases to completion ### Error Handling Strategy The application uses a comprehensive error handling system with: @@ -206,7 +243,7 @@ The application uses a comprehensive error handling system with: ### Testing Mode The agent includes a testing mode that skips external tool execution for faster development cycles: ```bash -shannon --pipeline-testing +task start URL= REPO= PIPELINE_TESTING=true ``` ### Security Focus @@ -218,89 +255,29 @@ This is explicitly designed as a **defensive security tool** for: The tool should only be used on systems you own or have explicit permission to test. -## File Structure +## Key Files & Directories -``` -src/ # TypeScript source files -├── shannon.ts # Main orchestration script (entry point) -├── constants.ts # Shared constants -├── config-parser.ts # Configuration handling -├── error-handling.ts # Error management -├── tool-checker.ts # Tool validation -├── session-manager.ts # Agent definitions, order, and parallel groups -├── queue-validation.ts # Deliverable validation -├── splash-screen.ts # ASCII art splash screen -├── progress-indicator.ts # Progress display utilities -├── types/ # TypeScript type definitions -│ ├── index.ts # Barrel exports -│ ├── agents.ts # Agent type definitions -│ ├── config.ts # Configuration interfaces -│ ├── errors.ts # Error type definitions -│ └── session.ts # Session type definitions -├── audit/ # Audit system -│ ├── index.ts # Public API -│ ├── audit-session.ts # Main facade (logger + metrics + mutex) -│ ├── logger.ts # Append-only crash-safe logging -│ ├── metrics-tracker.ts # Timing, cost, attempt tracking -│ └── utils.ts # Path generation, atomic writes -├── ai/ -│ └── claude-executor.ts # Claude Agent SDK integration -├── phases/ -│ ├── pre-recon.ts # Pre-reconnaissance phase -│ └── reporting.ts # Final report assembly -├── prompts/ -│ └── prompt-manager.ts # Prompt loading and variable substitution -├── setup/ -│ └── environment.ts # Local repository setup -├── cli/ -│ ├── ui.ts # Help text display -│ └── input-validator.ts # URL and path validation -└── utils/ - ├── git-manager.ts # Git operations - ├── metrics.ts # Timing utilities - ├── output-formatter.ts # Output formatting utilities - └── concurrency.ts # SessionMutex for parallel execution -dist/ # Compiled JavaScript output -├── shannon.js # Compiled entry point -└── ... # Other compiled files -package.json # Node.js dependencies -.shannon-store.json # Session lock file -audit-logs/ # Centralized audit data (default, or use --output) -└── {hostname}_{sessionId}/ - ├── session.json # Comprehensive metrics - ├── prompts/ # Prompt snapshots - │ └── {agent}.md - ├── agents/ # Agent execution logs - │ └── {timestamp}_{agent}_attempt-{N}.log - └── deliverables/ # Security reports and findings - └── ... -configs/ # Configuration files -├── config-schema.json # JSON Schema validation -├── example-config.yaml # Template configuration -├── juice-shop-config.yaml # Juice Shop example -├── keygraph-config.yaml # Keygraph configuration -├── chatwoot-config.yaml # Chatwoot configuration -├── metabase-config.yaml # Metabase configuration -└── cal-com-config.yaml # Cal.com configuration -prompts/ # AI prompt templates -├── shared/ # Shared content for all prompts -│ ├── _target.txt # Target URL template -│ ├── _rules.txt # Rules template -│ ├── _vuln-scope.txt # Vulnerability scope template -│ ├── _exploit-scope.txt # Exploitation scope template -│ └── login-instructions.txt # Login flow template -├── pre-recon-code.txt # Code analysis -├── recon.txt # Reconnaissance -├── vuln-*.txt # Vulnerability assessment -├── exploit-*.txt # Exploitation -└── report-executive.txt # Executive reporting -scripts/ # Utility scripts -└── export-metrics.js # Export metrics to CSV -deliverables/ # Output directory (in target repo) -docs/ # Documentation -├── unified-audit-system-design.md -└── migration-guide.md -``` +**Entry Points:** +- `src/shannon.ts` - Main orchestration (direct CLI) +- `src/temporal/workflows.ts` - Temporal workflow definition +- `src/temporal/activities.ts` - Activity implementations with heartbeats +- `src/temporal/worker.ts` - Worker process entry point +- `src/temporal/client.ts` - CLI client for starting workflows + +**Core Logic:** +- `src/session-manager.ts` - Agent definitions, execution order, parallel groups +- `src/ai/claude-executor.ts` - Claude Agent SDK integration +- `src/config-parser.ts` - YAML config parsing with JSON Schema validation +- `src/audit/` - Crash-safe logging and metrics system + +**Configuration:** +- `Taskfile.yml` - Task runner commands +- `docker-compose.yml` - Temporal server + worker containers +- `configs/` - YAML configs with `config-schema.json` for validation +- `prompts/` - AI prompt templates (`vuln-*.txt`, `exploit-*.txt`, etc.) + +**Output:** +- `audit-logs/{hostname}_{sessionId}/` - Session metrics, agent logs, deliverables ## Troubleshooting @@ -309,8 +286,15 @@ docs/ # Documentation - **"Repository not found"**: Ensure target local directory exists and is accessible - **Concurrent runs blocked**: Only one session can run at a time per target +### Temporal & Docker Issues +- **"Temporal not ready"**: Wait for health check or run `docker compose logs temporal` +- **Worker not processing**: Ensure worker container is running with `docker compose ps` +- **Reset workflow state**: `task stop CLEAN=true` removes all Temporal data and volumes +- **Local apps unreachable**: Use `host.docker.internal` instead of `localhost` for URLs +- **Container permissions**: On Linux, may need `sudo` for docker commands + ### External Tool Dependencies -Missing tools can be skipped using `--pipeline-testing` mode during development: +Missing tools can be skipped using `PIPELINE_TESTING=true` mode during development: - `nmap` - Network scanning - `subfinder` - Subdomain discovery - `whatweb` - Web technology detection @@ -319,6 +303,9 @@ Missing tools can be skipped using `--pipeline-testing` mode during development: ```bash # Export metrics to CSV ./scripts/export-metrics.js --session-id --output metrics.csv + +# View Temporal workflow history +open http://localhost:8233 ``` -Note: For recovery from corrupted state, simply delete `.shannon-store.json` or edit JSON files directly. +Note: For recovery from corrupted state, simply delete `.shannon-store.json` or run `task stop CLEAN=true`. diff --git a/README.md b/README.md index 4f728cb4..897478b6 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,11 @@ Shannon is available in two editions: - [Product Line](#-product-line) - [Setup & Usage Instructions](#-setup--usage-instructions) - [Prerequisites](#prerequisites) - - [Authentication Setup](#authentication-setup) - - [Quick Start with Docker](#quick-start-with-docker) + - [Quick Start](#quick-start) + - [Monitoring Progress](#monitoring-progress) + - [Stopping Shannon](#stopping-shannon) + - [Usage Examples](#usage-examples) - [Configuration (Optional)](#configuration-optional) - - [Usage Patterns](#usage-patterns) - [Output and Results](#output-and-results) - [Sample Reports & Benchmarks](#-sample-reports--benchmarks) - [Architecture](#-architecture) @@ -98,36 +99,72 @@ Shannon is available in two editions: ### Prerequisites -- **Claude Console account with credits** - Required for AI-powered analysis -- **Docker installed** - Primary deployment method +- **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/)) +- **Task** - Task runner for simplified commands ([Install Task](https://taskfile.dev/installation/)) +- **Anthropic API key or Claude Code OAuth token** - Get from [Anthropic Console](https://console.anthropic.com) -### Authentication Setup +### Quick Start -You need either a **Claude Code OAuth token** or an **Anthropic API key** to run Shannon. Get your token from the [Anthropic Console](https://console.anthropic.com) and pass it to Docker via the `-e` flag. +```bash +# 1. Clone Shannon +git clone https://github.com/KeygraphHQ/shannon.git +cd shannon -### Environment Configuration (Recommended) +# 2. Configure credentials (choose one method) -To prevent Claude Code from hitting token limits during long report generation, set the max output tokens environment variable: +# Option A: Export environment variables +export ANTHROPIC_API_KEY="your-api-key" # or CLAUDE_CODE_OAUTH_TOKEN +export CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # recommended -**For local runs:** -```bash -export CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 +# Option B: Create a .env file +cat > .env << 'EOF' +ANTHROPIC_API_KEY=your-api-key +CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 +EOF + +# 3. Run a pentest +task start URL=https://your-app.com REPO=/path/to/your/repo ``` -**For Docker runs:** +Shannon will build the containers, start the workflow, and return a workflow ID. The pentest runs in the background. + +### Monitoring Progress + ```bash --e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 +# View real-time worker logs +task logs + +# Query a specific workflow's progress +task query ID=shannon-1234567890 + +# Open the Temporal Web UI for detailed monitoring +open http://localhost:8233 ``` -### Quick Start with Docker +### Stopping Shannon -#### Build the Container +```bash +# Stop all containers (preserves workflow data) +task stop + +# Full cleanup (removes all data) +task stop CLEAN=true +``` + +### Usage Examples ```bash -docker build -t shannon:latest . +# Basic pentest +task start URL=https://example.com REPO=/path/to/repo + +# With a configuration file +task start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml + +# Custom output directory +task start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports ``` -#### Prepare Your Repository +### Prepare Your Repository Shannon is designed for **web application security testing** and expects all application code to be available in a single directory structure. This works well for: @@ -137,105 +174,35 @@ Shannon is designed for **web application security testing** and expects all app **For monorepos:** ```bash -git clone https://github.com/your-org/your-monorepo.git repos/your-app +git clone https://github.com/your-org/your-monorepo.git /path/to/your-app ``` **For multi-repository applications** (e.g., separate frontend/backend): ```bash -mkdir repos/your-app -cd repos/your-app +mkdir /path/to/your-app +cd /path/to/your-app git clone https://github.com/your-org/frontend.git git clone https://github.com/your-org/backend.git git clone https://github.com/your-org/api.git ``` -**For existing local repositories:** - -```bash -cp -r /path/to/your-existing-repo repos/your-app -``` - -#### Run Your First Pentest - -**With Claude Console OAuth Token:** - -```bash -docker run --rm -it \ - --network host \ - --cap-add=NET_RAW \ - --cap-add=NET_ADMIN \ - -e CLAUDE_CODE_OAUTH_TOKEN="$CLAUDE_CODE_OAUTH_TOKEN" \ - -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ - -v "$(pwd)/repos:/app/repos" \ - -v "$(pwd)/configs:/app/configs" \ - # Comment below line if using custom output directory - -v "$(pwd)/audit-logs:/app/audit-logs" \ - shannon:latest \ - "https://your-app.com/" \ - "/app/repos/your-app" \ - --config /app/configs/example-config.yaml - # Optional: uncomment below for custom output directory - # -v "$(pwd)/reports:/app/reports" \ - # --output /app/reports -``` - -**With Anthropic API Key:** - -```bash -docker run --rm -it \ - --network host \ - --cap-add=NET_RAW \ - --cap-add=NET_ADMIN \ - -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ - -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ - -v "$(pwd)/repos:/app/repos" \ - -v "$(pwd)/configs:/app/configs" \ - # Comment below line if using custom output directory - -v "$(pwd)/audit-logs:/app/audit-logs" \ - shannon:latest \ - "https://your-app.com/" \ - "/app/repos/your-app" \ - --config /app/configs/example-config.yaml - # Optional: uncomment below for custom output directory - # -v "$(pwd)/reports:/app/reports" \ - # --output /app/reports -``` - -#### Platform-Specific Instructions +### Platform-Specific Instructions **For Linux (Native Docker):** -Add the `--user $(id -u):$(id -g)` flag to the Docker commands above to avoid permission issues with volume mounts. Docker Desktop on macOS and Windows handles this automatically, but native Linux Docker requires explicit user mapping. +You may need to run Task commands with `sudo` depending on your Docker setup. If you encounter permission issues with output files, ensure your user has access to the Docker socket. -**Network Capabilities:** +**For macOS:** -- `--cap-add=NET_RAW` - Enables advanced port scanning with nmap -- `--cap-add=NET_ADMIN` - Allows network administration for security tools -- `--network host` - Provides access to target network interfaces +Works out of the box with Docker Desktop installed. **Testing Local Applications:** Docker containers cannot reach `localhost` on your host machine. Use `host.docker.internal` in place of `localhost`: ```bash -docker run --rm -it \ - --add-host=host.docker.internal:host-gateway \ - --cap-add=NET_RAW \ - --cap-add=NET_ADMIN \ - -e CLAUDE_CODE_OAUTH_TOKEN="$CLAUDE_CODE_OAUTH_TOKEN" \ - -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ - -v "$(pwd)/repos:/app/repos" \ - -v "$(pwd)/configs:/app/configs" \ - # Comment below line if using custom output directory - -v "$(pwd)/audit-logs:/app/audit-logs" \ - shannon:latest \ - "http://host.docker.internal:3000" \ - "/app/repos/your-app" \ - --config /app/configs/example-config.yaml - # Optional: uncomment below for custom output directory - # -v "$(pwd)/reports:/app/reports" \ - # --output /app/reports +task start URL=http://host.docker.internal:3000 REPO=/path/to/repo ``` ### Configuration (Optional) @@ -288,12 +255,17 @@ If your application uses two-factor authentication, simply add the TOTP secret t ### Output and Results -All results are saved to `./audit-logs/` by default. Use `--output ` to specify a custom directory. If using `--output`, ensure that path is mounted to an accessible host directory (e.g., `-v "$(pwd)/custom-directory:/app/reports"`). +All results are saved to `./audit-logs/{hostname}_{sessionId}/` by default. Use `--output ` to specify a custom directory. -- **Pre-reconnaissance reports** - External scan results -- **Vulnerability assessments** - Potential vulnerabilities from thorough code analysis and network mapping -- **Exploitation results** - Proof-of-concept attempts -- **Executive reports** - Business-focused security summaries +Output structure: +``` +audit-logs/{hostname}_{sessionId}/ +├── session.json # Metrics and session data +├── agents/ # Per-agent execution logs +├── prompts/ # Prompt snapshots for reproducibility +└── deliverables/ + └── executive-report.md # Final comprehensive security report +``` --- From 89cc30bb9401707baa86827a0952e896b31a3f19 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 17:42:06 -0800 Subject: [PATCH 14/24] refactor: replace Taskfile with bash CLI script - Add shannon bash script with start/logs/query/stop/help commands - Remove Taskfile.yml dependency (no longer requires Task installation) - Update README.md and CLAUDE.md to use ./shannon commands - Update client.ts output to show ./shannon commands --- CLAUDE.md | 29 ++++---- README.md | 21 +++--- Taskfile.yml | 94 ------------------------- shannon | 151 +++++++++++++++++++++++++++++++++++++++++ src/temporal/client.ts | 4 +- 5 files changed, 177 insertions(+), 122 deletions(-) delete mode 100644 Taskfile.yml create mode 100755 shannon diff --git a/CLAUDE.md b/CLAUDE.md index f4d0395b..cbc6f70b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,6 @@ This is an AI-powered penetration testing agent designed for defensive security ### Prerequisites - **Docker** - Container runtime -- **Task** - Task runner ([Install Task](https://taskfile.dev/installation/)) - **Anthropic API key** - Set in `.env` file ### Running the Penetration Testing Agent (Docker + Temporal) @@ -22,27 +21,27 @@ cp .env.example .env # CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # Prevents token limits during long reports # Start a pentest workflow -task start URL= REPO= +./shannon start URL= REPO= ``` Examples: ```bash -task start URL=https://example.com REPO=/path/to/repo -task start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml -task start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports +./shannon start URL=https://example.com REPO=/path/to/repo +./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml +./shannon start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports ``` ### Monitoring Progress ```bash -task logs # View real-time worker logs -task query ID= # Query specific workflow progress +./shannon logs # View real-time worker logs +./shannon query ID= # Query specific workflow progress # Temporal Web UI available at http://localhost:8233 ``` ### Stopping Shannon ```bash -task stop # Stop containers (preserves workflow data) -task stop CLEAN=true # Full cleanup including volumes +./shannon stop # Stop containers (preserves workflow data) +./shannon stop CLEAN=true # Full cleanup including volumes ``` ### Options @@ -68,7 +67,7 @@ TOTP generation is handled automatically via the `generate_totp` MCP tool during npm run build # Run with pipeline testing mode (fast, minimal deliverables) -task start URL= REPO= PIPELINE_TESTING=true +./shannon start URL= REPO= PIPELINE_TESTING=true ``` ## Architecture & Components @@ -94,7 +93,7 @@ Shannon uses Temporal for durable workflow orchestration: Key features: - **Crash recovery** - Workflows resume automatically after worker restart -- **Queryable progress** - Real-time status via `task query` or Temporal Web UI +- **Queryable progress** - Real-time status via `./shannon query` or Temporal Web UI - **Intelligent retry** - Distinguishes transient vs permanent errors - **Parallel execution** - 5 concurrent agents in vulnerability/exploitation phases @@ -243,7 +242,7 @@ The application uses a comprehensive error handling system with: ### Testing Mode The agent includes a testing mode that skips external tool execution for faster development cycles: ```bash -task start URL= REPO= PIPELINE_TESTING=true +./shannon start URL= REPO= PIPELINE_TESTING=true ``` ### Security Focus @@ -271,7 +270,7 @@ The tool should only be used on systems you own or have explicit permission to t - `src/audit/` - Crash-safe logging and metrics system **Configuration:** -- `Taskfile.yml` - Task runner commands +- `shannon` - CLI script for running pentests - `docker-compose.yml` - Temporal server + worker containers - `configs/` - YAML configs with `config-schema.json` for validation - `prompts/` - AI prompt templates (`vuln-*.txt`, `exploit-*.txt`, etc.) @@ -289,7 +288,7 @@ The tool should only be used on systems you own or have explicit permission to t ### Temporal & Docker Issues - **"Temporal not ready"**: Wait for health check or run `docker compose logs temporal` - **Worker not processing**: Ensure worker container is running with `docker compose ps` -- **Reset workflow state**: `task stop CLEAN=true` removes all Temporal data and volumes +- **Reset workflow state**: `./shannon stop CLEAN=true` removes all Temporal data and volumes - **Local apps unreachable**: Use `host.docker.internal` instead of `localhost` for URLs - **Container permissions**: On Linux, may need `sudo` for docker commands @@ -308,4 +307,4 @@ Missing tools can be skipped using `PIPELINE_TESTING=true` mode during developme open http://localhost:8233 ``` -Note: For recovery from corrupted state, simply delete `.shannon-store.json` or run `task stop CLEAN=true`. +Note: For recovery from corrupted state, simply delete `.shannon-store.json` or run `./shannon stop CLEAN=true`. diff --git a/README.md b/README.md index 897478b6..bac7fd5a 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ Shannon is available in two editions: ### Prerequisites - **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/)) -- **Task** - Task runner for simplified commands ([Install Task](https://taskfile.dev/installation/)) - **Anthropic API key or Claude Code OAuth token** - Get from [Anthropic Console](https://console.anthropic.com) ### Quick Start @@ -123,7 +122,7 @@ CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 EOF # 3. Run a pentest -task start URL=https://your-app.com REPO=/path/to/your/repo +./shannon start URL=https://your-app.com REPO=/path/to/your/repo ``` Shannon will build the containers, start the workflow, and return a workflow ID. The pentest runs in the background. @@ -132,10 +131,10 @@ Shannon will build the containers, start the workflow, and return a workflow ID. ```bash # View real-time worker logs -task logs +./shannon logs # Query a specific workflow's progress -task query ID=shannon-1234567890 +./shannon query ID=shannon-1234567890 # Open the Temporal Web UI for detailed monitoring open http://localhost:8233 @@ -145,23 +144,23 @@ open http://localhost:8233 ```bash # Stop all containers (preserves workflow data) -task stop +./shannon stop # Full cleanup (removes all data) -task stop CLEAN=true +./shannon stop CLEAN=true ``` ### Usage Examples ```bash # Basic pentest -task start URL=https://example.com REPO=/path/to/repo +./shannon start URL=https://example.com REPO=/path/to/repo # With a configuration file -task start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml +./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml # Custom output directory -task start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports +./shannon start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports ``` ### Prepare Your Repository @@ -191,7 +190,7 @@ git clone https://github.com/your-org/api.git **For Linux (Native Docker):** -You may need to run Task commands with `sudo` depending on your Docker setup. If you encounter permission issues with output files, ensure your user has access to the Docker socket. +You may need to run commands with `sudo` depending on your Docker setup. If you encounter permission issues with output files, ensure your user has access to the Docker socket. **For macOS:** @@ -202,7 +201,7 @@ Works out of the box with Docker Desktop installed. Docker containers cannot reach `localhost` on your host machine. Use `host.docker.internal` in place of `localhost`: ```bash -task start URL=http://host.docker.internal:3000 REPO=/path/to/repo +./shannon start URL=http://host.docker.internal:3000 REPO=/path/to/repo ``` ### Configuration (Optional) diff --git a/Taskfile.yml b/Taskfile.yml deleted file mode 100644 index 61cf6c7a..00000000 --- a/Taskfile.yml +++ /dev/null @@ -1,94 +0,0 @@ -version: '3' - -dotenv: ['.env'] - -vars: - COMPOSE_FILE: docker-compose.yml - -tasks: - default: - silent: true - cmds: [task help] - - help: - desc: Show usage information - silent: true - cmds: - - | - echo "Shannon - AI Penetration Testing Framework" - echo "" - echo "Usage:" - echo " task start URL= REPO= Start a pentest workflow" - echo " task logs View real-time worker logs" - echo " task query ID= Query workflow progress" - echo " task stop Stop all containers" - echo " task help Show this help message" - echo "" - echo "Options for 'start':" - echo " CONFIG= Configuration file (YAML)" - echo " OUTPUT= Output directory for reports" - echo "" - echo "Options for 'stop':" - echo " CLEAN=true Remove all data including volumes" - echo "" - echo "Examples:" - echo " task start URL=https://example.com REPO=/path/to/repo" - echo " task start URL=https://example.com REPO=/path/to/repo CONFIG=./config.yaml" - echo " task query ID=shannon-1234567890" - echo " task stop CLEAN=true" - echo "" - echo "Monitor workflows at http://localhost:8233" - - start: - desc: Start a pentest workflow - silent: true - requires: - vars: [URL, REPO] - cmds: - - | - if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then - echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" - exit 1 - fi - - TARGET_REPO={{.REPO}} docker compose -f {{.COMPOSE_FILE}} up -d --build - - | - for i in $(seq 1 30); do - docker compose -f {{.COMPOSE_FILE}} exec -T temporal \ - temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" && break - [ $i -eq 30 ] && echo "Timeout waiting for Temporal" && exit 1 - sleep 2 - done - - | - ARGS="" - {{if .CONFIG}}ARGS="$ARGS --config {{.CONFIG}}"{{end}} - {{if .OUTPUT}}ARGS="$ARGS --output {{.OUTPUT}}"{{end}} - {{if eq .PIPELINE_TESTING "true"}}ARGS="$ARGS --pipeline-testing"{{end}} - docker compose -f {{.COMPOSE_FILE}} exec -T worker \ - node dist/temporal/client.js "{{.URL}}" "/target-repo" $ARGS {{.CLI_ARGS}} - - logs: - desc: View real-time worker logs - silent: true - cmds: - - docker compose -f {{.COMPOSE_FILE}} logs -f worker {{.CLI_ARGS}} - - query: - desc: Query workflow progress - silent: true - requires: - vars: [ID] - cmds: - - | - docker compose -f {{.COMPOSE_FILE}} exec -T worker \ - node dist/temporal/query.js "{{.ID}}" - - stop: - desc: Stop all containers - silent: true - cmds: - - | - {{if eq .CLEAN "true"}} - docker compose -f {{.COMPOSE_FILE}} down -v - {{else}} - docker compose -f {{.COMPOSE_FILE}} down - {{end}} diff --git a/shannon b/shannon new file mode 100755 index 00000000..651d94ec --- /dev/null +++ b/shannon @@ -0,0 +1,151 @@ +#!/bin/bash +# Shannon CLI - AI Penetration Testing Framework + +set -e + +COMPOSE_FILE="docker-compose.yml" + +# Load .env if present +if [ -f .env ]; then + set -a + source .env + set +a +fi + +show_help() { + cat << 'EOF' +Shannon - AI Penetration Testing Framework + +Usage: + ./shannon start URL= REPO= Start a pentest workflow + ./shannon logs View real-time worker logs + ./shannon query ID= Query workflow progress + ./shannon stop Stop all containers + ./shannon help Show this help message + +Options for 'start': + CONFIG= Configuration file (YAML) + OUTPUT= Output directory for reports + PIPELINE_TESTING=true Use minimal prompts for fast testing + +Options for 'stop': + CLEAN=true Remove all data including volumes + +Examples: + ./shannon start URL=https://example.com REPO=/path/to/repo + ./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./config.yaml + ./shannon query ID=shannon-1234567890 + ./shannon stop CLEAN=true + +Monitor workflows at http://localhost:8233 +EOF +} + +# Parse KEY=value arguments into variables +parse_args() { + for arg in "$@"; do + case "$arg" in + URL=*) URL="${arg#URL=}" ;; + REPO=*) REPO="${arg#REPO=}" ;; + CONFIG=*) CONFIG="${arg#CONFIG=}" ;; + OUTPUT=*) OUTPUT="${arg#OUTPUT=}" ;; + ID=*) ID="${arg#ID=}" ;; + CLEAN=*) CLEAN="${arg#CLEAN=}" ;; + PIPELINE_TESTING=*) PIPELINE_TESTING="${arg#PIPELINE_TESTING=}" ;; + esac + done +} + +cmd_start() { + parse_args "$@" + + # Validate required vars + if [ -z "$URL" ] || [ -z "$REPO" ]; then + echo "ERROR: URL and REPO are required" + echo "Usage: ./shannon start URL= REPO=" + exit 1 + fi + + # Check for API key + if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then + echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" + exit 1 + fi + + # Start containers + TARGET_REPO="$REPO" docker compose -f "$COMPOSE_FILE" up -d --build + + # Wait for Temporal to be ready + echo "Waiting for Temporal to be ready..." + for i in $(seq 1 30); do + if docker compose -f "$COMPOSE_FILE" exec -T temporal \ + temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING"; then + break + fi + if [ "$i" -eq 30 ]; then + echo "Timeout waiting for Temporal" + exit 1 + fi + sleep 2 + done + + # Build optional args + ARGS="" + [ -n "$CONFIG" ] && ARGS="$ARGS --config $CONFIG" + [ -n "$OUTPUT" ] && ARGS="$ARGS --output $OUTPUT" + [ "$PIPELINE_TESTING" = "true" ] && ARGS="$ARGS --pipeline-testing" + + # Run the client + docker compose -f "$COMPOSE_FILE" exec -T worker \ + node dist/temporal/client.js "$URL" "/target-repo" $ARGS +} + +cmd_logs() { + docker compose -f "$COMPOSE_FILE" logs -f worker "$@" +} + +cmd_query() { + parse_args "$@" + + if [ -z "$ID" ]; then + echo "ERROR: ID is required" + echo "Usage: ./shannon query ID=" + exit 1 + fi + + docker compose -f "$COMPOSE_FILE" exec -T worker \ + node dist/temporal/query.js "$ID" +} + +cmd_stop() { + parse_args "$@" + + if [ "$CLEAN" = "true" ]; then + docker compose -f "$COMPOSE_FILE" down -v + else + docker compose -f "$COMPOSE_FILE" down + fi +} + +# Main command dispatch +case "${1:-help}" in + start) + shift + cmd_start "$@" + ;; + logs) + shift + cmd_logs "$@" + ;; + query) + shift + cmd_query "$@" + ;; + stop) + shift + cmd_stop "$@" + ;; + help|--help|-h|*) + show_help + ;; +esac diff --git a/src/temporal/client.ts b/src/temporal/client.ts index e16e7111..73936a9b 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -163,8 +163,8 @@ async function startPipeline(): Promise { if (!waitForCompletion) { console.log(chalk.bold('Monitor progress:')); console.log(chalk.white(' Web UI: ') + chalk.blue(`http://localhost:8233/namespaces/default/workflows/${workflowId}`)); - console.log(chalk.white(' Logs: ') + chalk.gray('task logs')); - console.log(chalk.white(' Query: ') + chalk.gray(`task query ID=${workflowId}`)); + console.log(chalk.white(' Logs: ') + chalk.gray('./shannon logs')); + console.log(chalk.white(' Query: ') + chalk.gray(`./shannon query ID=${workflowId}`)); console.log(); return; } From e521e98a8f285404868ef22dbd64ee1557b0e147 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 17:50:33 -0800 Subject: [PATCH 15/24] docs: fix deliverable filename in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bac7fd5a..51b9916d 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ audit-logs/{hostname}_{sessionId}/ ├── agents/ # Per-agent execution logs ├── prompts/ # Prompt snapshots for reproducibility └── deliverables/ - └── executive-report.md # Final comprehensive security report + └── comprehensive_security_assessment_report.md # Final comprehensive security report ``` --- From 50629a24abddcadf6baaa9f51875f7ab826e0ec5 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 18:06:44 -0800 Subject: [PATCH 16/24] refactor: remove direct CLI and .shannon-store.json in favor of Temporal - Delete src/shannon.ts direct CLI entry point (Temporal is now the only mode) - Remove .shannon-store.json session lock (Temporal handles workflow deduplication) - Remove broken scripts/export-metrics.js (imported non-existent function) - Update package.json to remove main, start script, and bin entry - Clean up CLAUDE.md and debug.md to remove obsolete references --- .claude/commands/debug.md | 25 +- .gitignore | 1 - CLAUDE.md | 20 - package.json | 5 - scripts/export-metrics.js | 181 --------- src/shannon.ts | 810 -------------------------------------- 6 files changed, 8 insertions(+), 1034 deletions(-) delete mode 100755 scripts/export-metrics.js delete mode 100644 src/shannon.ts diff --git a/.claude/commands/debug.md b/.claude/commands/debug.md index e69da00b..0f4de115 100644 --- a/.claude/commands/debug.md +++ b/.claude/commands/debug.md @@ -31,26 +31,18 @@ ls -lt audit-logs//agents/ cat audit-logs//agents/.log ``` -**Check for lock file issues:** -```bash -# Session lock file (prevents concurrent runs) -cat .shannon-store.json - -# Remove if stale (no active session) -rm .shannon-store.json -``` - ## Step 3: Trace the Call Path For Shannon, trace through these layers: -1. **CLI Entry** → `src/shannon.ts` - Argument parsing, session setup -2. **Config** → `src/config-parser.ts` - YAML loading, schema validation -3. **Session** → `src/session-manager.ts` - Agent definitions, execution order -4. **Audit** → `src/audit/audit-session.ts` - Logging facade, metrics tracking -5. **Executor** → `src/ai/claude-executor.ts` - SDK calls, MCP setup, retry logic -6. **Phases** → `src/phases/pre-recon.ts`, `reporting.ts` - Phase-specific logic -7. **Validation** → `src/queue-validation.ts` - Deliverable checks +1. **Temporal Client** → `src/temporal/client.ts` - Workflow initiation +2. **Workflow** → `src/temporal/workflows.ts` - Pipeline orchestration +3. **Activities** → `src/temporal/activities.ts` - Agent execution with heartbeats +4. **Config** → `src/config-parser.ts` - YAML loading, schema validation +5. **Session** → `src/session-manager.ts` - Agent definitions, execution order +6. **Audit** → `src/audit/audit-session.ts` - Logging facade, metrics tracking +7. **Executor** → `src/ai/claude-executor.ts` - SDK calls, MCP setup, retry logic +8. **Validation** → `src/queue-validation.ts` - Deliverable checks ## Step 4: Identify Root Cause @@ -58,7 +50,6 @@ For Shannon, trace through these layers: | Symptom | Likely Cause | Fix | |---------|--------------|-----| -| "A session is already running" | Stale `.shannon-store.json` | Delete the lock file | | Agent hangs indefinitely | MCP server crashed, Playwright timeout | Check Playwright logs in `/tmp/playwright-*` | | "Validation failed: Missing deliverable" | Agent didn't create expected file | Check `deliverables/` dir, review prompt | | Git checkpoint fails | Uncommitted changes, git lock | Run `git status`, remove `.git/index.lock` | diff --git a/.gitignore b/.gitignore index f0e66bbe..e28c076e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules/ -.shannon-store.json .env audit-logs/ dist/ diff --git a/CLAUDE.md b/CLAUDE.md index cbc6f70b..c113beba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,13 +51,6 @@ OUTPUT= Custom output directory for session folder (default: ./audi PIPELINE_TESTING=true Use minimal prompts for fast pipeline testing ``` -### Direct CLI (Local Development) -For development without Docker/Temporal: -```bash -npm install -shannon [--config ] [--output ] -``` - ### Generate TOTP for Authentication TOTP generation is handled automatically via the `generate_totp` MCP tool during authentication flows. @@ -72,9 +65,6 @@ npm run build ## Architecture & Components -### Main Entry Point -- `src/shannon.ts` - Main orchestration script that coordinates the entire penetration testing workflow (compiles to `dist/shannon.js`) - ### Core Modules - `src/config-parser.ts` - Handles YAML configuration parsing, validation, and distribution to agents - `src/error-handling.ts` - Comprehensive error handling with retry logic and categorized error types @@ -177,7 +167,6 @@ The agent implements a crash-safe audit system with the following features: - `{hostname}_{sessionId}/prompts/` - Exact prompts used for reproducibility - `{hostname}_{sessionId}/agents/` - Turn-by-turn execution logs - `{hostname}_{sessionId}/deliverables/` - Security reports and findings -- **.shannon-store.json**: Minimal session lock file (prevents concurrent runs) **Crash Safety:** - Append-only logging with immediate flush (survives kill -9) @@ -189,7 +178,6 @@ The agent implements a crash-safe audit system with the following features: - 5x faster execution with parallel vulnerability and exploitation phases **Metrics & Reporting:** -- Export metrics to CSV with `./scripts/export-metrics.js` - Phase-level and agent-level timing/cost aggregations - Validation results integrated with metrics @@ -257,7 +245,6 @@ The tool should only be used on systems you own or have explicit permission to t ## Key Files & Directories **Entry Points:** -- `src/shannon.ts` - Main orchestration (direct CLI) - `src/temporal/workflows.ts` - Temporal workflow definition - `src/temporal/activities.ts` - Activity implementations with heartbeats - `src/temporal/worker.ts` - Worker process entry point @@ -281,9 +268,7 @@ The tool should only be used on systems you own or have explicit permission to t ## Troubleshooting ### Common Issues -- **"A session is already running"**: Wait for the current session to complete, or delete `.shannon-store.json` - **"Repository not found"**: Ensure target local directory exists and is accessible -- **Concurrent runs blocked**: Only one session can run at a time per target ### Temporal & Docker Issues - **"Temporal not ready"**: Wait for health check or run `docker compose logs temporal` @@ -300,11 +285,6 @@ Missing tools can be skipped using `PIPELINE_TESTING=true` mode during developme ### Diagnostic & Utility Scripts ```bash -# Export metrics to CSV -./scripts/export-metrics.js --session-id --output metrics.csv - # View Temporal workflow history open http://localhost:8233 ``` - -Note: For recovery from corrupted state, simply delete `.shannon-store.json` or run `./shannon stop CLEAN=true`. diff --git a/package.json b/package.json index b470c720..0d3cb26a 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,8 @@ "name": "shannon", "version": "1.0.0", "type": "module", - "main": "./dist/shannon.js", "scripts": { "build": "tsc", - "start": "node ./dist/shannon.js", "temporal:server": "docker compose -f docker/docker-compose.temporal.yml up temporal -d", "temporal:server:stop": "docker compose -f docker/docker-compose.temporal.yml down", "temporal:worker": "node dist/temporal/worker.js", @@ -29,9 +27,6 @@ "zod": "^3.22.4", "zx": "^8.0.0" }, - "bin": { - "shannon": "./dist/shannon.js" - }, "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^25.0.3", diff --git a/scripts/export-metrics.js b/scripts/export-metrics.js deleted file mode 100755 index 9287272d..00000000 --- a/scripts/export-metrics.js +++ /dev/null @@ -1,181 +0,0 @@ -// 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. - -#!/usr/bin/env node - -/** - * Export Metrics to CSV - * - * Converts session.json from audit-logs into CSV format for spreadsheet analysis. - * - * DATA SOURCE: - * - Reads from: audit-logs/{hostname}_{sessionId}/session.json - * - Source of truth for all metrics, timing, and cost data - * - Automatically created by Shannon during agent execution - * - * CSV OUTPUT: - * - One row per agent with: agent, phase, status, attempts, duration_ms, cost_usd - * - Perfect for importing into Excel/Google Sheets for analysis - * - * USE CASES: - * - Compare performance across multiple sessions - * - Track costs and optimize budget - * - Identify slow agents for optimization - * - Generate charts and visualizations - * - Export data for external reporting tools - * - * EXAMPLES: - * ```bash - * # Export to stdout - * ./scripts/export-metrics.js --session-id abc123 - * - * # Export to file - * ./scripts/export-metrics.js --session-id abc123 --output metrics.csv - * - * # Find session ID from Shannon store - * cat .shannon-store.json | jq '.sessions | keys' - * ``` - * - * NOTE: For raw metrics, just read audit-logs/.../session.json directly. - * This script only exists to provide a spreadsheet-friendly CSV format. - */ - -import chalk from 'chalk'; -import { fs, path } from 'zx'; -import { getSession } from '../src/session-manager.js'; -import { AuditSession } from '../src/audit/index.js'; - -// Parse command-line arguments -function parseArgs() { - const args = { - sessionId: null, - output: null - }; - - for (let i = 2; i < process.argv.length; i++) { - const arg = process.argv[i]; - - if (arg === '--session-id' && process.argv[i + 1]) { - args.sessionId = process.argv[i + 1]; - i++; - } else if (arg === '--output' && process.argv[i + 1]) { - args.output = process.argv[i + 1]; - i++; - } else if (arg === '--help' || arg === '-h') { - printUsage(); - process.exit(0); - } else { - console.log(chalk.red(`❌ Unknown argument: ${arg}`)); - printUsage(); - process.exit(1); - } - } - - return args; -} - -function printUsage() { - console.log(chalk.cyan('\n📊 Export Metrics to CSV')); - console.log(chalk.gray('\nUsage: ./scripts/export-metrics.js [options]\n')); - console.log(chalk.white('Options:')); - console.log(chalk.gray(' --session-id Session ID to export (required)')); - console.log(chalk.gray(' --output Output CSV file path (default: stdout)')); - console.log(chalk.gray(' --help, -h Show this help\n')); - console.log(chalk.white('Examples:')); - console.log(chalk.gray(' # Export to stdout')); - console.log(chalk.gray(' ./scripts/export-metrics.js --session-id abc123\n')); - console.log(chalk.gray(' # Export to file')); - console.log(chalk.gray(' ./scripts/export-metrics.js --session-id abc123 --output metrics.csv\n')); -} - -// Export metrics for a session -async function exportMetrics(sessionId) { - const session = await getSession(sessionId); - if (!session) { - throw new Error(`Session ${sessionId} not found`); - } - - const auditSession = new AuditSession(session); - await auditSession.initialize(); - const metrics = await auditSession.getMetrics(); - - return exportAsCSV(session, metrics); -} - -// Export as CSV -function exportAsCSV(session, metrics) { - const lines = []; - - // Header - lines.push('agent,phase,status,attempts,duration_ms,cost_usd'); - - // Phase mapping - const phaseMap = { - 'pre-recon': 'pre-recon', - 'recon': 'recon', - 'injection-vuln': 'vulnerability-analysis', - 'xss-vuln': 'vulnerability-analysis', - 'auth-vuln': 'vulnerability-analysis', - 'authz-vuln': 'vulnerability-analysis', - 'ssrf-vuln': 'vulnerability-analysis', - 'injection-exploit': 'exploitation', - 'xss-exploit': 'exploitation', - 'auth-exploit': 'exploitation', - 'authz-exploit': 'exploitation', - 'ssrf-exploit': 'exploitation', - 'report': 'reporting' - }; - - // Agent rows - for (const [agentName, agentData] of Object.entries(metrics.metrics.agents)) { - const phase = phaseMap[agentName] || 'unknown'; - - lines.push([ - agentName, - phase, - agentData.status, - agentData.attempts.length, - agentData.final_duration_ms, - agentData.total_cost_usd.toFixed(4) - ].join(',')); - } - - return lines.join('\n'); -} - -// Main execution -async function main() { - const args = parseArgs(); - - if (!args.sessionId) { - console.log(chalk.red('❌ Must specify --session-id')); - printUsage(); - process.exit(1); - } - - console.log(chalk.cyan.bold('\n📊 Exporting Metrics to CSV\n')); - console.log(chalk.gray(`Session ID: ${args.sessionId}\n`)); - - const output = await exportMetrics(args.sessionId); - - if (args.output) { - await fs.writeFile(args.output, output); - console.log(chalk.green(`✅ Exported to: ${args.output}`)); - } else { - console.log(chalk.cyan('CSV Output:\n')); - console.log(output); - } - - console.log(); -} - -main().catch(error => { - console.log(chalk.red.bold(`\n🚨 Fatal error: ${error.message}`)); - if (process.env.DEBUG) { - console.log(chalk.gray(error.stack)); - } - process.exit(1); -}); diff --git a/src/shannon.ts b/src/shannon.ts deleted file mode 100644 index 8eddcdb3..00000000 --- a/src/shannon.ts +++ /dev/null @@ -1,810 +0,0 @@ -#!/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. - -import { path, fs, $ } from 'zx'; -import chalk, { type ChalkInstance } from 'chalk'; -import dotenv from 'dotenv'; - -dotenv.config(); - -// Config and Tools -import { parseConfig, distributeConfig } from './config-parser.js'; -import { checkToolAvailability, handleMissingTools } from './tool-checker.js'; - -// Session -import { AGENTS, getParallelGroups } from './session-manager.js'; -import { getPromptNameForAgent } from './types/agents.js'; -import type { AgentName, PromptName } from './types/index.js'; - -// Setup and Deliverables -import { setupLocalRepo } from './setup/environment.js'; - -// AI and Prompts -import { runClaudePromptWithRetry } from './ai/claude-executor.js'; -import { loadPrompt } from './prompts/prompt-manager.js'; - -// Phases -import { executePreReconPhase } from './phases/pre-recon.js'; -import { assembleFinalReport } from './phases/reporting.js'; - -// Utils -import { timingResults, displayTimingSummary, Timer } from './utils/metrics.js'; -import { formatDuration } from './utils/formatting.js'; -import { generateAuditPath } from './audit/utils.js'; -import type { SessionMetadata } from './audit/utils.js'; -import { AuditSession } from './audit/audit-session.js'; - -// CLI -import { showHelp, displaySplashScreen } from './cli/ui.js'; -import { validateWebUrl, validateRepoPath } from './cli/input-validator.js'; - -// Error Handling -import { PentestError, logError } from './error-handling.js'; - -import type { DistributedConfig } from './types/config.js'; -import type { ToolAvailability } from './tool-checker.js'; -import { safeValidateQueueAndDeliverable } from './queue-validation.js'; - -// Extend global namespace for SHANNON_DISABLE_LOADER -declare global { - var SHANNON_DISABLE_LOADER: boolean | undefined; -} - -// Session Lock File Management -const STORE_PATH = path.join(process.cwd(), '.shannon-store.json'); - -interface Session { - id: string; - webUrl: string; - repoPath: string; - status: 'in-progress' | 'completed' | 'failed'; - startedAt: string; -} - -interface SessionStore { - sessions: Session[]; -} - -function generateSessionId(): string { - return crypto.randomUUID(); -} - -async function loadSessions(): Promise { - try { - if (await fs.pathExists(STORE_PATH)) { - return await fs.readJson(STORE_PATH) as SessionStore; - } - } catch { - // Corrupted file, start fresh - } - return { sessions: [] }; -} - -async function saveSessions(store: SessionStore): Promise { - await fs.writeJson(STORE_PATH, store, { spaces: 2 }); -} - -// Session prevents concurrent runs on same repo - different repos can run in parallel -async function createSession(webUrl: string, repoPath: string): Promise { - const store = await loadSessions(); - - // Check for existing in-progress session - const existing = store.sessions.find( - s => s.repoPath === repoPath && s.status === 'in-progress' - ); - if (existing) { - throw new PentestError( - `Session already in progress for ${repoPath}`, - 'validation', - false, - { sessionId: existing.id } - ); - } - - const session: Session = { - id: generateSessionId(), - webUrl, - repoPath, - status: 'in-progress', - startedAt: new Date().toISOString() - }; - - store.sessions.push(session); - await saveSessions(store); - return session; -} - -async function updateSessionStatus( - sessionId: string, - status: 'in-progress' | 'completed' | 'failed' -): Promise { - const store = await loadSessions(); - const session = store.sessions.find(s => s.id === sessionId); - if (session) { - session.status = status; - await saveSessions(store); - } -} - -interface PromptVariables { - webUrl: string; - repoPath: string; - sourceDir: string; -} - -interface MainResult { - reportPath: string; - auditLogsPath: string; -} - -interface AgentResult { - success: boolean; - duration: number; - cost?: number; - error?: string; - retryable?: boolean; -} - -interface ParallelAgentResult { - agentName: AgentName; - success: boolean; - timing?: number | undefined; - cost?: number | undefined; - attempts: number; - error?: string | undefined; -} - -type VulnType = 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz'; - -interface ParallelAgentConfig { - phaseType: 'vuln' | 'exploit'; - headerText: string; - specialistLabel: string; -} - -interface AgentExecutionContext { - sourceDir: string; - variables: PromptVariables; - distributedConfig: DistributedConfig | null; - pipelineTestingMode: boolean; - sessionMetadata: SessionMetadata; -} - -// Configure zx to disable timeouts (let tools run as long as needed) -$.timeout = 0; - -function getAgentColor(agentName: AgentName): ChalkInstance { - const colorMap: Partial> = { - 'injection-vuln': chalk.red, - 'injection-exploit': chalk.red, - 'xss-vuln': chalk.yellow, - 'xss-exploit': chalk.yellow, - 'auth-vuln': chalk.blue, - 'auth-exploit': chalk.blue, - 'ssrf-vuln': chalk.magenta, - 'ssrf-exploit': chalk.magenta, - 'authz-vuln': chalk.green, - 'authz-exploit': chalk.green - }; - return colorMap[agentName] || chalk.cyan; -} - -// Non-fatal copy - failure logs warning but doesn't halt pipeline -async function consolidateOutputs(sourceDir: string, sessionPath: string): Promise { - const srcDeliverables = path.join(sourceDir, 'deliverables'); - const destDeliverables = path.join(sessionPath, 'deliverables'); - - try { - if (await fs.pathExists(srcDeliverables)) { - await fs.copy(srcDeliverables, destDeliverables, { overwrite: true }); - console.log(chalk.gray(`📄 Deliverables copied to session folder`)); - } else { - console.log(chalk.yellow(`âš ī¸ No deliverables directory found at ${srcDeliverables}`)); - } - } catch (error) { - const err = error as Error; - console.log(chalk.yellow(`âš ī¸ Failed to consolidate deliverables: ${err.message}`)); - } -} - -/** - * Run a single agent - */ -async function runAgent( - agentName: AgentName, - sourceDir: string, - variables: PromptVariables, - distributedConfig: DistributedConfig | null, - pipelineTestingMode: boolean, - sessionMetadata: SessionMetadata -): Promise { - const agent = AGENTS[agentName]; - const promptName = getPromptNameForAgent(agentName); - const prompt = await loadPrompt(promptName, variables, distributedConfig, pipelineTestingMode); - - return await runClaudePromptWithRetry( - prompt, - sourceDir, - '*', - '', - agent.displayName, - agentName, - getAgentColor(agentName), - sessionMetadata - ); -} - -/** - * Execute a single agent with retry logic - */ -async function executeAgentWithRetry( - agentName: AgentName, - context: AgentExecutionContext, - onSuccess?: (agentName: AgentName) => Promise -): Promise { - const { sourceDir, variables, distributedConfig, pipelineTestingMode, sessionMetadata } = context; - const maxAttempts = 3; - let lastError: Error | undefined; - let attempts = 0; - - while (attempts < maxAttempts) { - attempts++; - try { - const result = await runAgent( - agentName, - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); - - if (onSuccess) { - await onSuccess(agentName); - } - - return { - agentName, - success: result.success, - timing: result.duration, - cost: result.cost, - attempts - }; - } catch (error) { - lastError = error as Error; - if (attempts < maxAttempts) { - console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); - await new Promise(resolve => setTimeout(resolve, 5000)); - } - } - } - - return { - agentName, - success: false, - attempts, - error: lastError?.message || 'Unknown error' - }; -} - -/** - * Display results table for parallel agent execution - */ -function displayParallelResults( - results: PromiseSettledResult[], - agents: AgentName[], - headerText: string, - totalDuration: number -): ParallelAgentResult[] { - console.log(chalk.cyan(`\n${headerText}`)); - console.log(chalk.gray('-'.repeat(80))); - console.log(chalk.bold('Agent Status Attempt Duration Cost')); - console.log(chalk.gray('-'.repeat(80))); - - const processedResults: ParallelAgentResult[] = []; - - results.forEach((result, index) => { - const agentName = agents[index]!; - const agentDisplay = agentName.padEnd(22); - - if (result.status === 'fulfilled') { - const data = result.value; - processedResults.push(data); - - if (data.success) { - const duration = formatDuration(data.timing || 0); - const cost = `$${(data.cost || 0).toFixed(4)}`; - - console.log( - `${chalk.green(agentDisplay)} ${chalk.green('Success')} ` + - `${data.attempts}/3 ${duration.padEnd(11)} ${cost}` - ); - } else { - console.log( - `${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` + - `${data.attempts}/3 - -` - ); - if (data.error) { - console.log(chalk.gray(` Error: ${data.error.substring(0, 60)}...`)); - } - } - } else { - processedResults.push({ - agentName, - success: false, - attempts: 3, - error: String(result.reason) - }); - - console.log( - `${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` + - `3/3 - -` - ); - } - }); - - console.log(chalk.gray('-'.repeat(80))); - const successCount = processedResults.filter(r => r.success).length; - console.log(chalk.cyan(`Summary: ${successCount}/${agents.length} succeeded in ${formatDuration(totalDuration)}`)); - - return processedResults; -} - -/** - * Run agents in parallel with retry logic and result display - */ -async function runParallelAgents( - context: AgentExecutionContext, - config: ParallelAgentConfig -): Promise { - const { sourceDir } = context; - const { phaseType, headerText, specialistLabel } = config; - const parallelGroups = getParallelGroups(); - const allAgents = parallelGroups[phaseType]; - - // For exploit phase, filter to only eligible agents - let agents: AgentName[]; - if (phaseType === 'exploit') { - const eligibilityChecks = await Promise.all( - allAgents.map(async (agentName) => { - const vulnAgentName = agentName.replace('-exploit', '-vuln') as AgentName; - const vulnType = vulnAgentName.replace('-vuln', '') as VulnType; - - const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir); - - if (!validation.success || !validation.data?.shouldExploit) { - console.log(chalk.gray(`Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`)); - return { agentName, eligible: false }; - } - - console.log(chalk.blue(`${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`)); - return { agentName, eligible: true }; - }) - ); - - agents = eligibilityChecks - .filter(check => check.eligible) - .map(check => check.agentName); - - if (agents.length === 0) { - console.log(chalk.gray('No exploitation agents eligible (no vulnerabilities found)')); - return []; - } - } else { - agents = allAgents; - } - - console.log(chalk.cyan(`\nStarting ${agents.length} ${specialistLabel} in parallel...`)); - console.log(chalk.gray(' Specialists: ' + agents.join(', '))); - console.log(); - - const startTime = Date.now(); - - // Build onSuccess callback for vuln phase (validation logging) - const onSuccess = phaseType === 'vuln' - ? async (agentName: AgentName): Promise => { - const vulnType = agentName.replace('-vuln', '') as VulnType; - try { - const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir); - if (validation.success && validation.data) { - const message = validation.data.shouldExploit - ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` - : 'No vulnerabilities found'; - console.log(chalk.blue(`${agentName}: ${message}`)); - } - } catch { - // Validation failure is non-critical - } - } - : undefined; - - const results = await Promise.allSettled( - agents.map(async (agentName, index) => { - // Add 2-second stagger to prevent API overwhelm - await new Promise(resolve => setTimeout(resolve, index * 2000)); - return executeAgentWithRetry(agentName, context, onSuccess); - }) - ); - - const totalDuration = Date.now() - startTime; - - return displayParallelResults(results, agents, headerText, totalDuration); -} - -// Setup graceful cleanup on process signals -process.on('SIGINT', async () => { - console.log(chalk.yellow('\nâš ī¸ Received SIGINT, cleaning up...')); - - process.exit(0); -}); - -process.on('SIGTERM', async () => { - console.log(chalk.yellow('\nâš ī¸ Received SIGTERM, cleaning up...')); - - process.exit(0); -}); - -// Main orchestration function -async function main( - webUrl: string, - repoPath: string, - configPath: string | null = null, - pipelineTestingMode: boolean = false, - disableLoader: boolean = false, - outputPath: string | null = null -): Promise { - // Set global flag for loader control - global.SHANNON_DISABLE_LOADER = disableLoader; - - const totalTimer = new Timer('total-execution'); - timingResults.total = totalTimer; - - // Display splash screen - await displaySplashScreen(); - - console.log(chalk.cyan.bold('🚀 AI PENETRATION TESTING AGENT')); - console.log(chalk.cyan(`đŸŽ¯ Target: ${webUrl}`)); - console.log(chalk.cyan(`📁 Source: ${repoPath}`)); - if (configPath) { - console.log(chalk.cyan(`âš™ī¸ Config: ${configPath}`)); - } - if (outputPath) { - console.log(chalk.cyan(`📂 Output: ${outputPath}`)); - } - console.log(chalk.gray('─'.repeat(60))); - - // Parse configuration if provided - let distributedConfig: DistributedConfig | null = null; - if (configPath) { - try { - // Resolve config path - check configs folder if relative path - let resolvedConfigPath = configPath; - if (!path.isAbsolute(configPath)) { - const configsDir = path.join(process.cwd(), 'configs'); - const configInConfigsDir = path.join(configsDir, configPath); - // Check if file exists in configs directory, otherwise use original path - if (await fs.pathExists(configInConfigsDir)) { - resolvedConfigPath = configInConfigsDir; - } - } - - const config = await parseConfig(resolvedConfigPath); - distributedConfig = distributeConfig(config); - console.log(chalk.green(`✅ Configuration loaded successfully`)); - } catch (error) { - await logError(error as Error, `Configuration loading from ${configPath}`); - throw error; // Let the main error boundary handle it - } - } - - // Check tool availability - const toolAvailability: ToolAvailability = await checkToolAvailability(); - handleMissingTools(toolAvailability); - - // Setup local repository - console.log(chalk.blue('📁 Setting up local repository...')); - let sourceDir: string; - try { - sourceDir = await setupLocalRepo(repoPath); - console.log(chalk.green('✅ Local repository setup successfully')); - } catch (error) { - const err = error as Error; - console.log(chalk.red(`❌ Failed to setup local repository: ${err.message}`)); - console.log(chalk.gray('This could be due to:')); - console.log(chalk.gray(' - Insufficient permissions')); - console.log(chalk.gray(' - Repository path not accessible')); - console.log(chalk.gray(' - Git initialization issues')); - console.log(chalk.gray(' - Insufficient disk space')); - process.exit(1); - } - - const variables: PromptVariables = { webUrl, repoPath, sourceDir }; - - // Create session (acts as lock file) - const session: Session = await createSession(webUrl, repoPath); - console.log(chalk.blue(`Session created: ${session.id.substring(0, 8)}...`)); - - // Session metadata for audit logging - const sessionMetadata: SessionMetadata = { - id: session.id, - webUrl, - repoPath: sourceDir, - ...(outputPath && { outputPath }) - }; - - // Create outputs directory in source directory - try { - const outputsDir = path.join(sourceDir, 'outputs'); - await fs.ensureDir(outputsDir); - await fs.ensureDir(path.join(outputsDir, 'schemas')); - await fs.ensureDir(path.join(outputsDir, 'scans')); - } catch (error) { - const err = error as Error; - throw new PentestError( - `Failed to create output directories: ${err.message}`, - 'filesystem', - false, - { sourceDir, originalError: err.message } - ); - } - - try { - // PHASE 1: PRE-RECONNAISSANCE - const { duration: preReconDuration } = await executePreReconPhase( - webUrl, - sourceDir, - variables, - distributedConfig, - toolAvailability, - pipelineTestingMode, - session.id, - outputPath - ); - console.log(chalk.green(`Pre-reconnaissance complete in ${formatDuration(preReconDuration)}`)); - - // PHASE 2: RECONNAISSANCE - console.log(chalk.magenta.bold('\n🔎 PHASE 2: RECONNAISSANCE')); - console.log(chalk.magenta('Analyzing initial findings...')); - const reconTimer = new Timer('phase-2-recon'); - - await runAgent( - 'recon', - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); - const reconDuration = reconTimer.stop(); - console.log(chalk.green(`✅ Reconnaissance complete in ${formatDuration(reconDuration)}`)); - - // PHASE 3: VULNERABILITY ANALYSIS - const vulnTimer = new Timer('phase-3-vulnerability-analysis'); - console.log(chalk.red.bold('\n🚨 PHASE 3: VULNERABILITY ANALYSIS')); - - const executionContext: AgentExecutionContext = { - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - }; - - const vulnResults = await runParallelAgents(executionContext, { - phaseType: 'vuln', - headerText: 'Vulnerability Analysis Results', - specialistLabel: 'vulnerability analysis specialists' - }); - - const vulnDuration = vulnTimer.stop(); - console.log(chalk.green(`✅ Vulnerability analysis phase complete in ${formatDuration(vulnDuration)}`)); - - // PHASE 4: EXPLOITATION - const exploitTimer = new Timer('phase-4-exploitation'); - console.log(chalk.red.bold('\nđŸ’Ĩ PHASE 4: EXPLOITATION')); - - const exploitResults = await runParallelAgents(executionContext, { - phaseType: 'exploit', - headerText: 'Exploitation Results', - specialistLabel: 'exploitation specialists' - }); - - const exploitDuration = exploitTimer.stop(); - console.log(chalk.green(`✅ Exploitation phase complete in ${formatDuration(exploitDuration)}`)); - - // PHASE 5: REPORTING - console.log(chalk.greenBright.bold('\n📊 PHASE 5: REPORTING')); - console.log(chalk.greenBright('Generating executive summary and assembling final report...')); - const reportTimer = new Timer('phase-5-reporting'); - - // Assemble all deliverables into a single concatenated report - console.log(chalk.blue('📝 Assembling deliverables from specialist agents...')); - try { - await assembleFinalReport(sourceDir); - } catch (error) { - const err = error as Error; - console.log(chalk.red(`❌ Error assembling final report: ${err.message}`)); - } - - // Run reporter agent to create executive summary - console.log(chalk.blue('Generating executive summary and cleaning up report...')); - await runAgent( - 'report', - sourceDir, - variables, - distributedConfig, - pipelineTestingMode, - sessionMetadata - ); - - const reportDuration = reportTimer.stop(); - console.log(chalk.green(`✅ Final report generated in ${formatDuration(reportDuration)}`)); - - // Calculate final timing - timingResults.total.stop(); - - // Mark session as completed in both stores - await updateSessionStatus(session.id, 'completed'); - - // Update audit system's session.json status - const auditSession = new AuditSession(sessionMetadata); - await auditSession.updateSessionStatus('completed'); - - // Display comprehensive timing summary - displayTimingSummary(); - - console.log(chalk.cyan.bold('\n🎉 PENETRATION TESTING COMPLETE!')); - console.log(chalk.gray('─'.repeat(60))); - - // Calculate audit logs path - const auditLogsPath = generateAuditPath(sessionMetadata); - - // Consolidate deliverables into the session folder - await consolidateOutputs(sourceDir, auditLogsPath); - console.log(chalk.green(`\n📂 All outputs consolidated: ${auditLogsPath}`)); - - return { - reportPath: path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md'), - auditLogsPath - }; - - } catch (error) { - // Mark session as failed in both stores - await updateSessionStatus(session.id, 'failed'); - - // Update audit system's session.json status - const auditSession = new AuditSession(sessionMetadata); - await auditSession.updateSessionStatus('failed'); - - throw error; - } -} - -// Entry point - handle both direct node execution and shebang execution -let args = process.argv.slice(2); -// If first arg is the script name (from shebang), remove it -if (args[0] && args[0].includes('shannon')) { - args = args.slice(1); -} - -// Parse flags and arguments -let configPath: string | null = null; -let outputPath: string | null = null; -let pipelineTestingMode = false; -let disableLoader = false; -const nonFlagArgs: string[] = []; - -for (let i = 0; i < args.length; i++) { - if (args[i] === '--config') { - if (i + 1 < args.length) { - configPath = args[i + 1]!; - i++; // Skip the next argument - } else { - console.log(chalk.red('❌ --config flag requires a file path')); - process.exit(1); - } - } else if (args[i] === '--output') { - if (i + 1 < args.length) { - outputPath = path.resolve(args[i + 1]!); - i++; // Skip the next argument - } else { - console.log(chalk.red('❌ --output flag requires a directory path')); - process.exit(1); - } - } else if (args[i] === '--pipeline-testing') { - pipelineTestingMode = true; - } else if (args[i] === '--disable-loader') { - disableLoader = true; - } else if (!args[i]!.startsWith('-')) { - nonFlagArgs.push(args[i]!); - } -} - -// Handle help flag -if (args.includes('--help') || args.includes('-h') || args.includes('help')) { - showHelp(); - process.exit(0); -} - -// Handle no arguments - show help -if (nonFlagArgs.length === 0) { - console.log(chalk.red.bold('❌ Error: No arguments provided\n')); - showHelp(); - process.exit(1); -} - -// Handle insufficient arguments -if (nonFlagArgs.length < 2) { - console.log(chalk.red('❌ Both WEB_URL and REPO_PATH are required')); - console.log(chalk.gray('Usage: shannon [--config config.yaml]')); - console.log(chalk.gray('Help: shannon --help')); - process.exit(1); -} - -const [webUrl, repoPath] = nonFlagArgs; - -// Validate web URL -const webUrlValidation = validateWebUrl(webUrl!); -if (!webUrlValidation.valid) { - console.log(chalk.red(`❌ Invalid web URL: ${webUrlValidation.error}`)); - console.log(chalk.gray(`Expected format: https://example.com`)); - process.exit(1); -} - -// Validate repository path -const repoPathValidation = await validateRepoPath(repoPath!); -if (!repoPathValidation.valid) { - console.log(chalk.red(`❌ Invalid repository path: ${repoPathValidation.error}`)); - console.log(chalk.gray(`Expected: Accessible local directory path`)); - process.exit(1); -} - -// Success - show validated inputs -console.log(chalk.green('✅ Input validation passed:')); -console.log(chalk.gray(` Target Web URL: ${webUrl}`)); -console.log(chalk.gray(` Target Repository: ${repoPathValidation.path}\n`)); -console.log(chalk.gray(` Config Path: ${configPath}\n`)); -if (outputPath) { - console.log(chalk.gray(` Output Path: ${outputPath}\n`)); -} -if (pipelineTestingMode) { - console.log(chalk.yellow('⚡ PIPELINE TESTING MODE ENABLED - Using minimal test prompts for fast pipeline validation\n')); -} -if (disableLoader) { - console.log(chalk.yellow('âš™ī¸ LOADER DISABLED - Progress indicator will not be shown\n')); -} - -try { - const result = await main(webUrl!, repoPathValidation.path!, configPath, pipelineTestingMode, disableLoader, outputPath); - console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:')); - console.log(chalk.cyan(result.reportPath)); - console.log(chalk.green.bold('\n📂 AUDIT LOGS AVAILABLE:')); - console.log(chalk.cyan(result.auditLogsPath)); - -} catch (error) { - // Enhanced error boundary with proper logging - if (error instanceof PentestError) { - await logError(error, 'Main execution failed'); - console.log(chalk.red.bold('\n🚨 PENTEST EXECUTION FAILED')); - console.log(chalk.red(` Type: ${error.type}`)); - console.log(chalk.red(` Retryable: ${error.retryable ? 'Yes' : 'No'}`)); - - if (error.retryable) { - console.log(chalk.yellow(' Consider running the command again or checking network connectivity.')); - } - } else { - const err = error as Error; - console.log(chalk.red.bold('\n🚨 UNEXPECTED ERROR OCCURRED')); - console.log(chalk.red(` Error: ${err?.message || err?.toString() || 'Unknown error'}`)); - - if (process.env.DEBUG) { - console.log(chalk.gray(` Stack: ${err?.stack || 'No stack trace available'}`)); - } - } - - process.exit(1); -} From 65b9bc4690adaa6601aab571409d7f50c1dbc0cb Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Mon, 12 Jan 2026 18:36:07 -0800 Subject: [PATCH 17/24] chore: remove licensing comments from prompt files to prevent leaking into actual prompts --- prompts/exploit-auth.txt | 4 ---- prompts/exploit-authz.txt | 4 ---- prompts/exploit-injection.txt | 4 ---- prompts/exploit-ssrf.txt | 4 ---- prompts/exploit-xss.txt | 4 ---- prompts/pipeline-testing/exploit-auth.txt | 4 ---- prompts/pipeline-testing/exploit-authz.txt | 4 ---- prompts/pipeline-testing/exploit-injection.txt | 4 ---- prompts/pipeline-testing/exploit-ssrf.txt | 4 ---- prompts/pipeline-testing/exploit-xss.txt | 4 ---- prompts/pipeline-testing/pre-recon-code.txt | 4 ---- prompts/pipeline-testing/recon.txt | 4 ---- prompts/pipeline-testing/report-executive.txt | 4 ---- prompts/pipeline-testing/vuln-auth.txt | 4 ---- prompts/pipeline-testing/vuln-authz.txt | 4 ---- prompts/pipeline-testing/vuln-injection.txt | 4 ---- prompts/pipeline-testing/vuln-ssrf.txt | 4 ---- prompts/pipeline-testing/vuln-xss.txt | 4 ---- prompts/pre-recon-code.txt | 4 ---- prompts/recon.txt | 4 ---- prompts/report-executive.txt | 4 ---- prompts/shared/_exploit-scope.txt | 2 -- prompts/shared/_rules.txt | 2 -- prompts/shared/_target.txt | 2 -- prompts/shared/_vuln-scope.txt | 2 -- prompts/shared/login-instructions.txt | 2 -- prompts/vuln-auth.txt | 4 ---- prompts/vuln-authz.txt | 4 ---- prompts/vuln-injection.txt | 4 ---- prompts/vuln-ssrf.txt | 4 ---- prompts/vuln-xss.txt | 4 ---- 31 files changed, 114 deletions(-) diff --git a/prompts/exploit-auth.txt b/prompts/exploit-auth.txt index 979a5623..b36d8a82 100644 --- a/prompts/exploit-auth.txt +++ b/prompts/exploit-auth.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Authentication Exploitation Specialist. You are not an analyst; you are an active penetration tester. Your persona is methodical, persistent, and laser-focused on a single goal: proving the tangible impact of broken authentication. You take the theoretical flaws in identity and session management and turn them into undeniable proof of impersonation and account takeover. diff --git a/prompts/exploit-authz.txt b/prompts/exploit-authz.txt index 31a9a514..658f8dfe 100644 --- a/prompts/exploit-authz.txt +++ b/prompts/exploit-authz.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Authorization Exploitation Specialist. You are not an analyst; you are an active penetration tester. Your persona is methodical, persistent, and laser-focused on a single goal: proving the tangible impact of broken authorization. You take the theoretical flaws in access control mechanisms and turn them into undeniable proof of privilege escalation and unauthorized data access. diff --git a/prompts/exploit-injection.txt b/prompts/exploit-injection.txt index aa0b3a8e..5f47ed87 100644 --- a/prompts/exploit-injection.txt +++ b/prompts/exploit-injection.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Injection Exploitation Specialist. Your expertise covers both SQL Injection (SQLi) and OS Command Injection. You are not an analyst; you are an active penetration tester. Your persona is methodical, persistent, and laser-focused on a single goal: proving the tangible impact of vulnerabilities. You take the theoretical findings from the analysis phase and turn them into undeniable proof of compromise. diff --git a/prompts/exploit-ssrf.txt b/prompts/exploit-ssrf.txt index 41de156e..163caa42 100644 --- a/prompts/exploit-ssrf.txt +++ b/prompts/exploit-ssrf.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Server-Side Request Forgery (SSRF) Exploitation Specialist. You are not an analyst; you are an active penetration tester. Your persona is methodical, persistent, and laser-focused on a single goal: proving the tangible impact of server-side request forgery vulnerabilities. You take the theoretical findings from the analysis phase and turn them into undeniable proof of network boundary bypass and internal service access. diff --git a/prompts/exploit-xss.txt b/prompts/exploit-xss.txt index b90f11b6..3e3eb8de 100644 --- a/prompts/exploit-xss.txt +++ b/prompts/exploit-xss.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Cross-Site Scripting (XSS) Exploitation Specialist. You are not an analyst; you are an active penetration tester. Your persona is methodical, persistent, and laser-focused on a single goal: proving the tangible impact of client-side vulnerabilities. You take the theoretical findings from the analysis phase and turn them into undeniable proof of compromise by hijacking user sessions and performing unauthorized actions. diff --git a/prompts/pipeline-testing/exploit-auth.txt b/prompts/pipeline-testing/exploit-auth.txt index ca16a8ef..7c8e4fe6 100644 --- a/prompts/pipeline-testing/exploit-auth.txt +++ b/prompts/pipeline-testing/exploit-auth.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Authentication Exploitation Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/exploit-authz.txt b/prompts/pipeline-testing/exploit-authz.txt index a02b798d..aded0f99 100644 --- a/prompts/pipeline-testing/exploit-authz.txt +++ b/prompts/pipeline-testing/exploit-authz.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Authorization Exploitation Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/exploit-injection.txt b/prompts/pipeline-testing/exploit-injection.txt index 72637288..2effbd6f 100644 --- a/prompts/pipeline-testing/exploit-injection.txt +++ b/prompts/pipeline-testing/exploit-injection.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Injection Exploitation Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/exploit-ssrf.txt b/prompts/pipeline-testing/exploit-ssrf.txt index a421932e..4a0cc0a5 100644 --- a/prompts/pipeline-testing/exploit-ssrf.txt +++ b/prompts/pipeline-testing/exploit-ssrf.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for SSRF Exploitation Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/exploit-xss.txt b/prompts/pipeline-testing/exploit-xss.txt index 0b50bc0f..0ee9cbe6 100644 --- a/prompts/pipeline-testing/exploit-xss.txt +++ b/prompts/pipeline-testing/exploit-xss.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for XSS Exploitation Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/pre-recon-code.txt b/prompts/pipeline-testing/pre-recon-code.txt index ffcbd275..3911d8a8 100644 --- a/prompts/pipeline-testing/pre-recon-code.txt +++ b/prompts/pipeline-testing/pre-recon-code.txt @@ -1,5 +1 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - Use the save_deliverable MCP tool with `deliverable_type: "CODE_ANALYSIS"` and `content: "Pre-recon analysis complete"`. Then say "Done". \ No newline at end of file diff --git a/prompts/pipeline-testing/recon.txt b/prompts/pipeline-testing/recon.txt index bee6a337..ee8fd92e 100644 --- a/prompts/pipeline-testing/recon.txt +++ b/prompts/pipeline-testing/recon.txt @@ -1,5 +1 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - Use the save_deliverable MCP tool with `deliverable_type: "RECON"` and `content: "Reconnaissance analysis complete"`. Then say "Done". \ No newline at end of file diff --git a/prompts/pipeline-testing/report-executive.txt b/prompts/pipeline-testing/report-executive.txt index bd62c676..7bf21046 100644 --- a/prompts/pipeline-testing/report-executive.txt +++ b/prompts/pipeline-testing/report-executive.txt @@ -1,5 +1 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - Read `deliverables/comprehensive_security_assessment_report.md`, prepend "# Security Assessment Report\n\n**Target:** {{WEB_URL}}\n\n" to the content, and save it back. Say "Done". diff --git a/prompts/pipeline-testing/vuln-auth.txt b/prompts/pipeline-testing/vuln-auth.txt index 11d38015..afc5116a 100644 --- a/prompts/pipeline-testing/vuln-auth.txt +++ b/prompts/pipeline-testing/vuln-auth.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Auth Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/vuln-authz.txt b/prompts/pipeline-testing/vuln-authz.txt index 8859c466..5f42cc00 100644 --- a/prompts/pipeline-testing/vuln-authz.txt +++ b/prompts/pipeline-testing/vuln-authz.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Authorization Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/vuln-injection.txt b/prompts/pipeline-testing/vuln-injection.txt index 29e3928f..9b0c8426 100644 --- a/prompts/pipeline-testing/vuln-injection.txt +++ b/prompts/pipeline-testing/vuln-injection.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for Injection Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/vuln-ssrf.txt b/prompts/pipeline-testing/vuln-ssrf.txt index b8f4129f..9198eddb 100644 --- a/prompts/pipeline-testing/vuln-ssrf.txt +++ b/prompts/pipeline-testing/vuln-ssrf.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for SSRF Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pipeline-testing/vuln-xss.txt b/prompts/pipeline-testing/vuln-xss.txt index e81002cf..23c4f0e9 100644 --- a/prompts/pipeline-testing/vuln-xss.txt +++ b/prompts/pipeline-testing/vuln-xss.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - ## đŸ§Ē Pipeline Testing: MCP Isolation Test for XSS Agent **MCP Server Assignment:** Using `{{MCP_SERVER}}` for browser automation testing. diff --git a/prompts/pre-recon-code.txt b/prompts/pre-recon-code.txt index 9473b5c7..2d54c5d3 100644 --- a/prompts/pre-recon-code.txt +++ b/prompts/pre-recon-code.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - Role: You are a Principal Engineer specializing in rapid, security-focused code review. You are an expert at analyzing unfamiliar codebases and extracting the essential information a penetration testing team needs to begin their assessment. Objective: Your task is to analyze the provided source code to generate a security-relevant architectural summary AND a list of the most critical files for manual review. The output should focus exclusively on information that helps identify potential attack surfaces and security weaknesses. diff --git a/prompts/recon.txt b/prompts/recon.txt index a0efbefd..b17e7da4 100644 --- a/prompts/recon.txt +++ b/prompts/recon.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a world-class Reconnaissance Analyst and Attack Surface Mapper. Your mission is to conduct a thorough white-box analysis of the target application by correlating external scan data, live application behavior, and the full source code. diff --git a/prompts/report-executive.txt b/prompts/report-executive.txt index de248478..565ba25a 100644 --- a/prompts/report-executive.txt +++ b/prompts/report-executive.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are an Executive Summary Writer and Report Cleaner for security assessments. Your job is to: 1. MODIFY the existing concatenated report by adding an executive summary at the top diff --git a/prompts/shared/_exploit-scope.txt b/prompts/shared/_exploit-scope.txt index dfa96c8b..a7db4917 100644 --- a/prompts/shared/_exploit-scope.txt +++ b/prompts/shared/_exploit-scope.txt @@ -1,5 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. **MANDATORY EXPLOITATION CONSTRAINTS:** - ALL exploitation attempts MUST be performed against {{WEB_URL}} from external network - DO NOT attempt to exploit vulnerabilities that require: diff --git a/prompts/shared/_rules.txt b/prompts/shared/_rules.txt index 08d06bcf..566ea181 100644 --- a/prompts/shared/_rules.txt +++ b/prompts/shared/_rules.txt @@ -1,4 +1,2 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. Rules to Avoid: {{RULES_AVOID}} \ No newline at end of file diff --git a/prompts/shared/_target.txt b/prompts/shared/_target.txt index 396f95bf..822fd092 100644 --- a/prompts/shared/_target.txt +++ b/prompts/shared/_target.txt @@ -1,3 +1 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. URL: {{WEB_URL}} \ No newline at end of file diff --git a/prompts/shared/_vuln-scope.txt b/prompts/shared/_vuln-scope.txt index b4f5e5fa..457ce234 100644 --- a/prompts/shared/_vuln-scope.txt +++ b/prompts/shared/_vuln-scope.txt @@ -1,3 +1 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. **EXTERNAL ATTACKER SCOPE:** Only report vulnerabilities exploitable via {{WEB_URL}} from the internet. Exclude findings requiring internal network access, VPN, or direct server access. \ No newline at end of file diff --git a/prompts/shared/login-instructions.txt b/prompts/shared/login-instructions.txt index 1852fed2..5f481be6 100644 --- a/prompts/shared/login-instructions.txt +++ b/prompts/shared/login-instructions.txt @@ -1,5 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. **IF you are not already logged in when you open playwright, these are the login instructions** **NOTE: playwright automatically saves sessions so ensure you are not already logged in before executing these steps** diff --git a/prompts/vuln-auth.txt b/prompts/vuln-auth.txt index 9dc8db7a..1cc6ac77 100644 --- a/prompts/vuln-auth.txt +++ b/prompts/vuln-auth.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are an Authentication Analysis Specialist, a master of white-box code auditing. Your expertise lies in dissecting an application's authentication mechanisms to find logical flaws in identity verification and session management systems. diff --git a/prompts/vuln-authz.txt b/prompts/vuln-authz.txt index 4c618cc8..2bdfc928 100644 --- a/prompts/vuln-authz.txt +++ b/prompts/vuln-authz.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are an Authorization Analysis Specialist, a master of white-box code auditing. Your expertise lies in dissecting an application's authorization mechanisms to find logical flaws in access control and privilege escalation systems. diff --git a/prompts/vuln-injection.txt b/prompts/vuln-injection.txt index 586373dd..877ca80b 100644 --- a/prompts/vuln-injection.txt +++ b/prompts/vuln-injection.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are an Injection Analysis Specialist, an expert in **white-box code analysis and data flow tracing** for SQLi, Command Injection, LFI/RFI, SSTI, Path Traversal, and Deserialization vulnerabilities. Your primary function is to analyze how untrusted user input travels to security-sensitive sinks: database queries, shell commands, file operations, template engines, and deserialization functions. diff --git a/prompts/vuln-ssrf.txt b/prompts/vuln-ssrf.txt index 7b413db5..649cd684 100644 --- a/prompts/vuln-ssrf.txt +++ b/prompts/vuln-ssrf.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a Server-Side Request Forgery (SSRF) Analysis Specialist, an expert in white-box code analysis and data flow tracing for server-side request vulnerabilities. Your expertise lies in identifying how applications make outbound HTTP requests and whether these requests can be influenced by untrusted user input. diff --git a/prompts/vuln-xss.txt b/prompts/vuln-xss.txt index 7f6ed4e1..2b20502f 100644 --- a/prompts/vuln-xss.txt +++ b/prompts/vuln-xss.txt @@ -1,7 +1,3 @@ -# This Source Code Form is subject to the terms of the AGPL, v. 3.0 -# This section above is metadata and not part of the prompt. -=== PROMPT === - You are a Cross-Site Scripting (XSS) Analysis Specialist focused **solely on vulnerability analysis** (no exploitation). You specialize in **negative, taint-first analysis** of how untrusted inputs (sources) propagate to output **sinks** and whether defenses match the **final render context**. You follow the Injection specialist and precede Exploitation. From c12eca046cc0b113037dae0b475233dce11015c5 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 10:52:26 -0800 Subject: [PATCH 18/24] fix: resolve parallel workflow race conditions and retry logic bugs - Fix save_deliverable race condition using closure pattern instead of global variable - Fix error classification order so OutputValidationError matches before generic validation - Fix ApplicationFailure re-classification bug by checking instanceof before re-throwing - Add per-error-type retry limits (3 for output validation, 50 for billing) - Add fast retry intervals for pipeline testing mode (10s vs 5min) - Increase worker concurrent activities to 25 for parallel workflows --- CLAUDE.md | 7 +- docker-compose.yml | 3 +- mcp-server/src/index.ts | 22 +++-- mcp-server/src/tools/save-deliverable.ts | 103 +++++++++++++---------- mcp-server/src/utils/file-operations.ts | 12 ++- shannon | 74 ++++++++++++---- src/error-handling.ts | 12 ++- src/temporal/activities.ts | 20 +++++ src/temporal/worker.ts | 4 +- src/temporal/workflows.ts | 83 +++++++++++------- 10 files changed, 224 insertions(+), 116 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c113beba..04bce8c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,9 +46,10 @@ Examples: ### Options ```bash -CONFIG= YAML configuration file for authentication and testing parameters -OUTPUT= Custom output directory for session folder (default: ./audit-logs/) -PIPELINE_TESTING=true Use minimal prompts for fast pipeline testing +CONFIG= YAML configuration file for authentication and testing parameters +OUTPUT= Custom output directory for session folder (default: ./audit-logs/) +PIPELINE_TESTING=true Use minimal prompts and fast retry intervals (10s instead of 5min) +REBUILD=true Force Docker rebuild with --no-cache (use when code changes aren't picked up) ``` ### Generate TOTP for Authentication diff --git a/docker-compose.yml b/docker-compose.yml index 85582199..7d509e23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,8 @@ services: condition: service_healthy volumes: - ./prompts:/app/prompts - - ${TARGET_REPO:-/tmp/target-repo}:/target-repo + - ${TARGET_REPO:-.}:/target-repo + - ${BENCHMARKS_BASE:-.}:/benchmarks shm_size: 2gb ipc: host security_opt: diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 934f61b0..0844e967 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -11,22 +11,25 @@ * for Shannon penetration testing agents. * * Replaces bash script invocations with native tool access. + * + * Uses factory pattern to create tools with targetDir captured in closure, + * ensuring thread-safety when multiple workflows run in parallel. */ import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; -import { saveDeliverableTool } from './tools/save-deliverable.js'; +import { createSaveDeliverableTool } from './tools/save-deliverable.js'; import { generateTotpTool } from './tools/generate-totp.js'; -declare global { - var __SHANNON_TARGET_DIR: string | undefined; -} - /** * Create Shannon Helper MCP Server with target directory context + * + * Each workflow should create its own MCP server instance with its targetDir. + * The save_deliverable tool captures targetDir in a closure, preventing race + * conditions when multiple workflows run in parallel. */ export function createShannonHelperServer(targetDir: string): ReturnType { - // Store target directory for tool access - global.__SHANNON_TARGET_DIR = targetDir; + // Create save_deliverable tool with targetDir in closure (no global variable) + const saveDeliverableTool = createSaveDeliverableTool(targetDir); return createSdkMcpServer({ name: 'shannon-helper', @@ -35,8 +38,9 @@ export function createShannonHelperServer(targetDir: string): ReturnType; /** - * save_deliverable tool implementation + * Create save_deliverable handler with targetDir captured in closure + * + * This factory pattern ensures each MCP server instance has its own targetDir, + * preventing race conditions when multiple workflows run in parallel. */ -export async function saveDeliverable(args: SaveDeliverableInput): Promise { - try { - const { deliverable_type, content } = args; +function createSaveDeliverableHandler(targetDir: string) { + return async function saveDeliverable(args: SaveDeliverableInput): Promise { + try { + const { deliverable_type, content } = args; - // Validate queue JSON if applicable - if (isQueueType(deliverable_type)) { - const queueValidation = validateQueueJson(content); - if (!queueValidation.valid) { - const errorResponse = createValidationError( - queueValidation.message ?? 'Invalid queue JSON', - true, - { - deliverableType: deliverable_type, - expectedFormat: '{"vulnerabilities": [...]}', - } - ); - return createToolResult(errorResponse); + // Validate queue JSON if applicable + if (isQueueType(deliverable_type)) { + const queueValidation = validateQueueJson(content); + if (!queueValidation.valid) { + const errorResponse = createValidationError( + queueValidation.message ?? 'Invalid queue JSON', + true, + { + deliverableType: deliverable_type, + expectedFormat: '{"vulnerabilities": [...]}', + } + ); + return createToolResult(errorResponse); + } } - } - // Get filename and save file - const filename = DELIVERABLE_FILENAMES[deliverable_type]; - const filepath = saveDeliverableFile(filename, content); + // Get filename and save file (targetDir captured from closure) + const filename = DELIVERABLE_FILENAMES[deliverable_type]; + const filepath = saveDeliverableFile(targetDir, filename, content); - // Success response - const successResponse: SaveDeliverableResponse = { - status: 'success', - message: `Deliverable saved successfully: ${filename}`, - filepath, - deliverableType: deliverable_type, - validated: isQueueType(deliverable_type), - }; + // Success response + const successResponse: SaveDeliverableResponse = { + status: 'success', + message: `Deliverable saved successfully: ${filename}`, + filepath, + deliverableType: deliverable_type, + validated: isQueueType(deliverable_type), + }; - return createToolResult(successResponse); - } catch (error) { - const errorResponse = createGenericError( - error, - false, - { deliverableType: args.deliverable_type } - ); + return createToolResult(successResponse); + } catch (error) { + const errorResponse = createGenericError( + error, + false, + { deliverableType: args.deliverable_type } + ); - return createToolResult(errorResponse); - } + return createToolResult(errorResponse); + } + }; } /** - * Tool definition for MCP server - created using SDK's tool() function + * Factory function to create save_deliverable tool with targetDir in closure + * + * Each MCP server instance should call this with its own targetDir to ensure + * deliverables are saved to the correct workflow's directory. */ -export const saveDeliverableTool = tool( - 'save_deliverable', - 'Saves deliverable files with automatic validation. Queue files must have {"vulnerabilities": [...]} structure.', - SaveDeliverableInputSchema.shape, - saveDeliverable -); +export function createSaveDeliverableTool(targetDir: string) { + return tool( + 'save_deliverable', + 'Saves deliverable files with automatic validation. Queue files must have {"vulnerabilities": [...]} structure.', + SaveDeliverableInputSchema.shape, + createSaveDeliverableHandler(targetDir) + ); +} diff --git a/mcp-server/src/utils/file-operations.ts b/mcp-server/src/utils/file-operations.ts index a10e4388..8f718b15 100644 --- a/mcp-server/src/utils/file-operations.ts +++ b/mcp-server/src/utils/file-operations.ts @@ -14,16 +14,14 @@ import { writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; -declare global { - var __SHANNON_TARGET_DIR: string | undefined; -} - /** * Save deliverable file to deliverables/ directory + * + * @param targetDir - Target directory for deliverables (passed explicitly to avoid race conditions) + * @param filename - Name of the deliverable file + * @param content - File content to save */ -export function saveDeliverableFile(filename: string, content: string): string { - // Use target directory from global context (set by createShannonHelperServer) - const targetDir = global.__SHANNON_TARGET_DIR || process.cwd(); +export function saveDeliverableFile(targetDir: string, filename: string, content: string): string { const deliverablesDir = join(targetDir, 'deliverables'); const filepath = join(deliverablesDir, filename); diff --git a/shannon b/shannon index 651d94ec..920ee09c 100755 --- a/shannon +++ b/shannon @@ -52,10 +52,48 @@ parse_args() { ID=*) ID="${arg#ID=}" ;; CLEAN=*) CLEAN="${arg#CLEAN=}" ;; PIPELINE_TESTING=*) PIPELINE_TESTING="${arg#PIPELINE_TESTING=}" ;; + REBUILD=*) REBUILD="${arg#REBUILD=}" ;; esac done } +# Check if Temporal is running and healthy +is_temporal_ready() { + docker compose -f "$COMPOSE_FILE" exec -T temporal \ + temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" +} + +# Ensure containers are running +ensure_containers() { + # Quick check: if Temporal is already healthy, we're good + if is_temporal_ready; then + return 0 + fi + + # Need to start containers + echo "Starting Shannon containers..." + if [ "$REBUILD" = "true" ]; then + # Force rebuild without cache (use when code changes aren't being picked up) + echo "Rebuilding with --no-cache..." + docker compose -f "$COMPOSE_FILE" build --no-cache worker + fi + docker compose -f "$COMPOSE_FILE" up -d --build + + # Wait for Temporal to be ready + echo "Waiting for Temporal to be ready..." + for i in $(seq 1 30); do + if is_temporal_ready; then + echo "Temporal is ready!" + return 0 + fi + if [ "$i" -eq 30 ]; then + echo "Timeout waiting for Temporal" + exit 1 + fi + sleep 2 + done +} + cmd_start() { parse_args "$@" @@ -72,22 +110,22 @@ cmd_start() { exit 1 fi - # Start containers - TARGET_REPO="$REPO" docker compose -f "$COMPOSE_FILE" up -d --build - - # Wait for Temporal to be ready - echo "Waiting for Temporal to be ready..." - for i in $(seq 1 30); do - if docker compose -f "$COMPOSE_FILE" exec -T temporal \ - temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING"; then - break - fi - if [ "$i" -eq 30 ]; then - echo "Timeout waiting for Temporal" - exit 1 - fi - sleep 2 - done + # Determine container path for REPO + # - If REPO is already a container path (/benchmarks/*, /target-repo), use as-is + # - Otherwise, it's a host path - mount to /target-repo and use that + case "$REPO" in + /benchmarks/*|/target-repo|/target-repo/*) + CONTAINER_REPO="$REPO" + ;; + *) + # Host path - export for docker-compose mount + export TARGET_REPO="$REPO" + CONTAINER_REPO="/target-repo" + ;; + esac + + # Ensure containers are running (starts them if needed) + ensure_containers # Build optional args ARGS="" @@ -95,9 +133,9 @@ cmd_start() { [ -n "$OUTPUT" ] && ARGS="$ARGS --output $OUTPUT" [ "$PIPELINE_TESTING" = "true" ] && ARGS="$ARGS --pipeline-testing" - # Run the client + # Run the client to submit workflow docker compose -f "$COMPOSE_FILE" exec -T worker \ - node dist/temporal/client.js "$URL" "/target-repo" $ARGS + node dist/temporal/client.js "$URL" "$CONTAINER_REPO" $ARGS } cmd_logs() { diff --git a/src/error-handling.ts b/src/error-handling.ts index 2b837baa..f43d4820 100644 --- a/src/error-handling.ts +++ b/src/error-handling.ts @@ -247,8 +247,18 @@ export function classifyErrorForTemporal(error: unknown): TemporalErrorClassific return { type: 'PermissionError', retryable: false }; } + // === OUTPUT VALIDATION ERRORS (Retryable) === + // Agent didn't produce expected deliverables - retry may succeed + // IMPORTANT: Must come BEFORE generic 'validation' check below + if ( + message.includes('failed output validation') || + message.includes('output validation failed') + ) { + return { type: 'OutputValidationError', retryable: true }; + } + // Invalid Request (400) - malformed request is permanent - // Note: Checked AFTER billing since Anthropic billing is 400 + // Note: Checked AFTER billing and AFTER output validation if ( message.includes('invalid_request_error') || message.includes('malformed') || diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index c2b2dc70..40f9d3be 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -25,6 +25,10 @@ import chalk from 'chalk'; const MAX_ERROR_MESSAGE_LENGTH = 2000; const MAX_STACK_TRACE_LENGTH = 1000; +// Max retries for output validation errors (agent didn't save deliverables) +// Lower than default 50 since this is unlikely to self-heal +const MAX_OUTPUT_VALIDATION_RETRIES = 3; + /** * Truncate error message to prevent buffer overflow in Temporal serialization. */ @@ -193,6 +197,16 @@ async function runAgentActivity( success: false, error: 'Output validation failed', }); + + // Limit output validation retries (unlikely to self-heal) + if (attemptNumber >= MAX_OUTPUT_VALIDATION_RETRIES) { + throw ApplicationFailure.nonRetryable( + `Agent ${agentName} failed output validation after ${attemptNumber} attempts`, + 'OutputValidationError', + [{ agentName, attemptNumber, elapsed: Date.now() - startTime }] + ); + } + // Let Temporal retry (will be classified as OutputValidationError) throw new Error(`Agent ${agentName} failed output validation`); } @@ -224,6 +238,12 @@ async function runAgentActivity( console.error(`Failed to rollback git workspace for ${agentName}:`, rollbackErr); } + // If error is already an ApplicationFailure (e.g., from our retry limit logic), + // re-throw it directly without re-classifying + if (error instanceof ApplicationFailure) { + throw error; + } + // Classify error for Temporal retry behavior const classified = classifyErrorForTemporal(error); // Truncate message to prevent protobuf buffer overflow diff --git a/src/temporal/worker.ts b/src/temporal/worker.ts index 73462576..81c7f7ed 100644 --- a/src/temporal/worker.ts +++ b/src/temporal/worker.ts @@ -9,7 +9,7 @@ * Temporal worker for Shannon pentest pipeline. * * Polls the 'shannon-pipeline' task queue and executes activities. - * Handles up to 5 concurrent activities to support parallel agent execution. + * Handles up to 25 concurrent activities to support multiple parallel workflows. * * Usage: * npm run temporal:worker @@ -49,7 +49,7 @@ async function runWorker(): Promise { workflowBundle, activities, taskQueue: 'shannon-pipeline', - maxConcurrentActivityTaskExecutions: 5, // Match parallel agent count + maxConcurrentActivityTaskExecutions: 25, // Support multiple parallel workflows (5 agents × ~5 workflows) }); // Graceful shutdown handling diff --git a/src/temporal/workflows.ts b/src/temporal/workflows.ts index 700dc577..078b5d12 100644 --- a/src/temporal/workflows.ts +++ b/src/temporal/workflows.ts @@ -35,25 +35,44 @@ import { type PipelineProgress, } from './shared.js'; -// Activity proxy with retry configuration +// Retry configuration for production (long intervals for billing recovery) +const PRODUCTION_RETRY = { + initialInterval: '5 minutes', + maximumInterval: '30 minutes', + backoffCoefficient: 2, + maximumAttempts: 50, + nonRetryableErrorTypes: [ + 'AuthenticationError', + 'PermissionError', + 'InvalidRequestError', + 'RequestTooLargeError', + 'ConfigurationError', + 'InvalidTargetError', + 'ExecutionLimitError', + ], +}; + +// Retry configuration for pipeline testing (fast iteration) +const TESTING_RETRY = { + initialInterval: '10 seconds', + maximumInterval: '30 seconds', + backoffCoefficient: 2, + maximumAttempts: 5, + nonRetryableErrorTypes: PRODUCTION_RETRY.nonRetryableErrorTypes, +}; + +// Activity proxy with production retry configuration (default) const acts = proxyActivities({ startToCloseTimeout: '2 hours', heartbeatTimeout: '30 seconds', - retry: { - initialInterval: '5 minutes', - maximumInterval: '30 minutes', - backoffCoefficient: 2, - maximumAttempts: 50, - nonRetryableErrorTypes: [ - 'AuthenticationError', - 'PermissionError', - 'InvalidRequestError', - 'RequestTooLargeError', - 'ConfigurationError', - 'InvalidTargetError', - 'ExecutionLimitError', - ], - }, + retry: PRODUCTION_RETRY, +}); + +// Activity proxy with testing retry configuration (fast) +const testActs = proxyActivities({ + startToCloseTimeout: '10 minutes', + heartbeatTimeout: '30 seconds', + retry: TESTING_RETRY, }); export async function pentestPipelineWorkflow( @@ -61,6 +80,10 @@ export async function pentestPipelineWorkflow( ): Promise { const { workflowId } = workflowInfo(); + // Select activity proxy based on testing mode + // Pipeline testing uses fast retry intervals (10s) for quick iteration + const a = input.pipelineTestingMode ? testActs : acts; + // Workflow state (queryable) const state: PipelineState = { status: 'running', @@ -99,13 +122,13 @@ export async function pentestPipelineWorkflow( state.currentPhase = 'pre-recon'; state.currentAgent = 'pre-recon'; state.agentMetrics['pre-recon'] = - await acts.runPreReconAgent(activityInput); + await a.runPreReconAgent(activityInput); state.completedAgents.push('pre-recon'); // === Phase 2: Reconnaissance === state.currentPhase = 'recon'; state.currentAgent = 'recon'; - state.agentMetrics['recon'] = await acts.runReconAgent(activityInput); + state.agentMetrics['recon'] = await a.runReconAgent(activityInput); state.completedAgents.push('recon'); // === Phase 3: Vulnerability Analysis (Parallel) === @@ -113,11 +136,11 @@ export async function pentestPipelineWorkflow( state.currentAgent = 'vuln-agents'; const vulnResults = await Promise.all([ - acts.runInjectionVulnAgent(activityInput), - acts.runXssVulnAgent(activityInput), - acts.runAuthVulnAgent(activityInput), - acts.runSsrfVulnAgent(activityInput), - acts.runAuthzVulnAgent(activityInput), + a.runInjectionVulnAgent(activityInput), + a.runXssVulnAgent(activityInput), + a.runAuthVulnAgent(activityInput), + a.runSsrfVulnAgent(activityInput), + a.runAuthzVulnAgent(activityInput), ]); const vulnAgents = [ @@ -141,11 +164,11 @@ export async function pentestPipelineWorkflow( state.currentAgent = 'exploit-agents'; const exploitResults = await Promise.all([ - acts.runInjectionExploitAgent(activityInput), - acts.runXssExploitAgent(activityInput), - acts.runAuthExploitAgent(activityInput), - acts.runSsrfExploitAgent(activityInput), - acts.runAuthzExploitAgent(activityInput), + a.runInjectionExploitAgent(activityInput), + a.runXssExploitAgent(activityInput), + a.runAuthExploitAgent(activityInput), + a.runSsrfExploitAgent(activityInput), + a.runAuthzExploitAgent(activityInput), ]); const exploitAgents = [ @@ -169,10 +192,10 @@ export async function pentestPipelineWorkflow( state.currentAgent = 'report'; // First, assemble the concatenated report from exploitation evidence files - await acts.assembleReportActivity(activityInput); + await a.assembleReportActivity(activityInput); // Then run the report agent to add executive summary and clean up - state.agentMetrics['report'] = await acts.runReportAgent(activityInput); + state.agentMetrics['report'] = await a.runReportAgent(activityInput); state.completedAgents.push('report'); // === Complete === From eaff84b8475d747360f6bc1471036b33f43d3375 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 13:08:12 -0800 Subject: [PATCH 19/24] =?UTF-8?q?refactor:=20pipeline=20vuln=E2=86=92explo?= =?UTF-8?q?it=20workflow=20for=20parallel=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace sync barrier between vuln/exploit phases with independent pipelines - Each vuln type runs: vuln agent → queue check → conditional exploit - Add checkExploitationQueue activity to skip exploits when no vulns found - Use Promise.allSettled for graceful failure handling across pipelines - Add PipelineSummary type for aggregated cost/duration/turns metrics --- src/temporal/activities.ts | 40 +++++++++ src/temporal/client.ts | 17 ++-- src/temporal/shared.ts | 20 +++++ src/temporal/workflows.ts | 172 ++++++++++++++++++++++++++----------- 4 files changed, 188 insertions(+), 61 deletions(-) diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index 40f9d3be..abe763d3 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -56,6 +56,11 @@ import { import { loadPrompt } from '../prompts/prompt-manager.js'; import { parseConfig, distributeConfig } from '../config-parser.js'; import { classifyErrorForTemporal } from '../error-handling.js'; +import { + validateQueueAndDeliverable, + type VulnType, + type ExploitationDecision, +} from '../queue-validation.js'; import { createGitCheckpoint, commitGitSuccess, @@ -342,3 +347,38 @@ export async function assembleReportActivity(input: ActivityInput): Promise { + const { repoPath } = input; + + try { + const decision = await validateQueueAndDeliverable(vulnType, repoPath); + console.log( + chalk.blue( + `🔍 ${vulnType}: ${decision.shouldExploit ? `${decision.vulnerabilityCount} vulnerabilities found` : 'no vulnerabilities, skipping exploitation'}` + ) + ); + return decision; + } catch (error) { + // If validation fails (missing files, invalid JSON), log and skip exploitation + // This is safer than crashing - the vuln agent likely failed or found nothing + const errMsg = error instanceof Error ? error.message : String(error); + console.log(chalk.yellow(`âš ī¸ ${vulnType}: Queue validation failed (${errMsg}), skipping exploitation`)); + return { + shouldExploit: false, + shouldRetry: false, + vulnerabilityCount: 0, + vulnType, + }; + } +} diff --git a/src/temporal/client.ts b/src/temporal/client.ts index 73936a9b..3d402f39 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -190,18 +190,11 @@ async function startPipeline(): Promise { clearInterval(progressInterval); console.log(chalk.green.bold('\nPipeline completed successfully!')); - console.log( - chalk.gray(`Duration: ${Math.floor((Date.now() - result.startTime) / 1000)}s`) - ); - console.log(chalk.gray(`Agents completed: ${result.completedAgents.length}`)); - - // Show cost summary if available - const totalCost = Object.values(result.agentMetrics).reduce( - (sum, m) => sum + (m.costUsd ?? 0), - 0 - ); - if (totalCost > 0) { - console.log(chalk.gray(`Total cost: $${totalCost.toFixed(4)}`)); + if (result.summary) { + console.log(chalk.gray(`Duration: ${Math.floor(result.summary.totalDurationMs / 1000)}s`)); + console.log(chalk.gray(`Agents completed: ${result.summary.agentCount}`)); + console.log(chalk.gray(`Total turns: ${result.summary.totalTurns}`)); + console.log(chalk.gray(`Total cost: $${result.summary.totalCostUsd.toFixed(4)}`)); } } catch (error) { clearInterval(progressInterval); diff --git a/src/temporal/shared.ts b/src/temporal/shared.ts index 578b3a35..e10ad330 100644 --- a/src/temporal/shared.ts +++ b/src/temporal/shared.ts @@ -19,6 +19,13 @@ export interface AgentMetrics { numTurns: number | null; } +export interface PipelineSummary { + totalCostUsd: number; + totalDurationMs: number; // Wall-clock time (end - start) + totalTurns: number; + agentCount: number; +} + export interface PipelineState { status: 'running' | 'completed' | 'failed'; currentPhase: string | null; @@ -28,6 +35,7 @@ export interface PipelineState { error: string | null; startTime: number; agentMetrics: Record; + summary: PipelineSummary | null; } // Extended state returned by getProgress query (includes computed fields) @@ -36,6 +44,18 @@ export interface PipelineProgress extends PipelineState { elapsedMs: number; } +// Result from a single vuln→exploit pipeline +export interface VulnExploitPipelineResult { + vulnType: string; + vulnMetrics: AgentMetrics | null; + exploitMetrics: AgentMetrics | null; + exploitDecision: { + shouldExploit: boolean; + vulnerabilityCount: number; + } | null; + error: string | null; +} + // === Queries === export const getProgress = defineQuery('getProgress'); diff --git a/src/temporal/workflows.ts b/src/temporal/workflows.ts index 078b5d12..0b5e384e 100644 --- a/src/temporal/workflows.ts +++ b/src/temporal/workflows.ts @@ -7,11 +7,12 @@ /** * Temporal workflow for Shannon pentest pipeline. * - * Orchestrates the 5-phase penetration testing workflow: + * Orchestrates the penetration testing workflow: * 1. Pre-Reconnaissance (sequential) * 2. Reconnaissance (sequential) - * 3. Vulnerability Analysis (parallel - 5 agents) - * 4. Exploitation (parallel - 5 agents) + * 3-4. Vulnerability + Exploitation (5 pipelined pairs in parallel) + * Each pair: vuln agent → queue check → conditional exploit + * No synchronization barrier - exploits start when their vuln finishes * 5. Reporting (sequential) * * Features: @@ -19,6 +20,7 @@ * - Automatic retry with backoff for transient/billing errors * - Non-retryable classification for permanent errors * - Audit correlation via workflowId + * - Graceful failure handling: pipelines continue if one fails */ import { @@ -33,7 +35,11 @@ import { type PipelineInput, type PipelineState, type PipelineProgress, + type PipelineSummary, + type VulnExploitPipelineResult, + type AgentMetrics, } from './shared.js'; +import type { VulnType } from '../queue-validation.js'; // Retry configuration for production (long intervals for billing recovery) const PRODUCTION_RETRY = { @@ -75,6 +81,20 @@ const testActs = proxyActivities({ retry: TESTING_RETRY, }); +/** + * Compute aggregated metrics from the current pipeline state. + * Called on both success and failure to provide partial metrics. + */ +function computeSummary(state: PipelineState): PipelineSummary { + const metrics = Object.values(state.agentMetrics); + return { + totalCostUsd: metrics.reduce((sum, m) => sum + (m.costUsd ?? 0), 0), + totalDurationMs: Date.now() - state.startTime, + totalTurns: metrics.reduce((sum, m) => sum + (m.numTurns ?? 0), 0), + agentCount: state.completedAgents.length, + }; +} + export async function pentestPipelineWorkflow( input: PipelineInput ): Promise { @@ -94,6 +114,7 @@ export async function pentestPipelineWorkflow( error: null, startTime: Date.now(), agentMetrics: {}, + summary: null, }; // Register query handler for real-time progress inspection @@ -131,62 +152,113 @@ export async function pentestPipelineWorkflow( state.agentMetrics['recon'] = await a.runReconAgent(activityInput); state.completedAgents.push('recon'); - // === Phase 3: Vulnerability Analysis (Parallel) === - state.currentPhase = 'vulnerability-analysis'; - state.currentAgent = 'vuln-agents'; + // === Phases 3-4: Vulnerability Analysis + Exploitation (Pipelined) === + // Each vuln type runs as an independent pipeline: + // vuln agent → queue check → conditional exploit agent + // This eliminates the synchronization barrier between phases - each exploit + // starts immediately when its vuln agent finishes, not waiting for all. + state.currentPhase = 'vulnerability-exploitation'; + state.currentAgent = 'pipelines'; - const vulnResults = await Promise.all([ - a.runInjectionVulnAgent(activityInput), - a.runXssVulnAgent(activityInput), - a.runAuthVulnAgent(activityInput), - a.runSsrfVulnAgent(activityInput), - a.runAuthzVulnAgent(activityInput), - ]); + // Helper: Run a single vuln→exploit pipeline + async function runVulnExploitPipeline( + vulnType: VulnType, + runVulnAgent: () => Promise, + runExploitAgent: () => Promise + ): Promise { + // Step 1: Run vulnerability agent + const vulnMetrics = await runVulnAgent(); - const vulnAgents = [ - 'injection-vuln', - 'xss-vuln', - 'auth-vuln', - 'ssrf-vuln', - 'authz-vuln', - ] as const; - for (let i = 0; i < vulnAgents.length; i++) { - const agentName = vulnAgents[i]; - const metrics = vulnResults[i]; - if (agentName && metrics) { - state.agentMetrics[agentName] = metrics; - state.completedAgents.push(agentName); + // Step 2: Check exploitation queue (starts immediately after vuln) + const decision = await a.checkExploitationQueue(activityInput, vulnType); + + // Step 3: Conditionally run exploit agent + let exploitMetrics: AgentMetrics | null = null; + if (decision.shouldExploit) { + exploitMetrics = await runExploitAgent(); } + + return { + vulnType, + vulnMetrics, + exploitMetrics, + exploitDecision: { + shouldExploit: decision.shouldExploit, + vulnerabilityCount: decision.vulnerabilityCount, + }, + error: null, + }; } - // === Phase 4: Exploitation (Parallel) === - state.currentPhase = 'exploitation'; - state.currentAgent = 'exploit-agents'; - - const exploitResults = await Promise.all([ - a.runInjectionExploitAgent(activityInput), - a.runXssExploitAgent(activityInput), - a.runAuthExploitAgent(activityInput), - a.runSsrfExploitAgent(activityInput), - a.runAuthzExploitAgent(activityInput), + // Run all 5 pipelines in parallel with graceful failure handling + // Promise.allSettled ensures other pipelines continue if one fails + const pipelineResults = await Promise.allSettled([ + runVulnExploitPipeline( + 'injection', + () => a.runInjectionVulnAgent(activityInput), + () => a.runInjectionExploitAgent(activityInput) + ), + runVulnExploitPipeline( + 'xss', + () => a.runXssVulnAgent(activityInput), + () => a.runXssExploitAgent(activityInput) + ), + runVulnExploitPipeline( + 'auth', + () => a.runAuthVulnAgent(activityInput), + () => a.runAuthExploitAgent(activityInput) + ), + runVulnExploitPipeline( + 'ssrf', + () => a.runSsrfVulnAgent(activityInput), + () => a.runSsrfExploitAgent(activityInput) + ), + runVulnExploitPipeline( + 'authz', + () => a.runAuthzVulnAgent(activityInput), + () => a.runAuthzExploitAgent(activityInput) + ), ]); - const exploitAgents = [ - 'injection-exploit', - 'xss-exploit', - 'auth-exploit', - 'ssrf-exploit', - 'authz-exploit', - ] as const; - for (let i = 0; i < exploitAgents.length; i++) { - const agentName = exploitAgents[i]; - const metrics = exploitResults[i]; - if (agentName && metrics) { - state.agentMetrics[agentName] = metrics; - state.completedAgents.push(agentName); + // Aggregate results from all pipelines + const failedPipelines: string[] = []; + for (const result of pipelineResults) { + if (result.status === 'fulfilled') { + const { vulnType, vulnMetrics, exploitMetrics } = result.value; + + // Record vuln agent metrics + if (vulnMetrics) { + state.agentMetrics[`${vulnType}-vuln`] = vulnMetrics; + state.completedAgents.push(`${vulnType}-vuln`); + } + + // Record exploit agent metrics (if it ran) + if (exploitMetrics) { + state.agentMetrics[`${vulnType}-exploit`] = exploitMetrics; + state.completedAgents.push(`${vulnType}-exploit`); + } + } else { + // Pipeline failed - log error but continue with others + const errorMsg = + result.reason instanceof Error + ? result.reason.message + : String(result.reason); + failedPipelines.push(errorMsg); } } + // Log any pipeline failures (workflow continues despite failures) + if (failedPipelines.length > 0) { + console.log( + `âš ī¸ ${failedPipelines.length} pipeline(s) failed:`, + failedPipelines + ); + } + + // Update phase markers + state.currentPhase = 'exploitation'; + state.currentAgent = null; + // === Phase 5: Reporting === state.currentPhase = 'reporting'; state.currentAgent = 'report'; @@ -202,11 +274,13 @@ export async function pentestPipelineWorkflow( state.status = 'completed'; state.currentPhase = null; state.currentAgent = null; + state.summary = computeSummary(state); return state; } catch (error) { state.status = 'failed'; state.failedAgent = state.currentAgent; state.error = error instanceof Error ? error.message : String(error); + state.summary = computeSummary(state); throw error; } } From 3b391ec54cfc08471332b0f739e339b9d3a494ad Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 13:13:27 -0800 Subject: [PATCH 20/24] fix: re-throw retryable errors in checkExploitationQueue --- src/temporal/activities.ts | 46 +++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index abe763d3..edbe8f76 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -57,7 +57,7 @@ import { loadPrompt } from '../prompts/prompt-manager.js'; import { parseConfig, distributeConfig } from '../config-parser.js'; import { classifyErrorForTemporal } from '../error-handling.js'; import { - validateQueueAndDeliverable, + safeValidateQueueAndDeliverable, type VulnType, type ExploitationDecision, } from '../queue-validation.js'; @@ -354,6 +354,10 @@ export async function assembleReportActivity(input: ActivityInput): Promise { const { repoPath } = input; - try { - const decision = await validateQueueAndDeliverable(vulnType, repoPath); + const result = await safeValidateQueueAndDeliverable(vulnType, repoPath); + + if (result.success && result.data) { + const { shouldExploit, vulnerabilityCount } = result.data; console.log( chalk.blue( - `🔍 ${vulnType}: ${decision.shouldExploit ? `${decision.vulnerabilityCount} vulnerabilities found` : 'no vulnerabilities, skipping exploitation'}` + `🔍 ${vulnType}: ${shouldExploit ? `${vulnerabilityCount} vulnerabilities found` : 'no vulnerabilities, skipping exploitation'}` ) ); - return decision; - } catch (error) { - // If validation fails (missing files, invalid JSON), log and skip exploitation - // This is safer than crashing - the vuln agent likely failed or found nothing - const errMsg = error instanceof Error ? error.message : String(error); - console.log(chalk.yellow(`âš ī¸ ${vulnType}: Queue validation failed (${errMsg}), skipping exploitation`)); - return { - shouldExploit: false, - shouldRetry: false, - vulnerabilityCount: 0, - vulnType, - }; + return result.data; + } + + // Validation failed - check if we should retry or skip + const error = result.error; + if (error?.retryable) { + // Re-throw retryable errors so Temporal can retry the vuln agent + console.log(chalk.yellow(`âš ī¸ ${vulnType}: ${error.message} (retrying)`)); + throw error; } + + // Non-retryable error - skip exploitation gracefully + console.log( + chalk.yellow(`âš ī¸ ${vulnType}: ${error?.message ?? 'Unknown error'}, skipping exploitation`) + ); + return { + shouldExploit: false, + shouldRetry: false, + vulnerabilityCount: 0, + vulnType, + }; } From eb8ab3be865e32477cdcf7daa5b82c093f9fa6de Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 17:51:51 -0800 Subject: [PATCH 21/24] feat: add PostHog telemetry with persistent installation tracking - Add telemetry module with PostHog integration and opt-out support - Track workflow/agent lifecycle events (start, complete, fail, retry) - Persist anonymous installation ID to ~/.shannon/telemetry-id - Include hashed target hostname for unique target counting - Mount host ~/.shannon in container for ID persistence across rebuilds --- README.md | 37 +++++ docker-compose.yml | 1 + package-lock.json | 90 ++++++++++- package.json | 1 + src/telemetry/index.ts | 26 +++ src/telemetry/installation-id.ts | 78 +++++++++ src/telemetry/telemetry-config.ts | 68 ++++++++ src/telemetry/telemetry-events.ts | 60 +++++++ src/telemetry/telemetry-manager.ts | 246 +++++++++++++++++++++++++++++ src/temporal/activities.ts | 91 ++++++++++- src/temporal/client.ts | 18 +++ src/temporal/shared.ts | 1 + src/temporal/worker.ts | 6 + src/temporal/workflows.ts | 15 +- 14 files changed, 733 insertions(+), 5 deletions(-) create mode 100644 src/telemetry/index.ts create mode 100644 src/telemetry/installation-id.ts create mode 100644 src/telemetry/telemetry-config.ts create mode 100644 src/telemetry/telemetry-events.ts create mode 100644 src/telemetry/telemetry-manager.ts diff --git a/README.md b/README.md index 51b9916d..3a12d59e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Shannon is available in two editions: - [Architecture](#-architecture) - [Coverage and Roadmap](#-coverage-and-roadmap) - [Disclaimers](#-disclaimers) +- [Telemetry](#-telemetry) - [License](#-license) - [Community & Support](#-community--support) - [Get in Touch](#-get-in-touch) @@ -437,6 +438,42 @@ Shannon is designed for legitimate security auditing purposes only. Windows Defender may flag files in `xben-benchmark-results/` or `deliverables/` as malware. These are false positives caused by exploit code in the reports. Add an exclusion for the Shannon directory in Windows Defender, or use Docker/WSL2. +## 📊 Telemetry + +Shannon collects anonymous usage telemetry to help improve the tool. + +### What We Collect + +- Workflow and agent lifecycle events (start, complete, fail) +- Timing and cost metrics (duration, API costs) +- Error types (NOT error messages or stack traces) + +### What We DO NOT Collect + +- Target URLs, repository paths, or configuration +- Vulnerability findings or security reports +- Error messages, stack traces, or debugging info +- Any personally identifiable information (PII) + +### Opting Out + +Telemetry is enabled by default. To disable it, set one of: + +```bash +# Standard opt-out +export DO_NOT_TRACK=1 + +# Shannon-specific opt-out +export SHANNON_TELEMETRY=off +``` + +Or add to your `.env` file: + +```env +DO_NOT_TRACK=1 +``` + + ## 📜 License Shannon Lite is released under the [GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml index 7d509e23..37ee1ce6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: - ./prompts:/app/prompts - ${TARGET_REPO:-.}:/target-repo - ${BENCHMARKS_BASE:-.}:/benchmarks + - ${HOME}/.shannon:/tmp/.shannon shm_size: 2gb ipc: host security_opt: diff --git a/package-lock.json b/package-lock.json index 63e6d715..70cafd63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,10 @@ "figlet": "^1.9.3", "gradient-string": "^3.0.0", "js-yaml": "^4.1.0", + "posthog-node": "^5.20.0", "zod": "^3.22.4", "zx": "^8.0.0" }, - "bin": { - "shannon": "dist/shannon.js" - }, "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^25.0.3", @@ -462,6 +460,15 @@ "tslib": "2" } }, + "node_modules/@posthog/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.9.1.tgz", + "integrity": "sha512-kRb1ch2dhQjsAapZmu6V66551IF2LnCbc1rnrQqnR7ArooVyJN9KOPXre16AJ3ObJz2eTfuP7x25BMyS2Y5Exw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1501,6 +1508,20 @@ "node": ">=20" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1767,6 +1788,12 @@ "node": ">=8" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -1905,12 +1932,33 @@ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/posthog-node": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.20.0.tgz", + "integrity": "sha512-LkR5KfrvEQTnUtNKN97VxFB00KcYG1Iz8iKg8r0e/i7f1eQhg1WSZO+Jp1B4bvtHCmdpIE4HwYbvCCzFoCyjVg==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.9.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -2037,6 +2085,27 @@ "randombytes": "^2.1.0" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -2434,6 +2503,21 @@ "node": ">=10.13.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", diff --git a/package.json b/package.json index 0d3cb26a..c37c03ef 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "figlet": "^1.9.3", "gradient-string": "^3.0.0", "js-yaml": "^4.1.0", + "posthog-node": "^5.20.0", "zod": "^3.22.4", "zx": "^8.0.0" }, diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts new file mode 100644 index 00000000..0f186c21 --- /dev/null +++ b/src/telemetry/index.ts @@ -0,0 +1,26 @@ +// 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. + +/** + * Telemetry Module - Public API + * + * Usage: + * import { telemetry, TelemetryEvent } from '../telemetry/index.js'; + * + * telemetry.initialize(); + * telemetry.track(TelemetryEvent.WORKFLOW_START, { has_config: true }); + * await telemetry.shutdown(); + */ + +export { telemetry, hashTargetUrl } from './telemetry-manager.js'; +export { TelemetryEvent } from './telemetry-events.js'; +export { getInstallationId } from './installation-id.js'; +export type { + BaseTelemetryProperties, + AgentEventProperties, + WorkflowEventProperties, +} from './telemetry-events.js'; +export { loadTelemetryConfig } from './telemetry-config.js'; diff --git a/src/telemetry/installation-id.ts b/src/telemetry/installation-id.ts new file mode 100644 index 00000000..b9c56041 --- /dev/null +++ b/src/telemetry/installation-id.ts @@ -0,0 +1,78 @@ +// 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. + +/** + * Installation ID - Persistent anonymous identifier for telemetry. + * + * Generates a UUID and persists it to ~/.shannon/telemetry-id + * On subsequent runs, reads the existing ID from the file. + * Handles errors gracefully by returning a random UUID. + */ + +import { randomUUID } from 'crypto'; +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { homedir } from 'os'; + +const SHANNON_DIR = '.shannon'; +const TELEMETRY_ID_FILE = 'telemetry-id'; + +/** + * Get the path to the telemetry ID file. + * Returns ~/.shannon/telemetry-id + */ +function getTelemetryIdPath(): string { + return join(homedir(), SHANNON_DIR, TELEMETRY_ID_FILE); +} + +/** + * Get the path to the Shannon config directory. + * Returns ~/.shannon + */ +function getShannonDir(): string { + return join(homedir(), SHANNON_DIR); +} + +/** + * Get or create a persistent installation ID. + * + * - If ~/.shannon/telemetry-id exists, reads and returns the ID + * - If not, generates a new UUID, persists it, and returns it + * - On any error, returns a random UUID (doesn't persist) + * + * @returns Promise - The installation ID (UUID format) + */ +export async function getInstallationId(): Promise { + const filePath = getTelemetryIdPath(); + + try { + // Try to read existing ID + const existingId = await readFile(filePath, 'utf-8'); + const trimmedId = existingId.trim(); + + // Validate it looks like a UUID (basic check) + if (trimmedId.length >= 32) { + return trimmedId; + } + } catch { + // File doesn't exist or can't be read - will create new ID + } + + // Generate new ID + const newId = randomUUID(); + + try { + // Ensure ~/.shannon directory exists + await mkdir(getShannonDir(), { recursive: true }); + + // Persist the new ID + await writeFile(filePath, newId, 'utf-8'); + } catch { + // Failed to persist - return the ID anyway (won't be persistent) + } + + return newId; +} diff --git a/src/telemetry/telemetry-config.ts b/src/telemetry/telemetry-config.ts new file mode 100644 index 00000000..01df5626 --- /dev/null +++ b/src/telemetry/telemetry-config.ts @@ -0,0 +1,68 @@ +// 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. + +/** + * Telemetry configuration with opt-out support. + * + * Telemetry is enabled by default. Users can disable via: + * - DO_NOT_TRACK=1 (standard convention: https://consoledonottrack.com/) + * - SHANNON_TELEMETRY=off|false|0 + */ + +export interface TelemetryConfig { + enabled: boolean; + apiKey: string; + host: string; +} + +// PostHog project configuration +// This is a write-only key - safe to publish, users cannot read analytics +const POSTHOG_API_KEY = 'phc_9EF2G6mm83rfLef5WmVLiNSyGQ4x0p8NzTRKiEAgvD4'; +const POSTHOG_HOST = 'https://us.i.posthog.com'; + +/** + * Check if telemetry is enabled based on environment variables. + */ +function isTelemetryEnabled(): boolean { + // Standard opt-out: DO_NOT_TRACK + const doNotTrack = process.env.DO_NOT_TRACK; + if (doNotTrack === '1' || doNotTrack?.toLowerCase() === 'true') { + return false; + } + + // Shannon-specific opt-out + const shannonTelemetry = process.env.SHANNON_TELEMETRY?.toLowerCase(); + if ( + shannonTelemetry === 'off' || + shannonTelemetry === 'false' || + shannonTelemetry === '0' + ) { + return false; + } + + return true; +} + +/** + * Load telemetry configuration from environment. + * Never throws - returns disabled config on any error. + */ +export function loadTelemetryConfig(): TelemetryConfig { + try { + return { + enabled: isTelemetryEnabled(), + apiKey: POSTHOG_API_KEY, + host: POSTHOG_HOST, + }; + } catch { + // Config loading should never fail - return disabled + return { + enabled: false, + apiKey: POSTHOG_API_KEY, + host: POSTHOG_HOST, + }; + } +} diff --git a/src/telemetry/telemetry-events.ts b/src/telemetry/telemetry-events.ts new file mode 100644 index 00000000..32415244 --- /dev/null +++ b/src/telemetry/telemetry-events.ts @@ -0,0 +1,60 @@ +// 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. + +/** + * Telemetry event definitions for Shannon. + * + * All PostHog event names are defined here for consistency and type safety. + * These events are anonymous - no PII or sensitive data is ever sent. + */ + +/** + * Telemetry event names. + * Using an enum ensures consistency across the codebase. + */ +export enum TelemetryEvent { + // Workflow lifecycle (emitted from client.ts) + WORKFLOW_START = 'workflow_start', + + // Agent lifecycle (emitted from activities.ts) + AGENT_START = 'agent_start', + AGENT_COMPLETE = 'agent_complete', + AGENT_FAILED = 'agent_failed', + AGENT_RETRY = 'agent_retry', + + // Pipeline completion (emitted from report agent in activities.ts) + WORKFLOW_COMPLETE = 'workflow_complete', + WORKFLOW_FAILED = 'workflow_failed', +} + +/** + * Base properties included with every telemetry event. + */ +export interface BaseTelemetryProperties { + os_platform: string; + node_version: string; +} + +/** + * Properties for agent-level events. + */ +export interface AgentEventProperties { + agent_name: string; + attempt_number: number; + duration_ms?: number; + cost_usd?: number; + error_type?: string; // Only error classification, never the actual message +} + +/** + * Properties for workflow-level events. + */ +export interface WorkflowEventProperties { + has_config?: boolean; + total_duration_ms?: number; + total_cost_usd?: number; + error_type?: string; // Only error classification, never the actual message +} diff --git a/src/telemetry/telemetry-manager.ts b/src/telemetry/telemetry-manager.ts new file mode 100644 index 00000000..02e1bcc0 --- /dev/null +++ b/src/telemetry/telemetry-manager.ts @@ -0,0 +1,246 @@ +// 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. + +/** + * Telemetry Manager - PostHog integration with safety guarantees. + * + * CRITICAL: All public methods are wrapped in try-catch to ensure + * telemetry NEVER interferes with workflow execution. Failures are + * silently swallowed - telemetry is optional, not critical. + * + * Features: + * - Safe initialization (never throws) + * - Auto-redaction of sensitive data before sending + * - Fire-and-forget tracking (non-blocking) + * - Graceful shutdown with timeout (never blocks) + */ + +import { PostHog } from 'posthog-node'; +import crypto from 'crypto'; +import { loadTelemetryConfig, type TelemetryConfig } from './telemetry-config.js'; +import { TelemetryEvent, type BaseTelemetryProperties } from './telemetry-events.js'; + +// Shutdown timeout - don't block workflow completion +const SHUTDOWN_TIMEOUT_MS = 2000; + +// Sensitive keys to redact from properties (case-insensitive matching) +const SENSITIVE_KEYS = [ + 'weburl', + 'repopath', + 'configpath', + 'outputpath', + 'targeturl', + 'url', + 'path', + 'error', + 'message', + 'stack', + 'findings', + 'vulnerabilities', + 'credentials', + 'password', + 'secret', + 'token', + 'apikey', + 'key', +]; + +/** + * Generate anonymous distinct ID as a UUID. + */ +function generateDistinctId(): string { + return crypto.randomUUID(); +} + +/** + * Hash a URL's hostname using SHA-256. + * Returns a hex string hash of just the hostname portion. + * Returns undefined if URL is invalid. + */ +export function hashTargetUrl(url: string): string | undefined { + try { + const hostname = new URL(url).hostname; + return crypto.createHash('sha256').update(hostname).digest('hex'); + } catch { + return undefined; + } +} + +/** + * Check if a key name contains sensitive information. + */ +function isSensitiveKey(key: string): boolean { + const keyLower = key.toLowerCase(); + return SENSITIVE_KEYS.some((sensitive) => keyLower.includes(sensitive)); +} + +/** + * Redact sensitive values from properties object. + * Returns a new object with sensitive keys removed. + */ +function redactSensitiveData( + properties: Record +): Record { + const redacted: Record = {}; + + for (const [key, value] of Object.entries(properties)) { + // Skip sensitive keys entirely + if (isSensitiveKey(key)) { + continue; + } + + // Recursively redact nested objects + if (value && typeof value === 'object' && !Array.isArray(value)) { + redacted[key] = redactSensitiveData(value as Record); + } else if (typeof value === 'string') { + // Skip string values that look like paths or URLs + if ( + value.startsWith('/') || + value.startsWith('http') || + value.includes('://') + ) { + continue; + } + redacted[key] = value; + } else { + redacted[key] = value; + } + } + + return redacted; +} + +class TelemetryManager { + private client: PostHog | null = null; + private config: TelemetryConfig; + private distinctId: string; + private initialized = false; + private pipelineTestingMode = false; + + constructor() { + this.config = loadTelemetryConfig(); + this.distinctId = generateDistinctId(); + } + + + /** + * Set the distinct ID for all subsequent events. + * Call this with workflowId to ensure consistent ID across client/worker. + */ + setDistinctId(id: string): void { + this.distinctId = id; + } + + /** + * Initialize PostHog client. + * Safe: never throws, logs warning on failure. + * + * @param pipelineTestingMode - Whether running in testing mode + */ + initialize(pipelineTestingMode = false): void { + try { + if (this.initialized) { + return; + } + + this.pipelineTestingMode = pipelineTestingMode; + this.initialized = true; + + if (!this.config.enabled) { + return; + } + + // Don't initialize if API key isn't configured + if (this.config.apiKey.includes('REPLACE_WITH')) { + this.config.enabled = false; + return; + } + + this.client = new PostHog(this.config.apiKey, { + host: this.config.host, + disableGeoip: true, + flushAt: 10, + flushInterval: 5000, + }); + } catch { + // Initialization failure is silent - telemetry is optional + this.initialized = true; + this.config.enabled = false; + } + } + + /** + * Track an event with properties. + * Safe: never throws, silently fails on error. + * + * @param event - Event name from TelemetryEvent enum + * @param properties - Event properties (sensitive data auto-redacted) + */ + track(event: TelemetryEvent, properties: Record = {}): void { + try { + if (!this.config.enabled || !this.client) { + return; + } + + // Build base properties + const baseProps: BaseTelemetryProperties & Record = { + pipeline_testing_mode: this.pipelineTestingMode, + os_platform: process.platform, + node_version: process.version, + $lib: 'shannon', + }; + + // Redact sensitive data and merge with base props + const safeProps = { + ...baseProps, + ...redactSensitiveData(properties), + }; + + // Fire and forget - don't await + this.client.capture({ + distinctId: this.distinctId, + event, + properties: safeProps, + }); + } catch { + // Tracking failure is silent - never interfere with workflow + } + } + + /** + * Shutdown PostHog client gracefully. + * Safe: never throws, uses timeout to prevent blocking. + * + * @returns Promise that resolves when shutdown completes (or times out) + */ + async shutdown(): Promise { + try { + if (!this.client) { + return; + } + + // Race shutdown against timeout to never block workflow + await Promise.race([ + this.client.shutdown(), + new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)), + ]); + } catch { + // Shutdown failure is silent + } finally { + this.client = null; + } + } + + /** + * Check if telemetry is enabled. + */ + isEnabled(): boolean { + return this.config.enabled && this.client !== null; + } +} + +// Singleton instance - import this in other modules +export const telemetry = new TelemetryManager(); diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index edbe8f76..edfd969c 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -70,6 +70,7 @@ import { import { assembleFinalReport } from '../phases/reporting.js'; import { getPromptNameForAgent } from '../types/agents.js'; import { AuditSession } from '../audit/index.js'; +import { telemetry, TelemetryEvent, hashTargetUrl } from '../telemetry/index.js'; import type { AgentName } from '../types/agents.js'; import type { AgentMetrics } from './shared.js'; import type { DistributedConfig } from '../types/config.js'; @@ -88,6 +89,14 @@ export interface ActivityInput { outputPath?: string; pipelineTestingMode?: boolean; workflowId: string; + workflowStartTime?: number; // Epoch ms, used for total workflow duration in telemetry + installationId?: string; // Persistent anonymous ID for counting unique installations + // Workflow stats for telemetry (only passed to report agent) + workflowStats?: { + totalAgents: number; + agentsSucceeded: number; + agentsFailed: number; + }; } /** @@ -115,6 +124,7 @@ async function runAgentActivity( outputPath, pipelineTestingMode = false, workflowId, + installationId, } = input; const startTime = Date.now(); @@ -122,6 +132,18 @@ async function runAgentActivity( // Get attempt number from Temporal context (tracks retries automatically) const attemptNumber = Context.current().info.attempt; + // Set installationId as distinct ID for unique user tracking + if (installationId) { + telemetry.setDistinctId(installationId); + } + + // Track agent start + telemetry.track(TelemetryEvent.AGENT_START, { + agent_name: agentName, + attempt_number: attemptNumber, + workflow_id: workflowId, + }); + // Heartbeat loop - signals worker is alive to Temporal server const heartbeatInterval = setInterval(() => { const elapsed = Math.floor((Date.now() - startTime) / 1000); @@ -226,6 +248,15 @@ async function runAgentActivity( }); await commitGitSuccess(repoPath, agentName); + // Track agent completion + telemetry.track(TelemetryEvent.AGENT_COMPLETE, { + agent_name: agentName, + attempt_number: attemptNumber, + duration_ms: Date.now() - startTime, + cost_usd: result.cost ?? undefined, + workflow_id: workflowId, + }); + // 10. Return metrics return { durationMs: Date.now() - startTime, @@ -246,6 +277,17 @@ async function runAgentActivity( // If error is already an ApplicationFailure (e.g., from our retry limit logic), // re-throw it directly without re-classifying if (error instanceof ApplicationFailure) { + // Track retry or failure based on retryability + telemetry.track( + error.nonRetryable ? TelemetryEvent.AGENT_FAILED : TelemetryEvent.AGENT_RETRY, + { + agent_name: agentName, + attempt_number: attemptNumber, + duration_ms: Date.now() - startTime, + error_type: error.type || 'UnknownError', + workflow_id: workflowId, + } + ); throw error; } @@ -255,6 +297,18 @@ async function runAgentActivity( const rawMessage = error instanceof Error ? error.message : String(error); const message = truncateErrorMessage(rawMessage); + // Track retry or failure based on classification + telemetry.track( + classified.retryable ? TelemetryEvent.AGENT_RETRY : TelemetryEvent.AGENT_FAILED, + { + agent_name: agentName, + attempt_number: attemptNumber, + duration_ms: Date.now() - startTime, + error_type: classified.type, + workflow_id: workflowId, + } + ); + if (classified.retryable) { // Temporal will retry with configured backoff const failure = ApplicationFailure.create({ @@ -329,7 +383,42 @@ export async function runAuthzExploitAgent(input: ActivityInput): Promise { - return runAgentActivity('report', input); + // Use workflow start time for total duration if available, otherwise fall back to now + const workflowStartTime = input.workflowStartTime ?? Date.now(); + const stats = input.workflowStats; + const targetHash = hashTargetUrl(input.webUrl); + const workflowId = input.workflowId; + try { + const metrics = await runAgentActivity('report', input); + // Report agent success = workflow complete + telemetry.track(TelemetryEvent.WORKFLOW_COMPLETE, { + total_duration_ms: Date.now() - workflowStartTime, + total_cost_usd: metrics.costUsd ?? undefined, + total_agents: stats?.totalAgents, + agents_succeeded: stats?.agentsSucceeded, + agents_failed: stats?.agentsFailed, + target_hash: targetHash, + workflow_id: workflowId, + }); + return metrics; + } catch (error) { + // Report agent failure = workflow failed + const errorType = + error instanceof ApplicationFailure + ? error.type || 'UnknownError' + : classifyErrorForTemporal(error).type; + telemetry.track(TelemetryEvent.WORKFLOW_FAILED, { + total_duration_ms: Date.now() - workflowStartTime, + error_type: errorType, + last_agent: 'report', + total_agents: stats?.totalAgents, + agents_succeeded: stats?.agentsSucceeded, + agents_failed: stats?.agentsFailed, + target_hash: targetHash, + workflow_id: workflowId, + }); + throw error; + } } /** diff --git a/src/temporal/client.ts b/src/temporal/client.ts index 3d402f39..f5891304 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -31,6 +31,7 @@ import dotenv from 'dotenv'; import chalk from 'chalk'; import { displaySplashScreen } from '../splash-screen.js'; import { sanitizeHostname } from '../audit/utils.js'; +import { telemetry, TelemetryEvent, hashTargetUrl, getInstallationId } from '../telemetry/index.js'; // Import types only - these don't pull in workflow runtime code import type { PipelineInput, PipelineState, PipelineProgress } from './shared.js'; @@ -130,12 +131,20 @@ async function startPipeline(): Promise { const hostname = sanitizeHostname(webUrl); const workflowId = customWorkflowId || `${hostname}_shannon-${Date.now()}`; + // Get persistent installation ID for unique installation counting + const installationId = await getInstallationId(); + + // Initialize telemetry with installation ID as distinct ID (for unique user tracking) + telemetry.initialize(pipelineTestingMode); + telemetry.setDistinctId(installationId); + const input: PipelineInput = { webUrl, repoPath, ...(configPath && { configPath }), ...(outputPath && { outputPath }), ...(pipelineTestingMode && { pipelineTestingMode }), + installationId, }; console.log(chalk.green.bold(`✓ Workflow started: ${workflowId}`)); @@ -160,6 +169,14 @@ async function startPipeline(): Promise { } ); + // Track workflow start + telemetry.track(TelemetryEvent.WORKFLOW_START, { + has_config: !!configPath, + pipeline_testing_mode: pipelineTestingMode, + target_hash: hashTargetUrl(webUrl), + workflow_id: workflowId, + }); + if (!waitForCompletion) { console.log(chalk.bold('Monitor progress:')); console.log(chalk.white(' Web UI: ') + chalk.blue(`http://localhost:8233/namespaces/default/workflows/${workflowId}`)); @@ -202,6 +219,7 @@ async function startPipeline(): Promise { process.exit(1); } } finally { + await telemetry.shutdown(); await connection.close(); } } diff --git a/src/temporal/shared.ts b/src/temporal/shared.ts index e10ad330..49f39483 100644 --- a/src/temporal/shared.ts +++ b/src/temporal/shared.ts @@ -9,6 +9,7 @@ export interface PipelineInput { outputPath?: string; pipelineTestingMode?: boolean; workflowId?: string; // Added by client, used for audit correlation + installationId?: string; // Persistent anonymous ID for counting unique installations } export interface AgentMetrics { diff --git a/src/temporal/worker.ts b/src/temporal/worker.ts index 81c7f7ed..1a7549bf 100644 --- a/src/temporal/worker.ts +++ b/src/temporal/worker.ts @@ -26,6 +26,7 @@ import path from 'node:path'; import dotenv from 'dotenv'; import chalk from 'chalk'; import * as activities from './activities.js'; +import { telemetry } from '../telemetry/index.js'; dotenv.config(); @@ -37,6 +38,10 @@ async function runWorker(): Promise { const connection = await NativeConnection.connect({ address }); + // Initialize telemetry for activity execution + // Worker doesn't know pipelineTestingMode until activity runs, so default to false + telemetry.initialize(); + // Bundle workflows for Temporal's V8 isolate console.log(chalk.gray('Bundling workflows...')); const workflowBundle = await bundleWorkflowCode({ @@ -68,6 +73,7 @@ async function runWorker(): Promise { try { await worker.run(); } finally { + await telemetry.shutdown(); await connection.close(); console.log(chalk.gray('Worker stopped')); } diff --git a/src/temporal/workflows.ts b/src/temporal/workflows.ts index 0b5e384e..f39524e1 100644 --- a/src/temporal/workflows.ts +++ b/src/temporal/workflows.ts @@ -136,6 +136,9 @@ export async function pentestPipelineWorkflow( ...(input.pipelineTestingMode !== undefined && { pipelineTestingMode: input.pipelineTestingMode, }), + ...(input.installationId !== undefined && { + installationId: input.installationId, + }), }; try { @@ -267,7 +270,17 @@ export async function pentestPipelineWorkflow( await a.assembleReportActivity(activityInput); // Then run the report agent to add executive summary and clean up - state.agentMetrics['report'] = await a.runReportAgent(activityInput); + // Pass workflow start time and stats for accurate telemetry + const reportInput = { + ...activityInput, + workflowStartTime: state.startTime, + workflowStats: { + totalAgents: 13, // pre-recon, recon, 5 vuln, 5 exploit, report + agentsSucceeded: state.completedAgents.length, + agentsFailed: failedPipelines.length, + }, + }; + state.agentMetrics['report'] = await a.runReportAgent(reportInput); state.completedAgents.push('report'); // === Complete === From 636ae6fb19ce88d00e95d9bcb601cd1f3857e903 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 17:56:50 -0800 Subject: [PATCH 22/24] docs: update telemetry instructions --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 3a12d59e..3419b183 100644 --- a/README.md +++ b/README.md @@ -467,11 +467,7 @@ export DO_NOT_TRACK=1 export SHANNON_TELEMETRY=off ``` -Or add to your `.env` file: - -```env -DO_NOT_TRACK=1 -``` +Or add `DO_NOT_TRACK=1` to your `.env` file. ## 📜 License From 7dc8cfe5c795ebee7d11f0600c341dc6d28b21e8 Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Tue, 13 Jan 2026 18:17:33 -0800 Subject: [PATCH 23/24] fix: remove pipeline_testing_mode from telemetry events --- src/telemetry/telemetry-manager.ts | 7 +------ src/temporal/client.ts | 3 +-- src/temporal/worker.ts | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/telemetry/telemetry-manager.ts b/src/telemetry/telemetry-manager.ts index 02e1bcc0..ee6be5aa 100644 --- a/src/telemetry/telemetry-manager.ts +++ b/src/telemetry/telemetry-manager.ts @@ -118,7 +118,6 @@ class TelemetryManager { private config: TelemetryConfig; private distinctId: string; private initialized = false; - private pipelineTestingMode = false; constructor() { this.config = loadTelemetryConfig(); @@ -137,16 +136,13 @@ class TelemetryManager { /** * Initialize PostHog client. * Safe: never throws, logs warning on failure. - * - * @param pipelineTestingMode - Whether running in testing mode */ - initialize(pipelineTestingMode = false): void { + initialize(): void { try { if (this.initialized) { return; } - this.pipelineTestingMode = pipelineTestingMode; this.initialized = true; if (!this.config.enabled) { @@ -187,7 +183,6 @@ class TelemetryManager { // Build base properties const baseProps: BaseTelemetryProperties & Record = { - pipeline_testing_mode: this.pipelineTestingMode, os_platform: process.platform, node_version: process.version, $lib: 'shannon', diff --git a/src/temporal/client.ts b/src/temporal/client.ts index f5891304..47482fbd 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -135,7 +135,7 @@ async function startPipeline(): Promise { const installationId = await getInstallationId(); // Initialize telemetry with installation ID as distinct ID (for unique user tracking) - telemetry.initialize(pipelineTestingMode); + telemetry.initialize(); telemetry.setDistinctId(installationId); const input: PipelineInput = { @@ -172,7 +172,6 @@ async function startPipeline(): Promise { // Track workflow start telemetry.track(TelemetryEvent.WORKFLOW_START, { has_config: !!configPath, - pipeline_testing_mode: pipelineTestingMode, target_hash: hashTargetUrl(webUrl), workflow_id: workflowId, }); diff --git a/src/temporal/worker.ts b/src/temporal/worker.ts index 1a7549bf..80e1896d 100644 --- a/src/temporal/worker.ts +++ b/src/temporal/worker.ts @@ -39,7 +39,6 @@ async function runWorker(): Promise { const connection = await NativeConnection.connect({ address }); // Initialize telemetry for activity execution - // Worker doesn't know pipelineTestingMode until activity runs, so default to false telemetry.initialize(); // Bundle workflows for Temporal's V8 isolate From 9d69e43ecf5c52db0249f6a336b9dc4a05e2d78e Mon Sep 17 00:00:00 2001 From: ajmallesh Date: Thu, 15 Jan 2026 10:57:15 -0800 Subject: [PATCH 24/24] fix: resolve merge artifacts from main branch integration - Regenerate corrupted package-lock.json with duplicate entries - Remove unreachable dead code in runReportAgent --- package-lock.json | 1664 +----------------------------------- src/temporal/activities.ts | 1 - 2 files changed, 22 insertions(+), 1643 deletions(-) diff --git a/package-lock.json b/package-lock.json index ebf692cc..70cafd63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -420,41 +420,6 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/@jsonjoy.com/json-pointer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", @@ -464,15 +429,6 @@ "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, "engines": { "node": ">=10.0" }, @@ -493,81 +449,6 @@ "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@swc/core": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.8.tgz", - "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" - }, "engines": { "node": ">=10.0" }, @@ -2192,1540 +2073,39 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.8", - "@swc/core-darwin-x64": "1.15.8", - "@swc/core-linux-arm-gnueabihf": "1.15.8", - "@swc/core-linux-arm64-gnu": "1.15.8", - "@swc/core-linux-arm64-musl": "1.15.8", - "@swc/core-linux-x64-gnu": "1.15.8", - "@swc/core-linux-x64-musl": "1.15.8", - "@swc/core-win32-arm64-msvc": "1.15.8", - "@swc/core-win32-ia32-msvc": "1.15.8", - "@swc/core-win32-x64-msvc": "1.15.8" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.8.tgz", - "integrity": "sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.8.tgz", - "integrity": "sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.8.tgz", - "integrity": "sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" + "url": "https://opencollective.com/webpack" } }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.8.tgz", - "integrity": "sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" } }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.8.tgz", - "integrity": "sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.8.tgz", - "integrity": "sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.8.tgz", - "integrity": "sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.8.tgz", - "integrity": "sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.8.tgz", - "integrity": "sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.8.tgz", - "integrity": "sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@temporalio/activity": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.14.1.tgz", - "integrity": "sha512-wG2fTNgomhcKOzPY7mqhKqe8scawm4BvUYdgX1HJouHmVNRgtZurf2xQWJZQOTxWrsXfdoYqzohZLzxlNtcC5A==", - "license": "MIT", - "dependencies": { - "@temporalio/client": "1.14.1", - "@temporalio/common": "1.14.1", - "abort-controller": "^3.0.0" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/client": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.14.1.tgz", - "integrity": "sha512-AfWSA0LYzBvDLFiFgrPWqTGGq1NGnF3d4xKnxf0PGxSmv5SLb/aqQ9lzHg4DJ5UNkHO4M/NwzdxzzoaR1J5F8Q==", - "license": "MIT", - "dependencies": { - "@grpc/grpc-js": "^1.12.4", - "@temporalio/common": "1.14.1", - "@temporalio/proto": "1.14.1", - "abort-controller": "^3.0.0", - "long": "^5.2.3", - "uuid": "^11.1.0" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/common": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.14.1.tgz", - "integrity": "sha512-y49wOm3AIEKZufIQ/QU5JhTSaHJIEkiUt5bGB0/uSzCg8P4g8Cz0XoVPSbDwuCix533O9cOKcliYq7Gzjt/sIA==", - "license": "MIT", - "dependencies": { - "@temporalio/proto": "1.14.1", - "long": "^5.2.3", - "ms": "3.0.0-canary.1", - "nexus-rpc": "^0.0.1", - "proto3-json-serializer": "^2.0.0" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/core-bridge": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.14.1.tgz", - "integrity": "sha512-mrXXIFK5yNvsSZsTejLnL64JMuMliQjFKktSGITm2Ci7cWZ/ZTOVN6u+hCsUKfadYYv83jSuOC9Xe3z3RK273w==", - "license": "MIT", - "dependencies": { - "@grpc/grpc-js": "^1.12.4", - "@temporalio/common": "1.14.1" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/nexus": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.14.1.tgz", - "integrity": "sha512-51oTeJ8nntAMF8boFSlzVdHlyC7y/LaLQPZMjEEOV2pi8O9yOI7GZvYDIAHhY8Z8AcDVgbXb8x0BbkjkwNiUiQ==", - "license": "MIT", - "dependencies": { - "@temporalio/client": "1.14.1", - "@temporalio/common": "1.14.1", - "@temporalio/proto": "1.14.1", - "long": "^5.2.3", - "nexus-rpc": "^0.0.1" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/proto": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.14.1.tgz", - "integrity": "sha512-mCsUommDPXbXbBu60p1g4jpSqVb+GNR67yR0uKTU8ARb4qVZQo7SQnOUaneoxDERDXuR/yIjVCektMm+7Myb+A==", - "license": "MIT", - "dependencies": { - "long": "^5.2.3", - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/worker": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.14.1.tgz", - "integrity": "sha512-wFfN5gc03eq1bYAuJNsG9a1iWBG6hL9zAfYbxiJdshPhpHa82BtHGvXD447oT2BX3zqI+Jf2b0m/N0wgkW6wyQ==", - "license": "MIT", - "dependencies": { - "@grpc/grpc-js": "^1.12.4", - "@swc/core": "^1.3.102", - "@temporalio/activity": "1.14.1", - "@temporalio/client": "1.14.1", - "@temporalio/common": "1.14.1", - "@temporalio/core-bridge": "1.14.1", - "@temporalio/nexus": "1.14.1", - "@temporalio/proto": "1.14.1", - "@temporalio/workflow": "1.14.1", - "abort-controller": "^3.0.0", - "heap-js": "^2.6.0", - "memfs": "^4.6.0", - "nexus-rpc": "^0.0.1", - "proto3-json-serializer": "^2.0.0", - "protobufjs": "^7.2.5", - "rxjs": "^7.8.1", - "source-map": "^0.7.4", - "source-map-loader": "^4.0.2", - "supports-color": "^8.1.1", - "swc-loader": "^0.2.3", - "unionfs": "^4.5.1", - "webpack": "^5.94.0" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@temporalio/workflow": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.14.1.tgz", - "integrity": "sha512-MzshcoRo8zjQYa9WHrv3XC8LVvpRNSVaW3kOSTmHuTYDh/7be48WODOgs5yUpbnkpsw6rjVCDCgtB/K02cQwDg==", - "license": "MIT", - "dependencies": { - "@temporalio/common": "1.14.1", - "@temporalio/proto": "1.14.1", - "nexus-rpc": "^0.0.1" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/tinycolor2": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/boxen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^8.0.0", - "chalk": "^5.3.0", - "cli-boxes": "^3.0.0", - "string-width": "^7.2.0", - "type-fest": "^4.21.0", - "widest-line": "^5.0.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/camelcase": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", - "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", - "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/figlet": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.3.tgz", - "integrity": "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA==", - "license": "MIT", - "dependencies": { - "commander": "^14.0.0" - }, - "bin": { - "figlet": "bin/index.js" - }, - "engines": { - "node": ">= 17.0.0" - } - }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", - "license": "Unlicense" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gradient-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", - "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "tinygradient": "^1.1.5" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/heap-js": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", - "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "license": "MIT", - "engines": { - "node": ">=10.18" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/memfs": { - "version": "4.51.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.1.tgz", - "integrity": "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==", - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "3.0.0-canary.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", - "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", - "license": "MIT", - "engines": { - "node": ">=12.13" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/nexus-rpc": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.1.tgz", - "integrity": "sha512-hAWn8Hh2eewpB5McXR5EW81R3pR/ziuGhKCF3wFyUVCklanPqrIgMNr7jKCbzXeNVad0nUDfWpFRqh2u+zxQtw==", - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", - "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", - "license": "Apache-2.0", - "dependencies": { - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", diff --git a/src/temporal/activities.ts b/src/temporal/activities.ts index aa9c3171..c093827b 100644 --- a/src/temporal/activities.ts +++ b/src/temporal/activities.ts @@ -440,7 +440,6 @@ export async function runReportAgent(input: ActivityInput): Promise