Skip to main content

Overview

Shannon uses Temporal for workflow orchestration, providing production-grade durability, crash recovery, and observability. Temporal ensures that even if Shannon crashes or is interrupted, the pentest can resume from where it left off without re-running completed agents.

Why Temporal?

Penetration tests can run for 1-2 hours and cost $30-50 in API fees. Traditional orchestration approaches would force you to start over from scratch if anything goes wrong. Temporal solves this through:

Durable Execution

Workflow state is persisted to a database. If the worker crashes, it can resume from the last checkpoint.

Automatic Retries

Transient errors (rate limits, network issues) are automatically retried with exponential backoff.

Queryable State

Check workflow progress in real-time without waiting for completion.

Event History

Complete audit trail of every step taken, enabling debugging and compliance.

Architecture

Core Concepts

Workflows

Workflows contain the orchestration logic - what order to run agents, how to handle failures, when to run in parallel. File: src/temporal/workflows.ts
export async function pentestPipelineWorkflow(
  input: PipelineInput
): Promise<PipelineState> {
  const state: PipelineState = {
    status: 'running',
    currentPhase: null,
    currentAgent: null,
    completedAgents: [],
    failedAgent: null,
    error: null,
    startTime: Date.now(),
    agentMetrics: {},
    summary: null,
  };
  
  // Enable progress queries
  setHandler(getProgress, (): PipelineProgress => ({
    ...state,
    workflowId,
    elapsedMs: Date.now() - state.startTime,
  }));
  
  // Execute phases
  await runSequentialPhase('pre-recon', 'pre-recon', a.runPreReconAgent);
  await runSequentialPhase('recon', 'recon', a.runReconAgent);
  
  const pipelineResults = await runWithConcurrencyLimit(pipelineThunks, maxConcurrent);
  
  await runSequentialPhase('reporting', 'report', a.runReportAgent);
  
  state.status = 'completed';
  return state;
}
Workflows must be deterministic - they can’t make random choices or call external APIs directly. That’s why actual work happens in Activities.

Activities

Activities perform non-deterministic work like API calls, file I/O, or running agents. They can fail and be retried. File: src/temporal/activities.ts
export async function runReconAgent(
  input: ActivityInput
): Promise<AgentMetrics> {
  // Delegate to service layer
  return await executeAgent({
    agentName: 'recon',
    activityInput: input,
  });
}

export async function runInjectionVulnAgent(
  input: ActivityInput  
): Promise<AgentMetrics> {
  return await executeAgent({
    agentName: 'injection-vuln',
    activityInput: input,
  });
}
Activities are thin wrappers that:
  • Provide heartbeat signals (“I’m still alive”)
  • Classify errors as retryable vs non-retryable
  • Delegate actual work to services

Queries

Queries allow you to inspect workflow state without waiting for completion:
// Define query in workflow
setHandler(getProgress, (): PipelineProgress => ({
  ...state,
  workflowId,
  elapsedMs: Date.now() - state.startTime,
}));
# Query from CLI
./shannon query ID=shannon-1234567890
Output:
{
  "status": "running",
  "currentPhase": "vulnerability-exploitation",
  "currentAgent": "pipelines",
  "completedAgents": ["pre-recon", "recon"],
  "elapsedMs": 1847293,
  "agentMetrics": {
    "pre-recon": { "costUsd": 2.45, "durationMs": 892341 },
    "recon": { "costUsd": 1.87, "durationMs": 954952 }
  }
}

Retry Strategies

Shannon implements intelligent retry logic with three presets:
Optimized for production API usage with generous retry windows:
const PRODUCTION_RETRY = {
  initialInterval: '5 minutes',
  maximumInterval: '30 minutes',
  backoffCoefficient: 2,
  maximumAttempts: 50,
  nonRetryableErrorTypes: [
    'AuthenticationError',
    'PermissionError',
    'InvalidRequestError',
    'RequestTooLargeError',
    'ConfigurationError',
    'InvalidTargetError',
    'ExecutionLimitError',
  ],
};
Timeline:
  • Attempt 1: Immediate
  • Attempt 2: +5 minutes
  • Attempt 3: +10 minutes
  • Attempt 4: +20 minutes
  • Attempt 5: +30 minutes
  • … up to 50 attempts
