refactor: replace console.log/chalk with ActivityLogger across services
- Add ActivityLogger interface wrapping Temporal's Context.current().log - Thread logger parameter through claude-executor, message-handlers, git-manager, prompt-manager, reporting, and agent validators - Remove chalk dependency from all service/activity files; CLI files keep console.log for terminal output - Replace colorFn: ChalkInstance parameter with structured logger.info/warn/error calls - Use replay-safe `log` import from @temporalio/workflow in workflows.ts
This commit is contained in:
+31
-20
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
import { heartbeat, ApplicationFailure, Context } from '@temporalio/activity';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
@@ -35,6 +34,7 @@ import { assembleFinalReport, injectModelIntoReport } from '../phases/reporting.
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import { executeGitCommandWithRetry } from '../utils/git-manager.js';
|
||||
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
||||
import { createActivityLogger } from './activity-logger.js';
|
||||
|
||||
// Max lengths to prevent Temporal protobuf buffer overflow
|
||||
const MAX_ERROR_MESSAGE_LENGTH = 2000;
|
||||
@@ -114,6 +114,8 @@ async function runAgentActivity(
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
|
||||
try {
|
||||
const logger = createActivityLogger();
|
||||
|
||||
// Build session metadata and get/create container
|
||||
const sessionMetadata = buildSessionMetadata(input);
|
||||
const container = getOrCreateContainer(workflowId, sessionMetadata);
|
||||
@@ -134,7 +136,8 @@ async function runAgentActivity(
|
||||
pipelineTestingMode,
|
||||
attemptNumber,
|
||||
},
|
||||
auditSession
|
||||
auditSession,
|
||||
logger
|
||||
);
|
||||
|
||||
// Success - return metrics
|
||||
@@ -251,12 +254,13 @@ export async function runReportAgent(input: ActivityInput): Promise<AgentMetrics
|
||||
*/
|
||||
export async function assembleReportActivity(input: ActivityInput): Promise<void> {
|
||||
const { repoPath } = input;
|
||||
console.log(chalk.blue(' Assembling deliverables from specialist agents...'));
|
||||
const logger = createActivityLogger();
|
||||
logger.info('Assembling deliverables from specialist agents...');
|
||||
try {
|
||||
await assembleFinalReport(repoPath);
|
||||
await assembleFinalReport(repoPath, logger);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(` Warning: Error assembling final report: ${err.message}`));
|
||||
logger.warn(`Error assembling final report: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,14 +269,15 @@ export async function assembleReportActivity(input: ActivityInput): Promise<void
|
||||
*/
|
||||
export async function injectReportMetadataActivity(input: ActivityInput): Promise<void> {
|
||||
const { repoPath, sessionId, outputPath } = input;
|
||||
const logger = createActivityLogger();
|
||||
const effectiveOutputPath = outputPath
|
||||
? path.join(outputPath, sessionId)
|
||||
: path.join('./audit-logs', sessionId);
|
||||
try {
|
||||
await injectModelIntoReport(repoPath, effectiveOutputPath);
|
||||
await injectModelIntoReport(repoPath, effectiveOutputPath, logger);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(` Warning: Error injecting model into report: ${err.message}`));
|
||||
logger.warn(`Error injecting model into report: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,12 +294,13 @@ export async function checkExploitationQueue(
|
||||
vulnType: VulnType
|
||||
): Promise<ExploitationDecision> {
|
||||
const { repoPath, workflowId } = input;
|
||||
const logger = createActivityLogger();
|
||||
|
||||
// Reuse container's service if available (from prior vuln agent runs)
|
||||
const existingContainer = getContainer(workflowId);
|
||||
const checker = existingContainer?.exploitationChecker ?? new ExploitationCheckerService();
|
||||
|
||||
return checker.checkQueue(vulnType, repoPath);
|
||||
return checker.checkQueue(vulnType, repoPath, logger);
|
||||
}
|
||||
|
||||
// === Resume Activities ===
|
||||
@@ -368,9 +374,8 @@ export async function loadResumeState(
|
||||
const deliverableExists = await fileExists(deliverablePath);
|
||||
|
||||
if (!deliverableExists) {
|
||||
console.log(
|
||||
chalk.yellow(`Agent ${agentName} shows success but deliverable missing, will re-run`)
|
||||
);
|
||||
const logger = createActivityLogger();
|
||||
logger.warn(`Agent ${agentName} shows success but deliverable missing, will re-run`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -400,10 +405,12 @@ export async function loadResumeState(
|
||||
const checkpointHash = await findLatestCommit(expectedRepoPath, checkpoints);
|
||||
const originalWorkflowId = session.session.originalWorkflowId || session.session.id;
|
||||
|
||||
console.log(chalk.cyan(`=== RESUME STATE ===`));
|
||||
console.log(`Workspace: ${workspaceName}`);
|
||||
console.log(`Completed agents: ${completedAgents.length}`);
|
||||
console.log(`Checkpoint: ${checkpointHash}`);
|
||||
const logger = createActivityLogger();
|
||||
logger.info('Resume state loaded', {
|
||||
workspace: workspaceName,
|
||||
completedAgents: completedAgents.length,
|
||||
checkpoint: checkpointHash,
|
||||
});
|
||||
|
||||
return {
|
||||
workspaceName,
|
||||
@@ -446,7 +453,8 @@ export async function restoreGitCheckpoint(
|
||||
checkpointHash: string,
|
||||
incompleteAgents: AgentName[]
|
||||
): Promise<void> {
|
||||
console.log(chalk.blue(`Restoring git workspace to ${checkpointHash}...`));
|
||||
const logger = createActivityLogger();
|
||||
logger.info(`Restoring git workspace to ${checkpointHash}...`);
|
||||
|
||||
await executeGitCommandWithRetry(
|
||||
['git', 'reset', '--hard', checkpointHash],
|
||||
@@ -465,15 +473,15 @@ export async function restoreGitCheckpoint(
|
||||
try {
|
||||
const exists = await fileExists(deliverablePath);
|
||||
if (exists) {
|
||||
console.log(chalk.yellow(`Cleaning partial deliverable: ${agentName}`));
|
||||
logger.warn(`Cleaning partial deliverable: ${agentName}`);
|
||||
await fs.unlink(deliverablePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.gray(`Note: Failed to delete ${deliverablePath}: ${error}`));
|
||||
logger.info(`Note: Failed to delete ${deliverablePath}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green('Workspace restored to clean state'));
|
||||
logger.info('Workspace restored to clean state');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,7 +569,10 @@ export async function logWorkflowComplete(
|
||||
try {
|
||||
await copyDeliverablesToAudit(sessionMetadata, repoPath);
|
||||
} catch (copyErr) {
|
||||
console.error('Failed to copy deliverables to audit-logs:', copyErr);
|
||||
const logger = createActivityLogger();
|
||||
logger.error('Failed to copy deliverables to audit-logs', {
|
||||
error: copyErr instanceof Error ? copyErr.message : String(copyErr),
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up container
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 { Context } from '@temporalio/activity';
|
||||
|
||||
/**
|
||||
* Logger interface for services called from Temporal activities.
|
||||
* Keeps services Temporal-agnostic while providing structured logging.
|
||||
*/
|
||||
export interface ActivityLogger {
|
||||
info(message: string, attrs?: Record<string, unknown>): void;
|
||||
warn(message: string, attrs?: Record<string, unknown>): void;
|
||||
error(message: string, attrs?: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityLogger backed by Temporal's Context.current().log.
|
||||
* Must be called inside a running Temporal activity — throws otherwise.
|
||||
*/
|
||||
export class TemporalActivityLogger implements ActivityLogger {
|
||||
info(message: string, attrs?: Record<string, unknown>): void {
|
||||
Context.current().log.info(message, attrs ?? {});
|
||||
}
|
||||
|
||||
warn(message: string, attrs?: Record<string, unknown>): void {
|
||||
Context.current().log.warn(message, attrs ?? {});
|
||||
}
|
||||
|
||||
error(message: string, attrs?: Record<string, unknown>): void {
|
||||
Context.current().log.error(message, attrs ?? {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ActivityLogger. Must be called inside a Temporal activity.
|
||||
* Throws if called outside an activity context.
|
||||
*/
|
||||
export function createActivityLogger(): ActivityLogger {
|
||||
return new TemporalActivityLogger();
|
||||
}
|
||||
+40
-44
@@ -28,7 +28,6 @@
|
||||
|
||||
import { Connection, Client, WorkflowNotFoundError } from '@temporalio/client';
|
||||
import dotenv from 'dotenv';
|
||||
import chalk from 'chalk';
|
||||
import { displaySplashScreen } from '../splash-screen.js';
|
||||
import { sanitizeHostname } from '../audit/utils.js';
|
||||
import { readJson, fileExists } from '../audit/utils.js';
|
||||
@@ -89,18 +88,18 @@ async function terminateExistingWorkflows(
|
||||
const description = await handle.describe();
|
||||
|
||||
if (description.status.name === 'RUNNING') {
|
||||
console.log(chalk.yellow(`Terminating running workflow: ${wfId}`));
|
||||
console.log(`Terminating running workflow: ${wfId}`);
|
||||
await handle.terminate('Superseded by resume workflow');
|
||||
terminated.push(wfId);
|
||||
console.log(chalk.green(`Terminated: ${wfId}`));
|
||||
console.log(`Terminated: ${wfId}`);
|
||||
} else {
|
||||
console.log(chalk.gray(`Workflow already ${description.status.name}: ${wfId}`));
|
||||
console.log(`Workflow already ${description.status.name}: ${wfId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof WorkflowNotFoundError) {
|
||||
console.log(chalk.gray(`Workflow not found (already cleaned up): ${wfId}`));
|
||||
console.log(`Workflow not found (already cleaned up): ${wfId}`);
|
||||
} else {
|
||||
console.log(chalk.red(`Failed to terminate ${wfId}: ${error}`));
|
||||
console.log(`Failed to terminate ${wfId}: ${error}`);
|
||||
// Continue anyway - don't block resume on termination failure
|
||||
}
|
||||
}
|
||||
@@ -118,13 +117,13 @@ function isValidWorkspaceName(name: string): boolean {
|
||||
}
|
||||
|
||||
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('\nShannon Temporal Client');
|
||||
console.log('Start a pentest pipeline workflow\n');
|
||||
console.log('Usage:');
|
||||
console.log(
|
||||
' node dist/temporal/client.js <webUrl> <repoPath> [options]\n'
|
||||
);
|
||||
console.log(chalk.yellow('Options:'));
|
||||
console.log('Options:');
|
||||
console.log(' --config <path> Configuration file path');
|
||||
console.log(' --output <path> Output directory for audit logs');
|
||||
console.log(' --pipeline-testing Use minimal prompts for fast testing');
|
||||
@@ -133,7 +132,7 @@ function showUsage(): void {
|
||||
' --workflow-id <id> Custom workflow ID (default: shannon-<timestamp>)'
|
||||
);
|
||||
console.log(' --wait Wait for workflow completion with progress polling\n');
|
||||
console.log(chalk.yellow('Examples:'));
|
||||
console.log('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'
|
||||
@@ -205,7 +204,7 @@ async function startPipeline(): Promise<void> {
|
||||
}
|
||||
|
||||
if (!webUrl || !repoPath) {
|
||||
console.log(chalk.red('Error: webUrl and repoPath are required'));
|
||||
console.log('Error: webUrl and repoPath are required');
|
||||
showUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -214,7 +213,7 @@ async function startPipeline(): Promise<void> {
|
||||
await displaySplashScreen();
|
||||
|
||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||
console.log(chalk.gray(`Connecting to Temporal at ${address}...`));
|
||||
console.log(`Connecting to Temporal at ${address}...`);
|
||||
|
||||
const connection = await Connection.connect({ address });
|
||||
const client = new Client({ connection });
|
||||
@@ -232,21 +231,21 @@ async function startPipeline(): Promise<void> {
|
||||
if (workspaceExists) {
|
||||
// === Resume Mode: existing workspace ===
|
||||
isResume = true;
|
||||
console.log(chalk.cyan('=== RESUME MODE ==='));
|
||||
console.log('=== RESUME MODE ===');
|
||||
console.log(`Workspace: ${resumeFromWorkspace}\n`);
|
||||
|
||||
// Terminate any running workflows for this workspace
|
||||
terminatedWorkflows = await terminateExistingWorkflows(client, resumeFromWorkspace);
|
||||
|
||||
if (terminatedWorkflows.length > 0) {
|
||||
console.log(chalk.yellow(`Terminated ${terminatedWorkflows.length} previous workflow(s)\n`));
|
||||
console.log(`Terminated ${terminatedWorkflows.length} previous workflow(s)\n`);
|
||||
}
|
||||
|
||||
// Validate URL matches workspace
|
||||
const session = await readJson<SessionJson>(sessionPath);
|
||||
|
||||
if (session.session.webUrl !== webUrl) {
|
||||
console.error(chalk.red('ERROR: URL mismatch with workspace'));
|
||||
console.error('ERROR: URL mismatch with workspace');
|
||||
console.error(` Workspace URL: ${session.session.webUrl}`);
|
||||
console.error(` Provided URL: ${webUrl}`);
|
||||
process.exit(1);
|
||||
@@ -258,12 +257,12 @@ async function startPipeline(): Promise<void> {
|
||||
} else {
|
||||
// === New Named Workspace ===
|
||||
if (!isValidWorkspaceName(resumeFromWorkspace)) {
|
||||
console.error(chalk.red(`ERROR: Invalid workspace name: "${resumeFromWorkspace}"`));
|
||||
console.error(chalk.gray(' Must be 1-128 characters, alphanumeric/hyphens/underscores, starting with alphanumeric'));
|
||||
console.error(`ERROR: Invalid workspace name: "${resumeFromWorkspace}"`);
|
||||
console.error(' Must be 1-128 characters, alphanumeric/hyphens/underscores, starting with alphanumeric');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('=== NEW NAMED WORKSPACE ==='));
|
||||
console.log('=== NEW NAMED WORKSPACE ===');
|
||||
console.log(`Workspace: ${resumeFromWorkspace}\n`);
|
||||
|
||||
workflowId = `${resumeFromWorkspace}_shannon-${Date.now()}`;
|
||||
@@ -293,22 +292,22 @@ async function startPipeline(): Promise<void> {
|
||||
const effectiveDisplayPath = displayOutputPath || outputPath || './audit-logs';
|
||||
const outputDir = `${effectiveDisplayPath}/${sessionId}`;
|
||||
|
||||
console.log(chalk.green.bold(`✓ Workflow started: ${workflowId}`));
|
||||
console.log(`✓ Workflow started: ${workflowId}`);
|
||||
if (isResume) {
|
||||
console.log(chalk.gray(` (Resuming workspace: ${sessionId})`));
|
||||
console.log(` (Resuming workspace: ${sessionId})`);
|
||||
}
|
||||
console.log();
|
||||
console.log(chalk.white(' Target: ') + chalk.cyan(webUrl));
|
||||
console.log(chalk.white(' Repository: ') + chalk.cyan(repoPath));
|
||||
console.log(chalk.white(' Workspace: ') + chalk.cyan(sessionId));
|
||||
console.log(` Target: ${webUrl}`);
|
||||
console.log(` Repository: ${repoPath}`);
|
||||
console.log(` Workspace: ${sessionId}`);
|
||||
if (configPath) {
|
||||
console.log(chalk.white(' Config: ') + chalk.cyan(configPath));
|
||||
console.log(` Config: ${configPath}`);
|
||||
}
|
||||
if (displayOutputPath) {
|
||||
console.log(chalk.white(' Output: ') + chalk.cyan(displayOutputPath));
|
||||
console.log(` Output: ${displayOutputPath}`);
|
||||
}
|
||||
if (pipelineTestingMode) {
|
||||
console.log(chalk.white(' Mode: ') + chalk.yellow('Pipeline Testing'));
|
||||
console.log(` Mode: Pipeline Testing`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
@@ -323,12 +322,12 @@ async function startPipeline(): Promise<void> {
|
||||
);
|
||||
|
||||
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(`./shannon logs ID=${workflowId}`));
|
||||
console.log('Monitor progress:');
|
||||
console.log(` Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}`);
|
||||
console.log(` Logs: ./shannon logs ID=${workflowId}`);
|
||||
console.log();
|
||||
console.log(chalk.bold('Output:'));
|
||||
console.log(chalk.white(' Reports: ') + chalk.cyan(outputDir));
|
||||
console.log('Output:');
|
||||
console.log(` Reports: ${outputDir}`);
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
@@ -339,10 +338,7 @@ async function startPipeline(): Promise<void> {
|
||||
const progress = await handle.query<PipelineProgress>(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`)
|
||||
`[${elapsed}s] Phase: ${progress.currentPhase || 'unknown'} | Agent: ${progress.currentAgent || 'none'} | Completed: ${progress.completedAgents.length}/13`
|
||||
);
|
||||
} catch {
|
||||
// Workflow may have completed
|
||||
@@ -353,12 +349,12 @@ async function startPipeline(): Promise<void> {
|
||||
const result = await handle.result();
|
||||
clearInterval(progressInterval);
|
||||
|
||||
console.log(chalk.green.bold('\nPipeline completed successfully!'));
|
||||
console.log('\nPipeline completed successfully!');
|
||||
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(`Run cost: $${result.summary.totalCostUsd.toFixed(4)}`));
|
||||
console.log(`Duration: ${Math.floor(result.summary.totalDurationMs / 1000)}s`);
|
||||
console.log(`Agents completed: ${result.summary.agentCount}`);
|
||||
console.log(`Total turns: ${result.summary.totalTurns}`);
|
||||
console.log(`Run cost: $${result.summary.totalCostUsd.toFixed(4)}`);
|
||||
|
||||
// Show cumulative cost from session.json (includes all resume attempts)
|
||||
if (isResume) {
|
||||
@@ -366,7 +362,7 @@ async function startPipeline(): Promise<void> {
|
||||
const session = await readJson<SessionJson>(
|
||||
path.join('./audit-logs', sessionId, 'session.json')
|
||||
);
|
||||
console.log(chalk.gray(`Cumulative cost: $${session.metrics.total_cost_usd.toFixed(4)}`));
|
||||
console.log(`Cumulative cost: $${session.metrics.total_cost_usd.toFixed(4)}`);
|
||||
} catch {
|
||||
// Non-fatal, skip cumulative cost display
|
||||
}
|
||||
@@ -374,7 +370,7 @@ async function startPipeline(): Promise<void> {
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(progressInterval);
|
||||
console.error(chalk.red.bold('\nPipeline failed:'), error);
|
||||
console.error('\nPipeline failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
@@ -383,6 +379,6 @@ async function startPipeline(): Promise<void> {
|
||||
}
|
||||
|
||||
startPipeline().catch((err) => {
|
||||
console.error(chalk.red('Client error:'), err);
|
||||
console.error('Client error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ 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();
|
||||
@@ -33,12 +32,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function runWorker(): Promise<void> {
|
||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||
console.log(chalk.cyan(`Connecting to Temporal at ${address}...`));
|
||||
console.log(`Connecting to Temporal at ${address}...`);
|
||||
|
||||
const connection = await NativeConnection.connect({ address });
|
||||
|
||||
// Bundle workflows for Temporal's V8 isolate
|
||||
console.log(chalk.gray('Bundling workflows...'));
|
||||
console.log('Bundling workflows...');
|
||||
const workflowBundle = await bundleWorkflowCode({
|
||||
workflowsPath: path.join(__dirname, 'workflows.js'),
|
||||
});
|
||||
@@ -54,26 +53,26 @@ async function runWorker(): Promise<void> {
|
||||
|
||||
// Graceful shutdown handling
|
||||
const shutdown = async (): Promise<void> => {
|
||||
console.log(chalk.yellow('\nShutting down worker...'));
|
||||
console.log('\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'));
|
||||
console.log('Shannon worker started');
|
||||
console.log('Task queue: shannon-pipeline');
|
||||
console.log('Press Ctrl+C to stop\n');
|
||||
|
||||
try {
|
||||
await worker.run();
|
||||
} finally {
|
||||
await connection.close();
|
||||
console.log(chalk.gray('Worker stopped'));
|
||||
console.log('Worker stopped');
|
||||
}
|
||||
}
|
||||
|
||||
runWorker().catch((err) => {
|
||||
console.error(chalk.red('Worker failed:'), err);
|
||||
console.error('Worker failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
+12
-12
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
log,
|
||||
proxyActivities,
|
||||
setHandler,
|
||||
workflowInfo,
|
||||
@@ -170,7 +171,7 @@ export async function pentestPipelineWorkflow(
|
||||
|
||||
// Check if all agents are already complete
|
||||
if (resumeState.completedAgents.length === ALL_AGENTS.length) {
|
||||
console.log(`All ${ALL_AGENTS.length} agents already completed. Nothing to resume.`);
|
||||
log.info(`All ${ALL_AGENTS.length} agents already completed. Nothing to resume.`);
|
||||
state.status = 'completed';
|
||||
state.completedAgents = [...resumeState.completedAgents];
|
||||
state.summary = computeSummary(state);
|
||||
@@ -184,7 +185,7 @@ export async function pentestPipelineWorkflow(
|
||||
resumeState.checkpointHash
|
||||
);
|
||||
|
||||
console.log('Resume state loaded and workspace restored');
|
||||
log.info('Resume state loaded and workspace restored');
|
||||
}
|
||||
|
||||
// Helper to check if an agent should be skipped
|
||||
@@ -203,7 +204,7 @@ export async function pentestPipelineWorkflow(
|
||||
state.completedAgents.push('pre-recon');
|
||||
await a.logPhaseTransition(activityInput, 'pre-recon', 'complete');
|
||||
} else {
|
||||
console.log('Skipping pre-recon (already complete)');
|
||||
log.info('Skipping pre-recon (already complete)');
|
||||
state.completedAgents.push('pre-recon');
|
||||
}
|
||||
|
||||
@@ -216,7 +217,7 @@ export async function pentestPipelineWorkflow(
|
||||
state.completedAgents.push('recon');
|
||||
await a.logPhaseTransition(activityInput, 'recon', 'complete');
|
||||
} else {
|
||||
console.log('Skipping recon (already complete)');
|
||||
log.info('Skipping recon (already complete)');
|
||||
state.completedAgents.push('recon');
|
||||
}
|
||||
|
||||
@@ -243,7 +244,7 @@ export async function pentestPipelineWorkflow(
|
||||
if (!shouldSkip(vulnAgentName)) {
|
||||
vulnMetrics = await runVulnAgent();
|
||||
} else {
|
||||
console.log(`Skipping ${vulnAgentName} (already complete)`);
|
||||
log.info(`Skipping ${vulnAgentName} (already complete)`);
|
||||
}
|
||||
|
||||
// Step 2: Check exploitation queue (only if vuln agent ran or completed previously)
|
||||
@@ -255,7 +256,7 @@ export async function pentestPipelineWorkflow(
|
||||
if (!shouldSkip(exploitAgentName)) {
|
||||
exploitMetrics = await runExploitAgent();
|
||||
} else {
|
||||
console.log(`Skipping ${exploitAgentName} (already complete)`);
|
||||
log.info(`Skipping ${exploitAgentName} (already complete)`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +330,7 @@ export async function pentestPipelineWorkflow(
|
||||
runVulnExploitPipeline(config.vulnType, config.runVuln, config.runExploit)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
log.info(
|
||||
`Skipping entire ${config.vulnType} pipeline (both agents complete)`
|
||||
);
|
||||
// Still need to mark them as completed in state
|
||||
@@ -378,10 +379,9 @@ export async function pentestPipelineWorkflow(
|
||||
|
||||
// Log any pipeline failures (workflow continues despite failures)
|
||||
if (failedPipelines.length > 0) {
|
||||
console.log(
|
||||
`⚠️ ${failedPipelines.length} pipeline(s) failed:`,
|
||||
failedPipelines
|
||||
);
|
||||
log.warn(`${failedPipelines.length} pipeline(s) failed`, {
|
||||
failures: failedPipelines,
|
||||
});
|
||||
}
|
||||
|
||||
// Update phase markers
|
||||
@@ -407,7 +407,7 @@ export async function pentestPipelineWorkflow(
|
||||
|
||||
await a.logPhaseTransition(activityInput, 'reporting', 'complete');
|
||||
} else {
|
||||
console.log('Skipping report (already complete)');
|
||||
log.info('Skipping report (already complete)');
|
||||
state.completedAgents.push('report');
|
||||
}
|
||||
|
||||
|
||||
+22
-34
@@ -20,7 +20,6 @@
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface SessionJson {
|
||||
session: {
|
||||
@@ -59,16 +58,7 @@ function formatDuration(ms: number): string {
|
||||
}
|
||||
|
||||
function getStatusDisplay(status: string): string {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return chalk.green(status);
|
||||
case 'in-progress':
|
||||
return chalk.yellow(status);
|
||||
case 'failed':
|
||||
return chalk.red(status);
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
@@ -83,8 +73,8 @@ async function listWorkspaces(): Promise<void> {
|
||||
try {
|
||||
entries = await fs.readdir(auditDir);
|
||||
} catch {
|
||||
console.log(chalk.yellow('No audit-logs directory found.'));
|
||||
console.log(chalk.gray(`Expected: ${auditDir}`));
|
||||
console.log('No audit-logs directory found.');
|
||||
console.log(`Expected: ${auditDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,15 +100,15 @@ async function listWorkspaces(): Promise<void> {
|
||||
}
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
console.log(chalk.yellow('\nNo workspaces found.'));
|
||||
console.log(chalk.gray('Run a pipeline first: ./shannon start URL=<url> REPO=<repo>'));
|
||||
console.log('\nNo workspaces found.');
|
||||
console.log('Run a pipeline first: ./shannon start URL=<url> REPO=<repo>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by creation date (most recent first)
|
||||
workspaces.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
console.log(chalk.cyan.bold('\n=== Shannon Workspaces ===\n'));
|
||||
console.log('\n=== Shannon Workspaces ===\n');
|
||||
|
||||
// Column widths
|
||||
const nameWidth = 30;
|
||||
@@ -129,16 +119,14 @@ async function listWorkspaces(): Promise<void> {
|
||||
|
||||
// Header
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' ' +
|
||||
'WORKSPACE'.padEnd(nameWidth) +
|
||||
'URL'.padEnd(urlWidth) +
|
||||
'STATUS'.padEnd(statusWidth) +
|
||||
'DURATION'.padEnd(durationWidth) +
|
||||
'COST'.padEnd(costWidth)
|
||||
)
|
||||
' ' +
|
||||
'WORKSPACE'.padEnd(nameWidth) +
|
||||
'URL'.padEnd(urlWidth) +
|
||||
'STATUS'.padEnd(statusWidth) +
|
||||
'DURATION'.padEnd(durationWidth) +
|
||||
'COST'.padEnd(costWidth)
|
||||
);
|
||||
console.log(chalk.gray(' ' + '\u2500'.repeat(nameWidth + urlWidth + statusWidth + durationWidth + costWidth)));
|
||||
console.log(' ' + '\u2500'.repeat(nameWidth + urlWidth + statusWidth + durationWidth + costWidth));
|
||||
|
||||
let resumableCount = 0;
|
||||
|
||||
@@ -154,15 +142,15 @@ async function listWorkspaces(): Promise<void> {
|
||||
resumableCount++;
|
||||
}
|
||||
|
||||
const resumeTag = isResumable ? chalk.cyan(' (resumable)') : '';
|
||||
const resumeTag = isResumable ? ' (resumable)' : '';
|
||||
|
||||
console.log(
|
||||
' ' +
|
||||
chalk.white(truncate(ws.name, nameWidth - 2).padEnd(nameWidth)) +
|
||||
chalk.gray(truncate(ws.url, urlWidth - 2).padEnd(urlWidth)) +
|
||||
getStatusDisplay(ws.status).padEnd(statusWidth + 10) + // +10 for chalk escape codes
|
||||
chalk.gray(duration.padEnd(durationWidth)) +
|
||||
chalk.gray(cost.padEnd(costWidth)) +
|
||||
truncate(ws.name, nameWidth - 2).padEnd(nameWidth) +
|
||||
truncate(ws.url, urlWidth - 2).padEnd(urlWidth) +
|
||||
getStatusDisplay(ws.status).padEnd(statusWidth) +
|
||||
duration.padEnd(durationWidth) +
|
||||
cost.padEnd(costWidth) +
|
||||
resumeTag
|
||||
);
|
||||
}
|
||||
@@ -170,16 +158,16 @@ async function listWorkspaces(): Promise<void> {
|
||||
console.log();
|
||||
const summary = `${workspaces.length} workspace${workspaces.length === 1 ? '' : 's'} found`;
|
||||
const resumeSummary = resumableCount > 0 ? ` (${resumableCount} resumable)` : '';
|
||||
console.log(chalk.gray(`${summary}${resumeSummary}`));
|
||||
console.log(`${summary}${resumeSummary}`);
|
||||
|
||||
if (resumableCount > 0) {
|
||||
console.log(chalk.gray('\nResume with: ./shannon start URL=<url> REPO=<repo> WORKSPACE=<name>'));
|
||||
console.log('\nResume with: ./shannon start URL=<url> REPO=<repo> WORKSPACE=<name>');
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
listWorkspaces().catch((err) => {
|
||||
console.error(chalk.red('Error listing workspaces:'), err);
|
||||
console.error('Error listing workspaces:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user