Shannon is built on several core design patterns that ensure modularity, reliability, and extensibility. Understanding these patterns is essential for contributing to or extending Shannon.
Core Design Principles
Configuration-Driven YAML configs with JSON Schema validation drive testing behavior
Progressive Analysis Each phase builds on deliverables from previous phases
SDK-First Claude Agent SDK handles autonomous AI execution
Modular Error Handling Explicit Result types and error classification
Configuration-Driven Design
Shannon’s behavior is driven by YAML configuration files validated against JSON Schema.
Benefits
Type safety : JSON Schema ensures configurations are valid before execution
Flexibility : Change testing behavior without code changes
Reproducibility : Configurations can be versioned and shared
Documentation : Schema serves as configuration documentation
Implementation
export async function parseConfig ( configPath : string ) : Promise < AppConfig > {
// 1. Load YAML file
const yamlContent = await fs . readFile ( configPath , 'utf-8' );
const rawConfig = yaml . load ( yamlContent );
// 2. Validate against JSON Schema
const validate = ajv . compile ( configSchema );
if ( ! validate ( rawConfig )) {
throw new PentestError (
'CONFIGURATION_ERROR' ,
'CONFIG_VALIDATION_FAILED' ,
`Schema validation failed: ${ ajv . errorsText ( validate . errors ) } `
);
}
// 3. Security validation (path traversal, dangerous patterns)
validateSecurityConstraints ( rawConfig );
return rawConfig as AppConfig ;
}
Configuration Schema
The schema (configs/config-schema.json) defines:
Required vs. optional fields
Type constraints (string, number, array, object)
Enum values (login_type, rule types)
Pattern validation (URLs, file paths)
Custom validation (credential requirements)
Changes to configuration structure require updating both the schema and TypeScript types in src/types/config.ts.
Progressive Analysis Pattern
Each agent builds on the deliverables from previous agents, creating a knowledge accumulation system.
Analysis Flow
Benefits
Efficiency : Later agents don’t repeat earlier work
Context : Each agent has full history of previous findings
Specialization : Agents focus on their specific task
Reproducibility : Intermediate results can be inspected
Implementation
Agents reference previous deliverables in prompts:
prompts/vuln-injection.txt
## Prerequisites
You have access to these deliverables from previous agents:
1. **code_analysis_deliverable.md** - Source code structure and technologies
2. **recon_deliverable.md** - Attack surface map with all endpoints
Review both deliverables to understand the application before starting analysis.
The MCP save_deliverable tool saves to deliverables/ directory in the repository, making files accessible to subsequent agents.
SDK-First Pattern
Shannon delegates autonomous execution to the Claude Agent SDK rather than scripting agent behavior.
Benefits
Autonomy : Agents can explore, adapt, and make decisions
Tool use : Agents can use browsers, CLI tools, and APIs naturally
Reasoning : Multi-turn reasoning about vulnerabilities
Flexibility : Handles unexpected application behaviors
Implementation
src/ai/claude-executor.ts
export async function executeAgent (
agentName : AgentName ,
prompt : string ,
sourceDir : string ,
logger : ActivityLogger
) : Promise < ClaudePromptResult > {
// Configure Claude SDK
const result = await query ({
system: prompt ,
messages: [],
model: modelName ,
maxTurns: 10_000 , // Allow deep exploration
bypassPermissions: true , // Auto-approve MCP tools
mcpServers: buildMcpServers ( sourceDir , agentName , logger ),
});
return result ;
}
Agents can:
Execute browser automation (Playwright MCP)
Run shell commands (file system access)
Save deliverables (custom MCP tool)
Generate TOTP codes (custom MCP tool)
Multi-Turn Execution
maxTurns : 10_000 // Up to 10,000 AI→Tool→AI cycles
This allows agents to:
Read source code
Form hypotheses about vulnerabilities
Test hypotheses with browser automation
Refine payloads based on responses
Document findings
Iterate until objective is met
Modular Error Handling
Shannon uses explicit error types and Result patterns instead of exceptions.
Error Classification
src/services/error-handling.ts
export class PentestError extends Error {
constructor (
public readonly code : ErrorCode ,
public readonly type : PentestErrorType ,
message : string ,
public readonly context ?: Record < string , unknown >
) {
super ( message );
}
}
export enum ErrorCode {
// Non-retryable errors
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR' ,
PERMISSION_ERROR = 'PERMISSION_ERROR' ,
INVALID_REQUEST = 'INVALID_REQUEST' ,
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR' ,
// Retryable errors
RATE_LIMIT_ERROR = 'RATE_LIMIT_ERROR' ,
NETWORK_ERROR = 'NETWORK_ERROR' ,
TIMEOUT_ERROR = 'TIMEOUT_ERROR' ,
EXECUTION_ERROR = 'EXECUTION_ERROR' ,
}
Result Type Pattern
Services return explicit Result<T, E> types:
src/services/agent-execution.ts
export async function execute (
agentName : AgentName ,
input : ActivityInput ,
logger : ActivityLogger
) : Promise < Result < AgentMetrics , PentestError >> {
try {
// Execute agent
const metrics = await runAgent ( agentName , input , logger );
return { ok: true , value: metrics };
} catch ( error ) {
const pentestError = classifyError ( error );
return { ok: false , error: pentestError };
}
}
Activities unwrap Results and throw for Temporal retry:
src/temporal/activities.ts
export async function runAgentActivity (
agentName : AgentName ,
input : ActivityInput
) : Promise < AgentMetrics > {
const result = await agentExecutionService . execute ( agentName , input , logger );
if ( ! result . ok ) {
// Throw classified error for Temporal retry
throw result . error ;
}
return result . value ;
}
Benefits
Explicit failures : Forces handling of error cases
Type safety : TypeScript enforces error checking
Retry control : Classification determines retry behavior
Context : Errors carry diagnostic information
Services Boundary Pattern
Business logic lives in src/services/, isolated from Temporal orchestration.
Architecture
src/temporal/ # Orchestration layer (Temporal-specific)
├── workflows.ts # Workflow orchestration
├── activities.ts # Thin activity wrappers
└── worker.ts # Temporal worker
src/services/ # Business logic layer (Temporal-agnostic)
├── agent-execution.ts # Agent execution logic
├── config-loader.ts # Configuration loading
├── prompt-manager.ts # Prompt template management
├── error-handling.ts # Error classification
└── container.ts # Dependency injection
Benefits
Testability : Services can be unit tested without Temporal
Modularity : Business logic is reusable outside workflows
Portability : Could swap Temporal for another orchestrator
Clarity : Separation of concerns between orchestration and logic
Implementation
Activities are thin wrappers:
src/temporal/activities.ts
export async function runInjectionVulnAgent (
input : ActivityInput
) : Promise < AgentMetrics > {
// 1. Initialize services
const logger = new TemporalActivityLogger ( 'injection-vuln' );
const container = createServiceContainer ( input . repoPath );
const agentService = container . get ( 'agentExecution' );
// 2. Heartbeat for Temporal
const heartbeatInterval = setInterval (() => {
Context . current (). heartbeat ();
}, 30_000 );
try {
// 3. Delegate to service
const result = await agentService . execute ( 'injection-vuln' , input , logger );
if ( ! result . ok ) {
throw result . error ; // Service error → Temporal error
}
return result . value ;
} finally {
clearInterval ( heartbeatInterval );
}
}
Activities should only handle: heartbeats, error classification, and service delegation. All business logic belongs in src/services/.
Dependency Injection Pattern
Services are wired together via a DI container for testability and modularity.
Container
src/services/container.ts
export function createServiceContainer ( repoPath : string ) : ServiceContainer {
const configLoader = new ConfigLoader ( repoPath );
const promptManager = new PromptManager ();
const errorHandler = new ErrorHandler ();
const agentExecution = new AgentExecutionService (
configLoader ,
promptManager ,
errorHandler
);
return {
get ( serviceName : ServiceName ) {
switch ( serviceName ) {
case 'agentExecution' : return agentExecution ;
case 'configLoader' : return configLoader ;
case 'promptManager' : return promptManager ;
default : throw new Error ( `Unknown service: ${ serviceName } ` );
}
}
};
}
Benefits
Testability : Mock dependencies in unit tests
Flexibility : Swap implementations without changing consumers
Lifecycle : Single source of truth for service instances
Type safety : ServiceName enum ensures valid lookups
Exclusion: AuditSession
AuditSession is not in the DI container because:
Per-agent lifecycle (not per-workflow)
Mutex for parallel safety
Created inline in activities
src/temporal/activities.ts
const auditSession = new AuditSession ( input . sessionId , agentName );
await auditSession . logAgentStart ();
Additional Patterns
Factory Functions
function createVulnValidator ( vulnType : VulnType ) : AgentValidator {
return async ( sourceDir : string , logger : ActivityLogger ) : Promise < boolean > => {
try {
await validateQueueAndDeliverable ( vulnType , sourceDir );
return true ;
} catch ( error ) {
logger . warn ( `Queue validation failed for ${ vulnType } : ${ error . message } ` );
return false ;
}
};
}
Single Source of Truth
The AGENTS registry is the single source of truth for agent configuration:
export const AGENTS : Readonly < Record < AgentName , AgentDefinition >> = Object . freeze ({ ... });
Derived data structures reference this registry:
AGENT_PHASE_MAP: Maps agents to phases
AGENT_VALIDATORS: Maps agents to validation functions
MCP_AGENT_MAPPING: Maps prompts to MCP servers
Immutability
export const AGENTS : Readonly < Record < AgentName , AgentDefinition >> = Object . freeze ({ ... });
Object.freeze(): Prevents runtime mutations
Readonly<>: TypeScript compile-time immutability
const: Prevents reassignment
Best Practices
Single Responsibility Each module/service/agent should have one clear purpose
Explicit Over Implicit Use explicit types (Result, ErrorCode) over implicit behavior (exceptions)
Boundary Enforcement Keep orchestration (Temporal) separate from business logic (services)
Immutable Data Prefer readonly data structures for configuration and constants
Next Steps
Code Style Follow Shannon’s coding conventions
Adding Agents Apply patterns when creating new agents
Error Handling Complete error handling reference
Architecture Overview High-level system architecture