Use for: Normal pentests

Non-Retryable Errors

Some errors should never be retried because they’re permanent:
  • AuthenticationError: Invalid API key
  • PermissionError: Insufficient permissions
  • InvalidRequestError: Malformed request
  • RequestTooLargeError: Prompt exceeds model limits
  • ConfigurationError: Invalid config file
  • InvalidTargetError: Target URL unreachable
  • ExecutionLimitError: Hit model context window limit
These cause immediate workflow failure.

Crash Recovery

Temporal’s durable execution means Shannon can recover from crashes:

Scenario 1: Worker Crash

If the worker container crashes mid-pentest:
1

Crash Detected

Temporal Server notices worker heartbeat stopped
2

Activity Timeout

After heartbeat timeout (60 minutes), activity is marked as failed
3

Retry Scheduled

Temporal schedules retry according to retry policy (5 min initial interval)
4

Worker Restarts

When worker comes back online, it picks up the scheduled retry
5

Resume Execution

Activity restarts from the beginning, but completed agents are already checkpointed

Scenario 2: Network Interruption

If network connection to Anthropic API is lost:
  1. Activity throws retryable error
  2. Temporal automatically retries with exponential backoff
  3. When network recovers, activity completes successfully

Scenario 3: API Rate Limit

If you hit Anthropic rate limits:
  1. Error classified as BillingError (retryable)
  2. Temporal backs off for 5-30 minutes (production) or 5-6 hours (subscription)
  3. Once rate limit resets, activity resumes

Workspace Resume

Shannon extends Temporal’s crash recovery with workspace resume, allowing you to manually resume interrupted pentests:

How It Works

1

Git Checkpoints

After each agent completes, Shannon commits deliverables to git:
git add deliverables/recon_deliverable.md
git commit -m "Agent: recon - completed successfully"
2

Session Metadata

Completed agents tracked in audit-logs/{workspace}/session.json:
{
  "sessionId": "example-com_shannon-1234",
  "workflowId": "shannon-1234567890",
  "completedAgents": ["pre-recon", "recon"],
  "checkpointHash": "a1b2c3d4e5f6"
}
3

Resume Command

To resume an interrupted run:
./shannon start URL=https://app.example.com REPO=my-repo \
  WORKSPACE=example-com_shannon-1234
4

State Restoration

Shannon:
  • Loads session.json to see which agents completed
  • Restores git workspace to checkpoint
  • Cleans up any incomplete deliverables
  • Starts new workflow, skipping completed agents
5

Continue Execution

Pentest continues from where it left off:
const shouldSkip = (agentName: string): boolean => {
  return resumeState?.completedAgents.includes(agentName) ?? false;
};

if (!shouldSkip('injection-vuln')) {
  state.agentMetrics['injection-vuln'] = await a.runInjectionVulnAgent(...);
} else {
  log.info('Skipping injection-vuln (already complete)');
}

Resume Example

# Start a pentest
./shannon start URL=https://example.com REPO=my-app WORKSPACE=audit-q1
# -> Workflow ID: shannon-1234567890
# -> Runs pre-recon, recon, starts vuln analysis...
# -> Interrupted! (Ctrl+C, crash, etc.)

# List workspaces to find the ID
./shannon workspaces
# Output:
# example-com_audit-q1 (2 of 13 agents complete)

# Resume from where it left off
./shannon start URL=https://example.com REPO=my-app WORKSPACE=audit-q1
# -> Skips pre-recon and recon
# -> Continues with vuln analysis
The URL parameter must match the original workspace URL. Shannon rejects mismatched URLs to prevent cross-target contamination.

Observability

Temporal provides multiple ways to monitor Shannon:

Temporal Web UI

Access at http://localhost:8233:
Temporal Web UI
Features:
  • Real-time workflow status
  • Event history (complete audit trail)
  • Stack traces for failures
  • Query execution
  • Workflow search and filtering

CLI Tools

# View real-time logs
./shannon logs

