backport: provider extensions and drop claude-code-router mode

Cherry-pick of KeygraphHQ/shannon#295 (581c208).

Upstream changes: removes router mode from CLI/worker, adds provider
extensions, new report-output-provider and checkpoint-provider interfaces,
refactored workflow orchestration.

Conflicts resolved: kept our README.md, CLAUDE.md, and deleted compose files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 13:32:23 -04:00
parent 59764717c1
commit c7be324083
27 changed files with 458 additions and 539 deletions
+32 -1
View File
@@ -22,6 +22,8 @@ import type { CheckpointProvider } from '../interfaces/checkpoint-provider.js';
import { NoOpCheckpointProvider } from '../interfaces/checkpoint-provider.js';
import type { FindingsProvider } from '../interfaces/findings-provider.js';
import { NoOpFindingsProvider } from '../interfaces/findings-provider.js';
import type { ReportOutputProvider } from '../interfaces/report-output-provider.js';
import { NoOpReportOutputProvider } from '../interfaces/report-output-provider.js';
import type { ContainerConfig } from '../types/config.js';
import { AgentExecutionService } from './agent-execution.js';
import { ConfigLoaderService } from './config-loader.js';
@@ -40,6 +42,7 @@ export interface ContainerDependencies {
readonly config: ContainerConfig;
readonly findingsProvider?: FindingsProvider;
readonly checkpointProvider?: CheckpointProvider;
readonly reportOutputProvider?: ReportOutputProvider;
}
/**
@@ -59,6 +62,7 @@ export class Container {
readonly exploitationChecker: ExploitationCheckerService;
readonly findingsProvider: FindingsProvider;
readonly checkpointProvider: CheckpointProvider;
readonly reportOutputProvider: ReportOutputProvider;
constructor(deps: ContainerDependencies) {
this.sessionMetadata = deps.sessionMetadata;
@@ -72,6 +76,7 @@ export class Container {
// Wire providers with default no-ops when not provided
this.findingsProvider = deps.findingsProvider ?? new NoOpFindingsProvider();
this.checkpointProvider = deps.checkpointProvider ?? new NoOpCheckpointProvider();
this.reportOutputProvider = deps.reportOutputProvider ?? new NoOpReportOutputProvider();
}
}
@@ -87,6 +92,32 @@ const DEFAULT_CONFIG: ContainerConfig = {
auditDir: './workspaces',
};
/**
* Factory function for creating containers.
*
* Default: creates a plain Container with NoOp providers. Consumers can call
* setContainerFactory() at worker startup to inject custom provider
* implementations into every container.
*/
type ContainerFactory = (
workflowId: string,
sessionMetadata: SessionMetadata,
config: ContainerConfig,
) => Container;
let containerFactory: ContainerFactory = (_workflowId, sessionMetadata, config) =>
new Container({ sessionMetadata, config });
/**
* Override the default container factory.
*
* Call once at worker startup to inject providers into all containers
* created during the worker's lifetime.
*/
export function setContainerFactory(factory: ContainerFactory): void {
containerFactory = factory;
}
/**
* Get or create a Container for a workflow.
*
@@ -106,7 +137,7 @@ export function getOrCreateContainer(
let container = containers.get(workflowId);
if (!container) {
container = new Container({ sessionMetadata, config });
container = containerFactory(workflowId, sessionMetadata, config);
containers.set(workflowId, container);
}
+3 -1
View File
@@ -16,7 +16,9 @@ export { AgentExecutionService } from './agent-execution.js';
export { ConfigLoaderService } from './config-loader.js';
export type { ContainerDependencies } from './container.js';
export { Container, getContainer, getOrCreateContainer, removeContainer } from './container.js';
export { Container, getContainer, getOrCreateContainer, removeContainer, setContainerFactory } from './container.js';
export { ExploitationCheckerService } from './exploitation-checker.js';
export { loadPrompt } from './prompt-manager.js';
export { assembleFinalReport, injectModelIntoReport } from './reporting.js';
export type { ClaudePromptResult } from '../ai/claude-executor.js';
export { runClaudePrompt } from '../ai/claude-executor.js';
+2 -2
View File
@@ -14,7 +14,7 @@
* Checks run sequentially, cheapest first:
* 1. Repository path exists and contains .git
* 2. Config file parses and validates (if provided)
* 3. Credentials validate via Claude Agent SDK query (API key, OAuth, Bedrock, Vertex AI, or router mode)
* 3. Credentials validate via Claude Agent SDK query (API key, OAuth, Bedrock, or Vertex AI)
* 4. Target URL is reachable from the container (DNS + HTTP)
*/
@@ -473,7 +473,7 @@ async function validateTargetUrl(targetUrl: string, logger: ActivityLogger): Pro
*
* 1. Repository path exists and contains .git
* 2. Config file parses and validates (if configPath provided)
* 3. Credentials validate (API key, OAuth, or router mode)
* 3. Credentials validate (API key, OAuth, Bedrock, or Vertex AI)
* 4. Target URL is reachable from the container
*
* Returns on first failure.
+9 -4
View File
@@ -17,7 +17,11 @@ interface DeliverableFile {
}
// Pure function: Assemble final report from specialist deliverables
export async function assembleFinalReport(sourceDir: string, logger: ActivityLogger): Promise<string> {
export async function assembleFinalReport(
sourceDir: string,
deliverablesSubdir: string | undefined,
logger: ActivityLogger,
): Promise<string> {
const deliverableFiles: DeliverableFile[] = [
{ name: 'Injection', path: 'injection_exploitation_evidence.md', required: false },
{ name: 'XSS', path: 'xss_exploitation_evidence.md', required: false },
@@ -29,7 +33,7 @@ export async function assembleFinalReport(sourceDir: string, logger: ActivityLog
const sections: string[] = [];
for (const file of deliverableFiles) {
const filePath = path.join(deliverablesDir(sourceDir), file.path);
const filePath = path.join(deliverablesDir(sourceDir, deliverablesSubdir), file.path);
try {
if (await fs.pathExists(filePath)) {
const content = await fs.readFile(filePath, 'utf8');
@@ -56,7 +60,7 @@ export async function assembleFinalReport(sourceDir: string, logger: ActivityLog
}
const finalContent = sections.join('\n\n');
const outputDir = deliverablesDir(sourceDir);
const outputDir = deliverablesDir(sourceDir, deliverablesSubdir);
const finalReportPath = path.join(outputDir, 'comprehensive_security_assessment_report.md');
try {
@@ -82,6 +86,7 @@ export async function assembleFinalReport(sourceDir: string, logger: ActivityLog
*/
export async function injectModelIntoReport(
repoPath: string,
deliverablesSubdir: string | undefined,
outputPath: string,
logger: ActivityLogger,
): Promise<void> {
@@ -118,7 +123,7 @@ export async function injectModelIntoReport(
logger.info(`Injecting model info into report: ${modelStr}`);
// 3. Read the final report
const reportPath = path.join(deliverablesDir(repoPath), 'comprehensive_security_assessment_report.md');
const reportPath = path.join(deliverablesDir(repoPath, deliverablesSubdir), 'comprehensive_security_assessment_report.md');
if (!(await fs.pathExists(reportPath))) {
logger.warn('Final report not found, skipping model injection');