mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-26 14:30:51 +02:00
feat(worker): migrate agent runtime from Claude Agent SDK to pi harness (#389)
* feat(worker): migrate agent runtime from Claude Agent SDK to pi harness * feat: remove Google Vertex AI provider support * fix(worker): route Bedrock and custom-base-URL providers from env * feat(prompts): instruct agents to call submit_exploitation_queue and submit_auth_result * fix(worker): count sub-agent cost and surface compaction failures * refactor(worker): rename claude-executor to pi-executor * feat(worker): pi-event-driven output formatting * fix(worker): gate adaptive thinking to Opus models, drop CLAUDE_THINKING_LEVEL * fix(worker): restore minLength/minItems on vuln-collector schemas * feat(worker): give task sub-agent write+bash, align tool descriptions * feat(worker): add glob custom tool and route code_path globs to it * refactor(prompts): use pi tool names (task, todo_write, read, bash, glob) * refactor(prompts): drop stale MCP terminology for collector tools * refactor(prompts): drop collector server names from deliverable instructions * fix(worker): restore minLength/minItems on pre-recon and exploit collector schemas * feat(worker): load playwright-cli skill via pi resource loader * refactor(cli): remove CLAUDE_CODE_MAX_OUTPUT_TOKENS config * build: drop @anthropic-ai/claude-code from worker image * docs: remove vertex references from llms context * docs(worker): update stale sdk comments * refactor(worker): unify provider precedence between preflight and executor * feat(worker): enforce bounded bash timeouts via pi extension * ci: bump the beta release line to 2.0.0 (#356) * fix(cli): pin npx command hints to beta tag * fix: render agent deliverables before the success commit so resume preserves them (#377) * feat(cli): restructure run folder and improve terminal UX (#383) * feat: surface report at run root and nest run internals under .shannon * feat: use plain-language wording in user-facing terminal messages * feat(cli): guide users to watch scan progress and surface report path on start * docs: sync run-folder layout and CLI wording across docs and comments * feat(cli): add version command reporting package version or git SHA * feat(cli): detect TTY for interactive prompts, color, and progress output * docs: document --yes flag, version command, and tty module * fix(cli): FORCE_COLOR precedence and plain uninstall --yes output * fix(cli): respect empty NO_COLOR * fix(cli): let NO_COLOR take precedence over FORCE_COLOR * docs: mark claude-code-router integration as removed * refactor(worker): converge shared core with shannon-oss (#388) * fix(worker): port keygraph shared-core correctness fixes * refactor(worker): adopt collectors/ and ai/pi/ layout; add task budget cap and cancellation * refactor(worker): drop inconsistent Collector "Server" suffix * refactor(worker): drop unused providerConfig/apiKey seams, resolve credentials from env only * refactor(worker): port oss code_path pattern expansion + external_directory allow * fix(worker): preserve dotfile paths in code_path avoid patterns (.env no longer stripped to env) * feat(worker): render Unprocessed Vulnerabilities section in exploit deliverable (align with oss) * feat(worker): request set_blind_spots for all vuln classes (align auth/ssrf with production prompts) * refactor(worker): adopt unified permissionSystem* naming and helper layout * refactor(worker): inline blind_spots into vuln deliverable section array * chore(worker): drop unused zod dependency (tree is typebox-native) * fix(worker): normalize base32 TOTP secret to accept padding and whitespace * refactor(worker): adopt shared toolResult helper and flatSchema naming in collectors * refactor(worker): use undefined over null in queue-schema builders * docs(worker): converge renderer/collector doc comments to current pi terminology * refactor(worker): adopt schema.ts cleanInput/stringEnum helpers in collectors * feat(worker): converge exploit-collector/renderer with vendored; capture and render overview for blocked findings * refactor(worker): converge session-tools/pipeline/exploitation-checker with vendored * refactor(worker): converge task-tool usage reporting with vendored onUsage callback * refactor(worker): converge structured output onto a submitTool executor channel * docs(worker): expand exploit-renderer docstring to match shannon-oss * docs(worker): adopt richer vuln-renderer docstring from shannon-oss * docs(worker): neutralize billing-detection wording for shannon-oss parity * fix(worker): verify checkpoint hash in the deliverables clone being reset * fix(worker): fail fast on malformed exploitation queue JSON * fix(worker): honor retryable flag when classifying exploitation-queue check failures * fix(worker): fail fast on corrupted session.json in run-scope validation * feat(worker): propagate Temporal cancellation signal into agent and auth pi sessions * fix(worker): mark exploit agent complete when exploitation is skipped so resume skips it * prompts: drop scan description from executive report prompt * refactor(worker): add createGenericSubmitTool for raw JSON-schema submit tools * refactor(worker): gate playwright-cli skill to browser agents via skillsOverride (adopt shannon-oss mechanism) * docs(worker): correct formatLogTime comment to UTC to match toISOString * refactor(worker): converge queue-schemas with shannon-oss (guarded count, decl order) * refactor(worker): converge task-tool with shannon-oss (byte-identical; modelRegistry optional) * fix(worker): use replaceLiteral for all prompt value insertions to prevent $-mangling * fix(worker): classify agent execution failures by error type instead of hardcoding validation * fix(worker): cap auth-failure detail at 250 chars to match shannon-oss * style(worker): apply biome formatting * refactor(worker): remove per-session task delegation cap from task tool * style(cli): collapse usage hint now that the beta tag is gone * chore: mark the pi harness migration as a breaking change BREAKING CHANGE: Google Vertex AI is no longer a supported provider. The CLAUDE_CODE_USE_VERTEX, ANTHROPIC_VERTEX_PROJECT, CLOUD_ML_REGION, and GOOGLE_APPLICATION_CREDENTIALS environment variables, along with the use_vertex, vertex_project, and cloud_ml_region config.toml keys, are removed. Vertex users must switch to Anthropic, AWS Bedrock, or a custom Anthropic-compatible base URL. The CLAUDE_CODE_MAX_OUTPUT_TOKENS environment variable and the max_output_tokens config.toml key are also removed.
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
* then persists everything to ~/.shannon/config.toml with 0o600 permissions.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import * as p from '@clack/prompts';
|
||||
@@ -14,7 +13,7 @@ import { requireInteractive } from '../tty.js';
|
||||
|
||||
const SHANNON_HOME = path.join(os.homedir(), '.shannon');
|
||||
|
||||
type Provider = 'anthropic' | 'custom_base_url' | 'bedrock' | 'vertex';
|
||||
type Provider = 'anthropic' | 'custom_base_url' | 'bedrock';
|
||||
|
||||
export async function setup(): Promise<void> {
|
||||
requireInteractive('setup', 'For non-interactive use, export credentials as env vars (e.g. ANTHROPIC_API_KEY).');
|
||||
@@ -27,7 +26,6 @@ export async function setup(): Promise<void> {
|
||||
{ value: 'anthropic' as const, label: 'Claude Direct', hint: 'recommended' },
|
||||
{ value: 'custom_base_url' as const, label: 'Custom Base URL', hint: 'proxies, gateways' },
|
||||
{ value: 'bedrock' as const, label: 'Claude via AWS Bedrock' },
|
||||
{ value: 'vertex' as const, label: 'Claude via Google Vertex AI' },
|
||||
],
|
||||
});
|
||||
if (p.isCancel(provider)) return cancelAndExit();
|
||||
@@ -53,8 +51,6 @@ async function setupProvider(provider: Provider): Promise<ShannonConfig> {
|
||||
return setupCustomBaseUrl();
|
||||
case 'bedrock':
|
||||
return setupBedrock();
|
||||
case 'vertex':
|
||||
return setupVertex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,75 +211,6 @@ async function setupBedrock(): Promise<ShannonConfig> {
|
||||
};
|
||||
}
|
||||
|
||||
async function setupVertex(): Promise<ShannonConfig> {
|
||||
// 1. Collect region and project ID
|
||||
const region = await p.text({
|
||||
message: 'Google Cloud region',
|
||||
placeholder: 'us-east5',
|
||||
validate: required('Region is required'),
|
||||
});
|
||||
if (p.isCancel(region)) return cancelAndExit();
|
||||
|
||||
const projectId = await p.text({
|
||||
message: 'GCP Project ID',
|
||||
validate: required('Project ID is required'),
|
||||
});
|
||||
if (p.isCancel(projectId)) return cancelAndExit();
|
||||
|
||||
// 2. File picker for service account key
|
||||
p.log.info('Select the path to your GCP Service Account JSON key file.');
|
||||
const keySourcePath = await p.path({
|
||||
message: 'Service Account JSON key file',
|
||||
validate: (value) => {
|
||||
if (!value) return 'Path is required';
|
||||
if (!fs.existsSync(value)) return 'File not found';
|
||||
if (!value.endsWith('.json')) return 'Must be a .json file';
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(keySourcePath)) return cancelAndExit();
|
||||
|
||||
// 3. Copy key to ~/.shannon/ and lock permissions
|
||||
const destPath = path.join(SHANNON_HOME, 'google-sa-key.json');
|
||||
fs.mkdirSync(SHANNON_HOME, { recursive: true });
|
||||
fs.copyFileSync(keySourcePath, destPath);
|
||||
fs.chmodSync(destPath, 0o600);
|
||||
p.log.success(`Key copied to ${destPath} (permissions: 0600)`);
|
||||
|
||||
// 4. Model tiers
|
||||
const models = await p.group({
|
||||
small: () =>
|
||||
p.text({
|
||||
message: 'Small model ID',
|
||||
placeholder: 'claude-haiku-4-5@20251001',
|
||||
validate: required('Small model ID is required'),
|
||||
}),
|
||||
medium: () =>
|
||||
p.text({
|
||||
message: 'Medium model ID',
|
||||
placeholder: 'claude-sonnet-4-6',
|
||||
validate: required('Medium model ID is required'),
|
||||
}),
|
||||
large: () =>
|
||||
p.text({
|
||||
message: 'Large model ID',
|
||||
placeholder: 'claude-opus-4-8',
|
||||
validate: required('Large model ID is required'),
|
||||
}),
|
||||
});
|
||||
if (p.isCancel(models)) return cancelAndExit();
|
||||
|
||||
return {
|
||||
vertex: {
|
||||
use: true,
|
||||
region,
|
||||
project_id: projectId,
|
||||
key_path: destPath,
|
||||
},
|
||||
models: { small: models.small, medium: models.medium, large: models.large },
|
||||
};
|
||||
}
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
async function maybePromptAdaptiveThinking(config: ShannonConfig): Promise<void> {
|
||||
|
||||
@@ -10,7 +10,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ensureImage, ensureInfra, randomSuffix, spawnWorker } from '../docker.js';
|
||||
import { buildEnvFlags, loadEnv, validateCredentials } from '../env.js';
|
||||
import { getCredentialsPath, getWorkspacesDir, initHome } from '../home.js';
|
||||
import { getWorkspacesDir, initHome } from '../home.js';
|
||||
import { isLocal } from '../mode.js';
|
||||
import { FINAL_REPORT_FILENAME, INTERNAL_DIR, resolveConfig, resolveRepo, resolveRunFile } from '../paths.js';
|
||||
import { displaySplash } from '../splash.js';
|
||||
@@ -107,13 +107,6 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
}
|
||||
fs.mkdirSync(path.join(repo.hostPath, '.playwright'), { recursive: true });
|
||||
|
||||
const credentialsPath = getCredentialsPath();
|
||||
const hasCredentials = fs.existsSync(credentialsPath);
|
||||
|
||||
if (hasCredentials) {
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = '/app/credentials/google-sa-key.json';
|
||||
}
|
||||
|
||||
// 10. Resolve output directory
|
||||
const outputDir = args.output ? path.resolve(args.output) : undefined;
|
||||
if (outputDir) {
|
||||
@@ -136,7 +129,6 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
containerName,
|
||||
envFlags: buildEnvFlags(),
|
||||
...(config && { config }),
|
||||
...(hasCredentials && { credentials: credentialsPath }),
|
||||
...(promptsDir && { promptsDir }),
|
||||
...(outputDir && { outputDir }),
|
||||
workspace,
|
||||
|
||||
@@ -24,7 +24,6 @@ interface ConfigMapping {
|
||||
/** Maps every supported env var to its TOML path (section.key) and expected type. */
|
||||
const CONFIG_MAP: readonly ConfigMapping[] = [
|
||||
// Core
|
||||
{ env: 'CLAUDE_CODE_MAX_OUTPUT_TOKENS', toml: 'core.max_tokens', type: 'number' },
|
||||
{ env: 'CLAUDE_ADAPTIVE_THINKING', toml: 'core.adaptive_thinking', type: 'boolean', boolFormat: 'literal' },
|
||||
|
||||
// Anthropic
|
||||
@@ -36,12 +35,6 @@ const CONFIG_MAP: readonly ConfigMapping[] = [
|
||||
{ env: 'AWS_REGION', toml: 'bedrock.region', type: 'string' },
|
||||
{ env: 'AWS_BEARER_TOKEN_BEDROCK', toml: 'bedrock.token', type: 'string' },
|
||||
|
||||
// Vertex
|
||||
{ env: 'CLAUDE_CODE_USE_VERTEX', toml: 'vertex.use', type: 'boolean' },
|
||||
{ env: 'CLOUD_ML_REGION', toml: 'vertex.region', type: 'string' },
|
||||
{ env: 'ANTHROPIC_VERTEX_PROJECT_ID', toml: 'vertex.project_id', type: 'string' },
|
||||
{ env: 'GOOGLE_APPLICATION_CREDENTIALS', toml: 'vertex.key_path', type: 'string' },
|
||||
|
||||
// Custom Base URL
|
||||
{ env: 'ANTHROPIC_BASE_URL', toml: 'custom_base_url.base_url', type: 'string' },
|
||||
{ env: 'ANTHROPIC_AUTH_TOKEN', toml: 'custom_base_url.auth_token', type: 'string' },
|
||||
@@ -156,20 +149,10 @@ function validateProviderFields(config: TOMLConfig, provider: string, errors: st
|
||||
validateModelTiers(config, 'bedrock', errors);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'vertex': {
|
||||
const required = ['use', 'region', 'project_id', 'key_path'];
|
||||
const missing = required.filter((k) => !keys.includes(k));
|
||||
if (missing.length > 0) {
|
||||
errors.push(`[vertex] missing required keys: ${missing.join(', ')}`);
|
||||
}
|
||||
validateModelTiers(config, 'vertex', errors);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Bedrock and Vertex require a [models] section with all three tiers. */
|
||||
/** Bedrock requires a [models] section with all three tiers. */
|
||||
function validateModelTiers(config: TOMLConfig, provider: string, errors: string[]): void {
|
||||
const models = config.models as Record<string, unknown> | undefined;
|
||||
if (!models || typeof models !== 'object') {
|
||||
@@ -229,7 +212,7 @@ function validateConfig(config: TOMLConfig): string[] {
|
||||
}
|
||||
|
||||
// 4. Only one provider section allowed (ignore empty sections)
|
||||
const PROVIDER_SECTIONS = ['anthropic', 'custom_base_url', 'bedrock', 'vertex'] as const;
|
||||
const PROVIDER_SECTIONS = ['anthropic', 'custom_base_url', 'bedrock'] as const;
|
||||
const present = PROVIDER_SECTIONS.filter((s) => {
|
||||
const section = config[s];
|
||||
return section && typeof section === 'object' && Object.keys(section).length > 0;
|
||||
|
||||
@@ -8,11 +8,10 @@ import { getConfigFile } from '../home.js';
|
||||
// === Types ===
|
||||
|
||||
export interface ShannonConfig {
|
||||
core?: { max_tokens?: number; adaptive_thinking?: boolean };
|
||||
core?: { adaptive_thinking?: boolean };
|
||||
anthropic?: { api_key?: string; oauth_token?: string };
|
||||
custom_base_url?: { base_url?: string; auth_token?: string };
|
||||
bedrock?: { use?: boolean; region?: string; token?: string };
|
||||
vertex?: { use?: boolean; region?: string; project_id?: string; key_path?: string };
|
||||
models?: { small?: string; medium?: string; large?: string };
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,6 @@ export interface WorkerOptions {
|
||||
containerName: string;
|
||||
envFlags: string[];
|
||||
config?: { hostPath: string; containerPath: string };
|
||||
credentials?: string;
|
||||
promptsDir?: string;
|
||||
outputDir?: string;
|
||||
workspace: string;
|
||||
@@ -293,11 +292,6 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
args.push('-v', `${opts.outputDir}:/app/output`);
|
||||
}
|
||||
|
||||
// Mount credentials file to fixed container path
|
||||
if (opts.credentials) {
|
||||
args.push('-v', `${opts.credentials}:/app/credentials/google-sa-key.json:ro`);
|
||||
}
|
||||
|
||||
// Environment
|
||||
args.push(...opts.envFlags);
|
||||
|
||||
|
||||
+1
-30
@@ -18,14 +18,9 @@ const FORWARD_VARS = [
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'AWS_REGION',
|
||||
'AWS_BEARER_TOKEN_BEDROCK',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'CLOUD_ML_REGION',
|
||||
'ANTHROPIC_VERTEX_PROJECT_ID',
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
'ANTHROPIC_SMALL_MODEL',
|
||||
'ANTHROPIC_MEDIUM_MODEL',
|
||||
'ANTHROPIC_LARGE_MODEL',
|
||||
'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
|
||||
'CLAUDE_ADAPTIVE_THINKING',
|
||||
] as const;
|
||||
|
||||
@@ -62,7 +57,7 @@ export function buildEnvFlags(): string[] {
|
||||
interface CredentialValidation {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
mode: 'api-key' | 'oauth' | 'custom-base-url' | 'bedrock' | 'vertex';
|
||||
mode: 'api-key' | 'oauth' | 'custom-base-url' | 'bedrock';
|
||||
}
|
||||
|
||||
/** Check if a custom Anthropic-compatible base URL is configured. */
|
||||
@@ -77,7 +72,6 @@ function detectProviders(): string[] {
|
||||
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) providers.push('Anthropic OAuth');
|
||||
if (isCustomBaseUrlConfigured()) providers.push('Custom Base URL');
|
||||
if (process.env.CLAUDE_CODE_USE_BEDROCK === '1') providers.push('AWS Bedrock');
|
||||
if (process.env.CLAUDE_CODE_USE_VERTEX === '1') providers.push('Google Vertex');
|
||||
return providers;
|
||||
}
|
||||
|
||||
@@ -120,29 +114,6 @@ export function validateCredentials(): CredentialValidation {
|
||||
}
|
||||
return { valid: true, mode: 'bedrock' };
|
||||
}
|
||||
if (process.env.CLAUDE_CODE_USE_VERTEX === '1') {
|
||||
const missing: string[] = [];
|
||||
if (!process.env.CLOUD_ML_REGION) missing.push('CLOUD_ML_REGION');
|
||||
if (!process.env.ANTHROPIC_VERTEX_PROJECT_ID) missing.push('ANTHROPIC_VERTEX_PROJECT_ID');
|
||||
if (!process.env.ANTHROPIC_SMALL_MODEL) missing.push('ANTHROPIC_SMALL_MODEL');
|
||||
if (!process.env.ANTHROPIC_MEDIUM_MODEL) missing.push('ANTHROPIC_MEDIUM_MODEL');
|
||||
if (!process.env.ANTHROPIC_LARGE_MODEL) missing.push('ANTHROPIC_LARGE_MODEL');
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'vertex',
|
||||
error: `Vertex AI mode requires: ${missing.join(', ')}`,
|
||||
};
|
||||
}
|
||||
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'vertex',
|
||||
error: 'Vertex AI mode requires GOOGLE_APPLICATION_CREDENTIALS',
|
||||
};
|
||||
}
|
||||
return { valid: true, mode: 'vertex' };
|
||||
}
|
||||
|
||||
const hint =
|
||||
getMode() === 'local'
|
||||
|
||||
+2
-20
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Shannon state directory management.
|
||||
*
|
||||
* Local mode (cloned repo): uses ./workspaces/, ./credentials/
|
||||
* Local mode (cloned repo): uses ./workspaces/
|
||||
* NPX mode: uses ~/.shannon/workspaces/, ~/.shannon/
|
||||
*/
|
||||
|
||||
@@ -20,32 +20,14 @@ export function getWorkspacesDir(): string {
|
||||
return getMode() === 'local' ? path.resolve('workspaces') : path.join(SHANNON_HOME, 'workspaces');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Vertex credentials file path.
|
||||
*
|
||||
* Checks GOOGLE_APPLICATION_CREDENTIALS env var first (may be set by TOML resolver),
|
||||
* then falls back to mode-appropriate default location.
|
||||
*/
|
||||
export function getCredentialsPath(): string {
|
||||
const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
if (envPath && fs.existsSync(envPath)) return path.resolve(envPath);
|
||||
|
||||
if (getMode() === 'local') {
|
||||
return path.resolve('credentials', 'google-sa-key.json');
|
||||
}
|
||||
|
||||
return path.join(SHANNON_HOME, 'google-sa-key.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize state directories.
|
||||
* Local mode: creates ./workspaces/ and ./credentials/
|
||||
* Local mode: creates ./workspaces/
|
||||
* NPX mode: creates ~/.shannon/workspaces/
|
||||
*/
|
||||
export function initHome(): void {
|
||||
if (getMode() === 'local') {
|
||||
fs.mkdirSync(path.resolve('workspaces'), { recursive: true });
|
||||
fs.mkdirSync(path.resolve('credentials'), { recursive: true });
|
||||
} else {
|
||||
fs.mkdirSync(path.join(SHANNON_HOME, 'workspaces'), { recursive: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user