# Query specific workflow
./shannon query ID=shannon-1234567890

# List all workspaces
./shannon workspaces

Audit Logs

Shannon maintains its own audit logs in audit-logs/{workspace}/:
audit-logs/example-com_shannon-1234/
├── session.json              # Session metadata and metrics
├── workflow.log              # Human-readable workflow log
├── agents/
│   ├── pre-recon.log        # Per-agent execution logs
│   ├── recon.log
│   └── injection-vuln.log
├── prompts/
│   ├── pre-recon.txt        # Prompt snapshots for reproducibility
│   └── recon.txt
└── deliverables/
    ├── code_analysis_deliverable.md
    └── recon_deliverable.md

Workflow State Machine

State Definitions:
  • Running: Workflow actively executing agents
  • Retry: Activity failed with retryable error, waiting for retry interval
  • Completed: All 13 agents completed successfully
  • Failed: Encountered non-retryable error or exceeded max retries
  • Terminated: User stopped workflow (Ctrl+C or ./shannon stop)

Temporal Client

The CLI interacts with Temporal through the client: File: src/temporal/client.ts
import { Connection, Client } from '@temporalio/client';

export async function startPentestWorkflow(
  input: PipelineInput
): Promise<string> {
  const connection = await Connection.connect({
    address: 'localhost:7233',
  });
  
  const client = new Client({ connection });
  
  const workflowId = `shannon-${Date.now()}`;
  
  const handle = await client.workflow.start(pentestPipelineWorkflow, {
    taskQueue: 'shannon-pentest',
    workflowId,
    args: [input],
  });
  
  return workflowId;
}

Worker Configuration

The worker polls for tasks and executes them: File: src/temporal/worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities.js';

async function run() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activities,
    taskQueue: 'shannon-pentest',
  });
  
  await worker.run();
}

run().catch((err) => {
  console.error(err);
  process.exit(1);
});

Data Flow

Best Practices

Named workspaces make resume easier:
# Good
./shannon start URL=... REPO=... WORKSPACE=q1-audit

# Harder to remember
./shannon start URL=... REPO=...
# -> Auto-named: example-com_shannon-1771007534808
Check progress periodically instead of waiting for completion:
# Terminal 1: Start pentest
./shannon start URL=... REPO=... WORKSPACE=my-audit

# Terminal 2: Monitor logs
./shannon logs

# Terminal 3: Query progress
watch -n 30 './shannon query ID=shannon-1234567890'
Match retry strategy to your Anthropic plan:Pay-as-you-go: Default (production) preset is fineSubscription plan: Use subscription preset
pipeline:
  retry_preset: subscription
Workspaces accumulate over time:
# List workspaces
./shannon workspaces

# Clean up old ones
rm -rf audit-logs/old-workspace-name

Troubleshooting

Symptoms: ./shannon start hangs or failsSolutions:
  1. Check if Temporal is running: docker compose ps
  2. Check Temporal health: curl http://localhost:8233
  3. View Temporal logs: docker compose logs temporal
  4. Restart Temporal: ./shannon stop && ./shannon start ...
Symptoms: Workflow shows as “Running” but no progressSolutions:
  1. Check worker logs: ./shannon logs
  2. Check worker status: docker compose ps worker
  3. Restart worker: docker compose restart worker
Symptoms: Resume starts agents from beginningSolutions:
  1. Verify workspace name: ./shannon workspaces
  2. Check session.json exists: cat audit-logs/{workspace}/session.json
  3. Verify URL matches: Compare URL in session.json with resume command
  4. Check git repository is clean: git status in target repo
Symptoms: Activity fails with “heartbeat timeout”Cause: Agent took longer than 60 minutes without sending heartbeatSolutions:
  1. Check if agent is actually stuck (logs)
  2. Increase heartbeat timeout in workflow config (not recommended)
  3. Resume workflow - it will retry the agent

Next Steps

Workspaces & Resume

Complete guide to workspace management and resume

Configuration

Configure retry strategies for your Anthropic plan

Troubleshooting

Common issues and solutions

Architecture

Understand Shannon’s overall architecture

Build docs developers (and LLMs) love