diff --git a/.env.example b/.env.example index 7101d14..b28c127 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,6 @@ # Shannon Environment Configuration # Copy this file to .env and fill in your credentials -# Recommended output token configuration for larger tool outputs -CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 - # Adaptive thinking is enabled automatically on Opus 4.6/4.7/4.8. Set to false to disable. # CLAUDE_ADAPTIVE_THINKING=false @@ -29,7 +26,7 @@ ANTHROPIC_API_KEY=your-api-key-here # Model Tier Overrides (Anthropic API / OAuth / Custom Base URL / Bedrock) # ============================================================================= # Override which model is used for each tier. Defaults are used if not set. -# Optional for direct Anthropic and custom base URL modes. Required for Bedrock/Vertex. +# Optional for direct Anthropic and custom base URL modes. Required for Bedrock. # ANTHROPIC_SMALL_MODEL=... # Small tier (default: claude-haiku-4-5-20251001) # ANTHROPIC_MEDIUM_MODEL=... # Medium tier (default: claude-sonnet-4-6) # ANTHROPIC_LARGE_MODEL=... # Large tier (default: claude-opus-4-8) @@ -47,20 +44,3 @@ ANTHROPIC_API_KEY=your-api-key-here # CLAUDE_CODE_USE_BEDROCK=1 # AWS_REGION=us-east-1 # AWS_BEARER_TOKEN_BEDROCK=your-bearer-token - -# ============================================================================= -# OPTION 4: Google Vertex AI -# ============================================================================= -# https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models -# Requires a GCP service account with roles/aiplatform.user. -# Download the SA key JSON from GCP Console (IAM > Service Accounts > Keys). -# Requires the model tier overrides above to be set with Vertex AI model IDs. -# Example Vertex AI model IDs: -# ANTHROPIC_SMALL_MODEL=claude-haiku-4-5@20251001 -# ANTHROPIC_MEDIUM_MODEL=claude-sonnet-4-6 -# ANTHROPIC_LARGE_MODEL=claude-opus-4-8 - -# CLAUDE_CODE_USE_VERTEX=1 -# CLOUD_ML_REGION=us-east5 -# ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id -# GOOGLE_APPLICATION_CREDENTIALS=./credentials/google-sa-key.json diff --git a/CLAUDE.md b/CLAUDE.md index f0b9017..2e76e58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -127,7 +127,7 @@ Infra (Temporal) runs via `docker-compose.yml`. Workers are ephemeral `docker ru - `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`) - `apps/worker/src/session-manager.ts` — Agent definitions (`AGENTS` record). Agent types in `apps/worker/src/types/agents.ts` - `apps/worker/src/config-parser.ts` — YAML config parsing with JSON Schema validation -- `apps/worker/src/ai/claude-executor.ts` — Claude Agent SDK integration with retry logic +- `apps/worker/src/ai/pi-executor.ts` — pi harness integration (retry disabled; Temporal owns retry) - `apps/worker/src/services/` — Business logic layer (Temporal-agnostic). Activities delegate here. Key: `agent-execution.ts`, `error-handling.ts`, `container.ts` - `apps/worker/src/types/` — Consolidated types: `Result`, `ErrorCode`, `AgentName`, `ActivityLogger`, etc. - `apps/worker/src/utils/` — Shared utilities (file I/O, formatting, concurrency) @@ -150,9 +150,9 @@ Durable workflow orchestration with crash recovery, queryable progress, intellig 5. **Reporting** (`report`) — Executive-level security report ### Supporting Systems -- **Configuration** — YAML configs in `apps/worker/configs/` with JSON Schema validation (`config-schema.json`). Supports auth settings (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), run-scope steering (`vuln_classes`, `exploit`), free-form `rules_of_engagement`, and post-hoc `report` filters (`min_severity`, `min_confidence`, `guidance`). `code_path` avoid rules are written into `~/.claude/settings.json` `permissions.deny` (`Read`/`Edit`) once per workflow by `apps/worker/src/temporal/activities.ts:syncCodePathDenyRules` so the SDK enforces them at the tool layer even in `bypassPermissions` mode. `vuln_classes`/`exploit` scope is locked into `session.json` on first run; resumes with a different scope fail fast (`persistOrValidateRunScope`). Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`) +- **Configuration** — YAML configs in `apps/worker/configs/` with JSON Schema validation (`config-schema.json`). Supports auth settings (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), run-scope steering (`vuln_classes`, `exploit`), free-form `rules_of_engagement`, and post-hoc `report` filters (`min_severity`, `min_confidence`, `guidance`). `code_path` avoid rules are enforced via the `@gotgenes/pi-permission-system` extension: `apps/worker/src/temporal/activities.ts:syncCodePathDenyRules` writes a global `path` deny config once per workflow (`apps/worker/src/ai/settings-writer.ts:writeCodePathPermissionConfig`), and the executor loads the extension when that config is present (`apps/worker/src/ai/pi-executor.ts`), so denies fire across every tool and child `task` session. `vuln_classes`/`exploit` scope is locked into `session.json` on first run; resumes with a different scope fail fast (`persistOrValidateRunScope`). Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`) - **Prompts** — Per-phase templates in `apps/worker/prompts/` with variable substitution (`{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`). Shared partials in `apps/worker/prompts/shared/` via `apps/worker/src/services/prompt-manager.ts`, including `_code-path-rules.txt` (focus/avoid `[FILE]`/`[GLOB]` routing) and `_rules-of-engagement.txt` (free-text engagement rules). When `exploit: false`, `apps/worker/src/services/findings-renderer.ts` deterministically converts each `*_exploitation_queue.json` into a `*_findings.md` for report assembly — no LLM in the loop -- **SDK Integration** — Uses `@anthropic-ai/claude-agent-sdk` with `maxTurns: 10_000` and `bypassPermissions` mode. Adaptive thinking is enabled by default on Opus 4.6/4.7/4.8 (`supportsAdaptiveThinking` in `apps/worker/src/ai/models.ts`); disable per-scan via `CLAUDE_ADAPTIVE_THINKING=false` (env) or `core.adaptive_thinking = false` (npx TOML). Browser automation via `playwright-cli` with session isolation (`-s=`). TOTP generation via `generate-totp` CLI tool. Login flow template at `apps/worker/prompts/shared/login-instructions.txt` supports form, SSO, API, and basic auth. On authenticated whitebox scans, the `validate-authentication` preflight performs the single real login and saves the browser session to `auth-state.json` in the per-session audit directory (path from `authStateFile()` in `apps/worker/src/audit/utils.ts`, derived from `generateAuditPath()`). The validation activity (`apps/worker/src/services/validate-authentication.ts`) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete; `logWorkflowComplete` deletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by `@include(shared/_shared-session.txt)` before their `` block — the partial restores the session and falls through to the full login flow if verification fails. `vuln-auth`/`exploit-auth` omit the include and own their own login +- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi-executor.ts` (`runPiPrompt` → `createAgentSession`, retry disabled so Temporal owns retry). Models resolve through pi-ai in `apps/worker/src/ai/models.ts` (Anthropic / Bedrock / custom base URL via `ModelRegistry`+`AuthStorage`). pi ships no JSON-schema output or `Task`/`TodoWrite` built-ins, so structured queues are captured via a `submit_exploitation_queue` custom tool (`apps/worker/src/ai/queue-schemas.ts`), and `task` (read-only child sessions) + `todo_write` are provided as custom tools (`apps/worker/src/ai/tools.ts`); the per-phase MCP collectors are pi custom tools (TypeBox `defineTool` in `apps/worker/src/mcp-server/`). Adaptive thinking (pi's `medium` level) is enabled only on Opus 4.6/4.7/4.8 (`supportsAdaptiveThinking`); every other model runs with thinking `off`. Disable per-scan via `CLAUDE_ADAPTIVE_THINKING=false` (→ `off`) / `core.adaptive_thinking = false` (npx TOML). Browser automation via `playwright-cli` with session isolation (`-s=`). TOTP generation via `generate-totp` CLI tool. Login flow template at `apps/worker/prompts/shared/login-instructions.txt` supports form, SSO, API, and basic auth. On authenticated whitebox scans, the `validate-authentication` preflight performs the single real login and saves the browser session to `auth-state.json` in the per-session audit directory (path from `authStateFile()` in `apps/worker/src/audit/utils.ts`, derived from `generateAuditPath()`). The validation activity (`apps/worker/src/services/validate-authentication.ts`) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete; `logWorkflowComplete` deletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by `@include(shared/_shared-session.txt)` before their `` block — the partial restores the session and falls through to the full login flow if verification fails. `vuln-auth`/`exploit-auth` omit the include and own their own login - **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. The run directory's top level holds only the human-facing report (`Security-Assessment-Report.md`, `FINAL_REPORT_FILENAME` in `apps/worker/src/paths.ts`); everything else — deliverables, per-agent logs, prompts, `session.json`, `workflow.log`, and browser artifacts — is nested under a hidden `.shannon/` internals dir (`INTERNAL_DIR`) so a customer sees only the report. Audit path helpers route through `generateInternalPath` (`apps/worker/src/audit/utils.ts`); the CLI nests the overlay backing dirs under the same `.shannon/` (`apps/cli/src/docker.ts`, `start.ts`). `session.json`/`workflow.log` reads use dual-read resolvers (`resolveSessionJsonPath`, `resolveRunFile`) that prefer `.shannon/` and fall back to the legacy run-root layout, so pre-restructure workspaces stay listable (`workspaces`/`logs`) without migration. Resuming a pre-restructure workspace upgrades it in place first: `migrateLegacyWorkspaceLayout` (`apps/cli/src/commands/start.ts`) renames the flat deliverables/logs/session entries into `.shannon/` (carrying the deliverables `.git` along) before the overlay dirs are mounted, so resume finds the old checkpoints instead of re-running every agent. The report is surfaced by copying the assembled `comprehensive_security_assessment_report.md` from the deliverables dir to the run root (`copyReportToRunRoot` in `apps/worker/src/services/reporting.ts`). WorkflowLogger (`apps/worker/src/audit/workflow-logger.ts`) provides unified human-readable per-workflow logs, backed by LogStream (`apps/worker/src/audit/log-stream.ts`) shared stream primitive - **Deliverables** — Saved to `.shannon/deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`) - **Workspaces & Resume** — Named workspaces via `-w ` or auto-named from URL+timestamp. Resume detects completed agents via `session.json`. `loadResumeState()` in `apps/worker/src/temporal/activities.ts` validates deliverable existence, restores git checkpoints, and cleans up incomplete deliverables. Workspace listing via `apps/worker/src/temporal/workspaces.ts` @@ -173,7 +173,7 @@ Durable workflow orchestration with crash recovery, queryable progress, intellig ### Key Design Patterns - **Configuration-Driven** — YAML configs with JSON Schema validation - **Progressive Analysis** — Each phase builds on previous results -- **SDK-First** — Claude Agent SDK handles autonomous analysis +- **Harness-First** — the pi harness (`@earendil-works/pi-coding-agent`) handles autonomous analysis - **Modular Error Handling** — `ErrorCode` enum, `Result` for explicit error propagation, automatic retry (3 attempts per agent) - **Services Boundary** — Activities are thin Temporal wrappers; `apps/worker/src/services/` owns business logic, accepts `ActivityLogger`, returns `Result`. No Temporal imports in services - **DI Container** — Per-workflow in `apps/worker/src/services/container.ts`. `AuditSession` excluded (parallel safety) @@ -233,7 +233,7 @@ Comments must be **timeless** — no references to this conversation, refactorin **Entry Points:** `apps/worker/src/temporal/workflows.ts`, `apps/worker/src/temporal/activities.ts`, `apps/worker/src/temporal/worker.ts` -**Core Logic:** `apps/worker/src/session-manager.ts`, `apps/worker/src/ai/claude-executor.ts`, `apps/worker/src/ai/settings-writer.ts` (writes `code_path` deny rules to `~/.claude/settings.json`), `apps/worker/src/config-parser.ts`, `apps/worker/src/services/` (incl. `preflight.ts`, `findings-renderer.ts`, `reporting.ts`), `apps/worker/src/audit/` +**Core Logic:** `apps/worker/src/session-manager.ts`, `apps/worker/src/ai/pi-executor.ts`, `apps/worker/src/ai/settings-writer.ts` (writes `code_path` deny rules to the `@gotgenes/pi-permission-system` global config), `apps/worker/src/config-parser.ts`, `apps/worker/src/services/` (incl. `preflight.ts`, `findings-renderer.ts`, `reporting.ts`), `apps/worker/src/audit/` **Config:** `docker-compose.yml`, `apps/cli/infra/compose.yml`, `apps/worker/configs/`, `apps/worker/prompts/`, `tsconfig.base.json` (shared compiler options), `turbo.json`, `biome.json` diff --git a/Dockerfile b/Dockerfile index bd567fa..7b063bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -91,7 +91,7 @@ COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/apps/worker /app/apps/worker COPY --from=builder /app/apps/cli/package.json /app/apps/cli/package.json -RUN npm install -g --ignore-scripts @anthropic-ai/claude-code@2.1.84 @playwright/cli@0.1.1 +RUN npm install -g --ignore-scripts @playwright/cli@0.1.1 RUN mkdir -p /tmp/.claude/skills && \ playwright-cli install --skills && \ cp -r .claude/skills/playwright-cli /tmp/.claude/skills/ && \ diff --git a/README.md b/README.md index a7e0cf4..ab2bd12 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Sample penetration test reports from intentionally vulnerable applications, prod - **Docker**: required for the worker container. - **Node.js 18+**: required for the recommended `npx` workflow. -- **AI provider credentials**: Anthropic is recommended. AWS Bedrock, Google Vertex AI, and compatible proxy setups are documented separately. +- **AI provider credentials**: Anthropic is recommended. AWS Bedrock and compatible proxy setups are documented separately. ### Run Shannon @@ -186,7 +186,7 @@ Use these guides for operational detail: | --- | --- | | [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. | | [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, report filters, and rate-limit settings. | -| [AI providers](docs/ai-providers.md) | Anthropic, AWS Bedrock, Google Vertex AI, and custom Anthropic-compatible endpoints. | +| [AI providers](docs/ai-providers.md) | Anthropic, AWS Bedrock, and custom Anthropic-compatible endpoints. | | [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. | | [Workspaces and resuming](docs/workspaces.md) | Naming workspaces, resuming interrupted scans, and workspace storage. | | [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. | diff --git a/apps/cli/src/commands/setup.ts b/apps/cli/src/commands/setup.ts index fe1dd44..1e130b2 100644 --- a/apps/cli/src/commands/setup.ts +++ b/apps/cli/src/commands/setup.ts @@ -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 { 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 { { 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 { return setupCustomBaseUrl(); case 'bedrock': return setupBedrock(); - case 'vertex': - return setupVertex(); } } @@ -215,75 +211,6 @@ async function setupBedrock(): Promise { }; } -async function setupVertex(): Promise { - // 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 { diff --git a/apps/cli/src/commands/start.ts b/apps/cli/src/commands/start.ts index 13c47fc..9867f76 100644 --- a/apps/cli/src/commands/start.ts +++ b/apps/cli/src/commands/start.ts @@ -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 { } 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 { containerName, envFlags: buildEnvFlags(), ...(config && { config }), - ...(hasCredentials && { credentials: credentialsPath }), ...(promptsDir && { promptsDir }), ...(outputDir && { outputDir }), workspace, diff --git a/apps/cli/src/config/resolver.ts b/apps/cli/src/config/resolver.ts index 677a63a..60e4927 100644 --- a/apps/cli/src/config/resolver.ts +++ b/apps/cli/src/config/resolver.ts @@ -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 | 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; diff --git a/apps/cli/src/config/writer.ts b/apps/cli/src/config/writer.ts index bf26d47..0a69bd1 100644 --- a/apps/cli/src/config/writer.ts +++ b/apps/cli/src/config/writer.ts @@ -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 }; } diff --git a/apps/cli/src/docker.ts b/apps/cli/src/docker.ts index 16e4963..012a4a4 100644 --- a/apps/cli/src/docker.ts +++ b/apps/cli/src/docker.ts @@ -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); diff --git a/apps/cli/src/env.ts b/apps/cli/src/env.ts index 2190838..581c617 100644 --- a/apps/cli/src/env.ts +++ b/apps/cli/src/env.ts @@ -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' diff --git a/apps/cli/src/home.ts b/apps/cli/src/home.ts index b8c979e..e6d61b6 100644 --- a/apps/cli/src/home.ts +++ b/apps/cli/src/home.ts @@ -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 }); } diff --git a/apps/worker/package.json b/apps/worker/package.json index c0bb627..d843178 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -19,7 +19,10 @@ "clean": "rm -rf dist" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "catalog:", + "@earendil-works/pi-agent-core": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-coding-agent": "^0.79.1", + "@gotgenes/pi-permission-system": "^10.9.0", "@temporalio/activity": "^1.11.0", "@temporalio/client": "^1.11.0", "@temporalio/worker": "^1.11.0", @@ -28,7 +31,7 @@ "ajv-formats": "^2.1.1", "dotenv": "^16.4.5", "js-yaml": "^4.1.0", - "zod": "^4.3.6", + "typebox": "1.1.38", "zx": "^8.0.0" }, "devDependencies": { diff --git a/apps/worker/prompts/exploit-auth.txt b/apps/worker/prompts/exploit-auth.txt index a4d69b5..67dbac0 100644 --- a/apps/worker/prompts/exploit-auth.txt +++ b/apps/worker/prompts/exploit-auth.txt @@ -116,7 +116,7 @@ Before beginning exploitation, read these strategic intelligence files in order: 2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, user roles, and data flow maps. 3. `.shannon/deliverables/auth_analysis_deliverable.md` - Strategic context from the Auth analysis specialist, including notes on session mechanisms, password policies, and flawed logic paths. -- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. +- You will manage your work using the **`todo_write` tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. @@ -145,18 +145,18 @@ You are the **Identity Compromise Specialist** - proving tangible impact of brok - **Browser Automation (playwright-cli skill):** Essential for interacting with multi-step authentication flows, injecting stolen session cookies, and verifying account takeover in a real browser context. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. -- **Task Agent:** Mandatory coder-executor for brute force batches, credential stuffing, token replay automation, and any scripted workflow. -- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. -- **Read tool:** To read false positives from your tracking file at `workspace/auth_false_positives.md`. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent:** Mandatory coder-executor for brute force batches, credential stuffing, token replay automation, and any scripted workflow. +- **`todo_write` tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. +- **`read` tool:** To read false positives from your tracking file at `workspace/auth_false_positives.md`. **CRITICAL TASK AGENT WORKFLOW:** -- Delegate every custom script or multi-step automation to the Task Agent; do not hand-write scripts in Bash. -- Keep requests ≤15 lines and use the template below so the Task Agent has clear inputs and success criteria. +- Delegate every custom script or multi-step automation to the `task` agent; do not hand-write scripts in Bash. +- Keep requests ≤15 lines and use the template below so the `task` agent has clear inputs and success criteria. -## Task Agent Scripting Requirements -Use the Task Agent to execute all bulk or repetitive auth attacks. Manual commands are for quick verification only. +## `task` agent Scripting Requirements +Use the `task` agent to execute all bulk or repetitive auth attacks. Manual commands are for quick verification only. **MANDATORY TEMPLATE:** " @@ -172,7 +172,7 @@ Inputs: Success Criteria: Print each attempt with status code and key response text so I can evaluate impact. " -**DO NOT:** exceed 5–6 inputs per run, ask for advanced parsing, or bypass the Task Agent for scripting. +**DO NOT:** exceed 5–6 inputs per run, ask for advanced parsing, or bypass the `task` agent for scripting. @@ -183,11 +183,11 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e - **Complete the Workflow:** For each vulnerability, you must follow the full Exploitation Workflow from Confirmation to Impact Demonstration. ## **The Task-Based Attack Plan** -Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process. +Your entire exploitation process will be managed through the `todo_write` tool. You MUST adhere to this process. **1. Initialization:** - Read `.shannon/deliverables/auth_exploitation_queue.json`. -- Use the TodoWrite tool to create tasks for each vulnerability in the queue. +- Use the `todo_write` tool to create tasks for each vulnerability in the queue. - Each task should include the vulnerability ID and the hypothesis to test. Example tasks: - "AUTH-VULN-01: Exploit Session Fixation on /login (Hypothesis: Session ID not rotated)" @@ -195,10 +195,10 @@ Your entire exploitation process will be managed through the TodoWrite tool. You **2. The Execution Loop:** - **A. Get Next Task:** Review your todo list and identify the next pending task to work on. -- **B. Plan & Decompose:** Mark the task as in_progress using TodoWrite. Read the `suggested_exploit_technique` from the vulnerability details. This is your primary directive. Plan the specific commands and tools needed to execute this attack pattern. If the attack is complex (e.g., a multi-step logic flaw), use TodoWrite to add smaller tasks. +- **B. Plan & Decompose:** Mark the task as in_progress using `todo_write`. Read the `suggested_exploit_technique` from the vulnerability details. This is your primary directive. Plan the specific commands and tools needed to execute this attack pattern. If the attack is complex (e.g., a multi-step logic flaw), use `todo_write` to add smaller tasks. - **C. Execute:** Run the command for the current task. -- **D. Update & Document:** Based on the result, update the TodoWrite status to completed if successful. Document the outcome in your memory for the final report. -- **E. Expand the Plan (Branching):** If successful and more steps are needed, use TodoWrite to add the next logical tasks required by the OWASP workflow. +- **D. Update & Document:** Based on the result, update the `todo_write` status to completed if successful. Document the outcome in your memory for the final report. +- **E. Expand the Plan (Branching):** If successful and more steps are needed, use `todo_write` to add the next logical tasks required by the OWASP workflow. ## **OWASP Authentication Exploitation Workflow** For each root vulnerability, follow this systematic, two-stage attack flow. @@ -289,7 +289,7 @@ Prioritize these based on the intelligence from your queue and vulnerability ana **2. Credential-Based Attacks** - **Credential Stuffing with cURL:** `curl -X POST -H "Content-Type: application/json" -d '{"username":"admin", "password":"CrackedPassword123"}' https://target.com/api/login` -- **Brute Force (Example with Task Agent):** +- **Brute Force (Example with `task` agent):** "Task: Write a python script to POST to /api/login with username 'admin' and passwords from this list: ['admin', 'password', '123456']. Print the status code for each." **3. Logic Flaws** @@ -304,8 +304,8 @@ Prioritize these based on the intelligence from your queue and vulnerability ana - -You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/auth_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. + +You emit your exploitation evidence through a single tool — `add_exploit`. The host renderer assembles `.shannon/deliverables/auth_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. **When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission. @@ -316,7 +316,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` **Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting. **Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible. - + ### Evidence Completeness Verification @@ -328,7 +328,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` - All technical components specified without ambiguity COMPLETION REQUIREMENTS (ALL must be satisfied): - 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool. + 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the `todo_write` tool. 2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/auth_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/auth_false_positives.md`, not via `add_exploit`. CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/auth_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. diff --git a/apps/worker/prompts/exploit-authz.txt b/apps/worker/prompts/exploit-authz.txt index 78120c3..6bbaac8 100644 --- a/apps/worker/prompts/exploit-authz.txt +++ b/apps/worker/prompts/exploit-authz.txt @@ -105,7 +105,7 @@ Before beginning exploitation, read these strategic intelligence files in order: 2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, user roles, and permission models. 3. `.shannon/deliverables/authz_analysis_deliverable.md` - Strategic context from the Authz analysis specialist, including notes on access control patterns, role hierarchies, and flawed logic paths. -- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. +- You will manage your work using the **`todo_write` tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. @@ -134,18 +134,18 @@ You are the **Privilege Escalation Specialist** - proving tangible impact of bro - **Browser Automation (playwright-cli skill):** Essential for interacting with complex authorization flows, testing role-based access controls in browser contexts, and verifying privilege escalation through UI elements. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. -- **Task Agent:** Mandatory coder-executor for IDOR sweeps, role escalation loops, and workflow bypass automation. -- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. -- **Read tool:** To read false positives from your tracking file at `workspace/authz_false_positives.md`. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent:** Mandatory coder-executor for IDOR sweeps, role escalation loops, and workflow bypass automation. +- **`todo_write` tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. +- **`read` tool:** To read false positives from your tracking file at `workspace/authz_false_positives.md`. **CRITICAL TASK AGENT WORKFLOW:** -- Delegate every multi-user iteration, role toggle test, or workflow automation script to the Task Agent—never handcraft these scripts yourself. -- Keep requests ≤15 lines and adhere to the template below so the Task Agent can act deterministically. +- Delegate every multi-user iteration, role toggle test, or workflow automation script to the `task` agent—never handcraft these scripts yourself. +- Keep requests ≤15 lines and adhere to the template below so the `task` agent can act deterministically. -## Task Agent Scripting Requirements -All repeated authorization tests must run through the Task Agent. +## `task` agent Scripting Requirements +All repeated authorization tests must run through the `task` agent. **MANDATORY TEMPLATE:** " @@ -161,7 +161,7 @@ Inputs: Success Criteria: Execute one request per identity, logging status code and key response text so I can confirm access levels. " -**DO NOT:** exceed 5 identities per run, ask for complex diffing, or bypass the Task Agent for scripting. +**DO NOT:** exceed 5 identities per run, ask for complex diffing, or bypass the `task` agent for scripting. @@ -172,11 +172,11 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e - **Complete the Workflow:** For each vulnerability, you must follow the full Exploitation Workflow from Confirmation to Impact Demonstration. ## **The Task-Based Attack Plan** -Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process. +Your entire exploitation process will be managed through the `todo_write` tool. You MUST adhere to this process. **1. Initialization:** - Read `.shannon/deliverables/authz_exploitation_queue.json`. -- Use the TodoWrite tool to create tasks for each vulnerability in the queue. +- Use the `todo_write` tool to create tasks for each vulnerability in the queue. - Each task should include the vulnerability ID, type, and the hypothesis to test. Example tasks: - "AUTHZ-VULN-01 (Horizontal): Exploit ownership bypass on /api/user/{id} (Hypothesis: Access to other users' data)" @@ -185,10 +185,10 @@ Your entire exploitation process will be managed through the TodoWrite tool. You **2. The Execution Loop:** - **A. Get Next Task:** Review your todo list and identify the next pending task to work on. -- **B. Plan & Decompose:** Mark the task as in_progress using TodoWrite. Read the vulnerability type (`Horizontal`, `Vertical`, or `Context_Workflow`) and the `minimal_witness` from the vulnerability details. This is your primary directive. Plan the specific commands and tools needed to execute this attack pattern. If the attack is complex (e.g., a multi-step privilege escalation), use TodoWrite to add smaller tasks. +- **B. Plan & Decompose:** Mark the task as in_progress using `todo_write`. Read the vulnerability type (`Horizontal`, `Vertical`, or `Context_Workflow`) and the `minimal_witness` from the vulnerability details. This is your primary directive. Plan the specific commands and tools needed to execute this attack pattern. If the attack is complex (e.g., a multi-step privilege escalation), use `todo_write` to add smaller tasks. - **C. Execute:** Run the command for the current task. -- **D. Update & Document:** Based on the result, update the TodoWrite status to completed if successful. Document the outcome in your memory for the final report. -- **E. Expand the Plan (Branching):** If successful and more steps are needed, use TodoWrite to add the next logical tasks required by the OWASP workflow. +- **D. Update & Document:** Based on the result, update the `todo_write` status to completed if successful. Document the outcome in your memory for the final report. +- **E. Expand the Plan (Branching):** If successful and more steps are needed, use `todo_write` to add the next logical tasks required by the OWASP workflow. ## **OWASP Authorization Exploitation Workflow** For each root vulnerability, follow this systematic, two-stage attack flow. @@ -312,8 +312,8 @@ Remember: The most effective attacks often come from understanding the specific - -You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/authz_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. + +You emit your exploitation evidence through a single tool — `add_exploit`. The host renderer assembles `.shannon/deliverables/authz_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. **When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission. @@ -324,7 +324,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` **Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting. **Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible. - + ### Evidence Completeness Verification @@ -336,7 +336,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` - All technical components specified without ambiguity COMPLETION REQUIREMENTS (ALL must be satisfied): - 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool. + 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the `todo_write` tool. 2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/authz_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/authz_false_positives.md`, not via `add_exploit`. CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/authz_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. diff --git a/apps/worker/prompts/exploit-injection.txt b/apps/worker/prompts/exploit-injection.txt index 26f8243..7bd01b8 100644 --- a/apps/worker/prompts/exploit-injection.txt +++ b/apps/worker/prompts/exploit-injection.txt @@ -87,7 +87,7 @@ Before beginning exploitation, read these strategic intelligence files in order: - Leverage vulnerability details and context from pre-analysis rather than blind testing. - Reference provided vulnerable code snippets to avoid blind guessing of query column counts, command syntax, or database/OS types. -- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. +- You will manage your work using the **`todo_write` tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. @@ -116,13 +116,13 @@ You are the **Proof-of-Impact Generator** - converting theoretical injection fla - **Browser Automation (playwright-cli skill):** For testing injection vulnerabilities through browser interactions when needed. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. -- **Task Agent:** Mandatory coder-executor for any custom scripting beyond single ad-hoc commands. -- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. -- **Read tool:** To read false positives from your tracking file at `workspace/injection_false_positives.md`. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent:** Mandatory coder-executor for any custom scripting beyond single ad-hoc commands. +- **`todo_write` tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. +- **`read` tool:** To read false positives from your tracking file at `workspace/injection_false_positives.md`. **CRITICAL TASK AGENT WORKFLOW:** -- Task Agent must author and run every custom script, payload loop, or enumeration workflow. Do not craft standalone scripts in Bash or other tools. +- `task` agent must author and run every custom script, payload loop, or enumeration workflow. Do not craft standalone scripts in Bash or other tools. - Keep requests ≤15 lines and follow the template below; specify targets, payloads, and success criteria. @@ -135,11 +135,11 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e - **Complete the Workflow:** For each vulnerability, you must follow the full OWASP Exploitation Workflow from Confirmation to either Exfiltration or a documented conclusion of non-exploitability. ## **The Task-Based Attack Plan** -Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process. +Your entire exploitation process will be managed through the `todo_write` tool. You MUST adhere to this process. **1. Initialization:** - Read the `.shannon/deliverables/injection_exploitation_queue.json` file. -- Use the TodoWrite tool to create tasks for each vulnerability in the queue. +- Use the `todo_write` tool to create tasks for each vulnerability in the queue. - Each task should include the vulnerability ID and the hypothesis to test. Example tasks: - "SQLI-VULN-01: Exploit endpoint /api/search?q= (Hypothesis: Basic UNION injection)" @@ -150,16 +150,16 @@ You will repeatedly perform the following loop until all tasks are completed: - **A. Get Next Task:** Review your todo list and identify the next pending task to work on. -- **B. Plan & Decompose:** Mark the task as in_progress using TodoWrite. Decide on the concrete command or action. If the task is complex (e.g., "Enumerate tables"), use TodoWrite to add smaller, actionable tasks. +- **B. Plan & Decompose:** Mark the task as in_progress using `todo_write`. Decide on the concrete command or action. If the task is complex (e.g., "Enumerate tables"), use `todo_write` to add smaller, actionable tasks. - **C. Execute:** Run the command for the current task (e.g., run `curl` with an `ORDER BY` payload). -- **D. Update & Document:** Based on the result, update the TodoWrite status: +- **D. Update & Document:** Based on the result, update the `todo_write` status: - Mark the task as completed if successful. - Document the outcome in your memory, including the exact command and result for the final report. - Example outcome to remember: "Step 1.1: Determined column count is 4 using ORDER BY - Command: curl 'https://target.com/api/search?q=test' ORDER BY 4--'" -- **E. Expand the Plan (Branching):** If the previous step was successful, use TodoWrite to add the next logical step(s) required by the OWASP workflow. **You must complete all required tasks for a vulnerability.** You are not permitted to skip tasks. +- **E. Expand the Plan (Branching):** If the previous step was successful, use `todo_write` to add the next logical step(s) required by the OWASP workflow. **You must complete all required tasks for a vulnerability.** You are not permitted to skip tasks. ## **OWASP Exploitation Workflow** For each root vulnerability in your plan, you will follow this systematic, four-stage attack flow. These stages will become the structured steps in your Markdown plan. @@ -184,11 +184,11 @@ Use the right tool for the job to ensure thoroughness. Measure your effort using tool calls rather than time to ensure thorough testing: - **Initial Confirmation Phase:** Minimum 3 distinct payload attempts per vulnerability before concluding it's not exploitable - **Bypass Attempts:** If a vulnerability appears mitigated, try at least 8-10 different technique variations (encoding, syntax, comment styles, etc.) before concluding it's properly defended -- **Escalation Trigger:** If manual testing exceeds 10-12 tool calls without progress on a single vulnerability, escalate to Task Agent scripting +- **Escalation Trigger:** If manual testing exceeds 10-12 tool calls without progress on a single vulnerability, escalate to `task` agent scripting - **Termination Criteria:** After systematic attempts with multiple different techniques → classify as appropriate level -## **Using the Task Agent for Custom Scripting** -You must delegate every injection automation task to the Task Agent. Use manual `curl` runs for spot checks, then escalate to scripted payload loops handled by the Task Agent. +## **Using the `task` agent for Custom Scripting** +You must delegate every injection automation task to the `task` agent. Use manual `curl` runs for spot checks, then escalate to scripted payload loops handled by the `task` agent. **TEMPLATE FOR SCRIPTING TASKS (REQUIRED):** " @@ -204,7 +204,7 @@ Inputs: Success Criteria: Print status code and response excerpt for each payload so I can analyze impact. " -**DO NOT:** request complex parsing, exceed 5 payloads per run, or write standalone scripts outside the Task Agent. +**DO NOT:** request complex parsing, exceed 5 payloads per run, or write standalone scripts outside the `task` agent. ### Proof of Exploitation Levels @@ -335,8 +335,8 @@ Prioritize your techniques based on the intelligence from the analysis deliverab - -You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/injection_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. + +You emit your exploitation evidence through a single tool — `add_exploit`. The host renderer assembles `.shannon/deliverables/injection_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. **When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission. @@ -347,7 +347,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` **Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting. **Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible. - + ### Evidence Completeness Verification @@ -359,7 +359,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` - All technical components specified without ambiguity COMPLETION REQUIREMENTS (ALL must be satisfied): -1. **Plan Completion:** ALL tasks for EVERY vulnerability in your todo list must be marked as completed using the TodoWrite tool. **No vulnerability or task can be left unaddressed.** +1. **Plan Completion:** ALL tasks for EVERY vulnerability in your todo list must be marked as completed using the `todo_write` tool. **No vulnerability or task can be left unaddressed.** 2. **Evidence Emission:** Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/injection_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/injection_false_positives.md`, not via `add_exploit`. **CRITICAL WARNING:** Announcing completion before every item in `.shannon/deliverables/injection_exploitation_queue.json` has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. Superficial testing is not acceptable. diff --git a/apps/worker/prompts/exploit-ssrf.txt b/apps/worker/prompts/exploit-ssrf.txt index d78ea43..fe800a6 100644 --- a/apps/worker/prompts/exploit-ssrf.txt +++ b/apps/worker/prompts/exploit-ssrf.txt @@ -105,7 +105,7 @@ Before beginning exploitation, read these strategic intelligence files in order: 2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, service endpoints, and internal architecture maps. 3. `.shannon/deliverables/ssrf_analysis_deliverable.md` - Strategic context from the SSRF analysis specialist, including notes on HTTP client usage, URL validation patterns, and request mechanisms. -- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. +- You will manage your work using the **`todo_write` tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts. @@ -133,19 +133,19 @@ You are the **Network Boundary Breaker** - proving tangible impact of SSRF vulne -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. - **Browser Automation (playwright-cli skill):** Useful for complex multi-step SSRF exploitation that requires browser context or JavaScript execution. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Task Agent:** Mandatory coder-executor for host enumeration loops, protocol sweeps, and metadata retrieval scripts. -- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. -- **Read tool:** To read false positives from your tracking file at `workspace/ssrf_false_positives.md`. +- **`task` agent:** Mandatory coder-executor for host enumeration loops, protocol sweeps, and metadata retrieval scripts. +- **`todo_write` tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow. +- **`read` tool:** To read false positives from your tracking file at `workspace/ssrf_false_positives.md`. **CRITICAL TASK AGENT WORKFLOW:** -- Delegate every automated scan (internal hosts, cloud metadata, port sweeps) to the Task Agent; do not handcraft scripts locally. +- Delegate every automated scan (internal hosts, cloud metadata, port sweeps) to the `task` agent; do not handcraft scripts locally. - Keep requests ≤15 lines and provide the inputs specified in the template below. -## Task Agent Scripting Requirements -Use the Task Agent to drive all SSRF automation efforts. +## `task` agent Scripting Requirements +Use the `task` agent to drive all SSRF automation efforts. **MANDATORY TEMPLATE:** " @@ -161,7 +161,7 @@ Inputs: Success Criteria: Issue requests for each target, log status code and indicator snippet so I can confirm impact. " -**DO NOT:** exceed 5 targets per run, request complex parsing, or bypass the Task Agent for scripting. +**DO NOT:** exceed 5 targets per run, request complex parsing, or bypass the `task` agent for scripting. @@ -172,11 +172,11 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e - **Complete the Workflow:** For each vulnerability, you must follow the full Exploitation Workflow from Confirmation to Impact Demonstration. ## **The Task-Based Attack Plan** -Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process. +Your entire exploitation process will be managed through the `todo_write` tool. You MUST adhere to this process. **1. Initialization:** - Read `.shannon/deliverables/ssrf_exploitation_queue.json`. -- Use the TodoWrite tool to create tasks for each vulnerability in the queue. +- Use the `todo_write` tool to create tasks for each vulnerability in the queue. - Each task should include the vulnerability ID and the hypothesis to test. Example tasks: - "SSRF-VULN-01: Exploit URL manipulation on /api/fetch (Hypothesis: Internal service access)" @@ -184,10 +184,10 @@ Your entire exploitation process will be managed through the TodoWrite tool. You **2. The Execution Loop:** - **A. Get Next Task:** Review your todo list and identify the next pending task to work on. -- **B. Plan & Decompose:** Mark the task as in_progress using TodoWrite. Read the `suggested_exploit_technique` from the vulnerability details. This is your primary directive. Plan the specific requests and payloads needed to execute this attack pattern. If the attack is complex (e.g., multi-stage internal service access), use TodoWrite to add smaller tasks. +- **B. Plan & Decompose:** Mark the task as in_progress using `todo_write`. Read the `suggested_exploit_technique` from the vulnerability details. This is your primary directive. Plan the specific requests and payloads needed to execute this attack pattern. If the attack is complex (e.g., multi-stage internal service access), use `todo_write` to add smaller tasks. - **C. Execute:** Run the command for the current task. -- **D. Update & Document:** Based on the result, update the TodoWrite status to completed if successful. Document the outcome in your memory for the final report. -- **E. Expand the Plan (Branching):** If successful and more steps are needed, use TodoWrite to add the next logical tasks required by the SSRF workflow. +- **D. Update & Document:** Based on the result, update the `todo_write` status to completed if successful. Document the outcome in your memory for the final report. +- **E. Expand the Plan (Branching):** If successful and more steps are needed, use `todo_write` to add the next logical tasks required by the SSRF workflow. ## **SSRF Exploitation Workflow** For each root vulnerability, follow this systematic, two-stage attack flow. @@ -389,8 +389,8 @@ A successful SSRF doesn't always mean data is immediately exfiltrated. Validatio - -You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/ssrf_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. + +You emit your exploitation evidence through a single tool — `add_exploit`. The host renderer assembles `.shannon/deliverables/ssrf_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. **When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission. @@ -401,7 +401,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` **Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting. **Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible. - + ### Evidence Completeness Verification @@ -413,7 +413,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` - All technical components specified without ambiguity COMPLETION REQUIREMENTS (ALL must be satisfied): - 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool. + 1. Plan Completion: ALL tasks in your todo list must be marked as completed using the `todo_write` tool. 2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/ssrf_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/ssrf_false_positives.md`, not via `add_exploit`. CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/ssrf_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. diff --git a/apps/worker/prompts/exploit-xss.txt b/apps/worker/prompts/exploit-xss.txt index 3e61cb2..3c05a4e 100644 --- a/apps/worker/prompts/exploit-xss.txt +++ b/apps/worker/prompts/exploit-xss.txt @@ -107,7 +107,7 @@ Before beginning exploitation, read these strategic intelligence files in order: - Use the `render_context` and `mismatch_reason` from the analysis queue to craft precise initial payloads. - Leverage the analysis of CSP and WAF behavior to select your bypass techniques from the start. -- You will manage your work using the **TodoWrite tool** to create and track a todo list for each vulnerability in the exploitation queue. This provides structured tracking of your exploitation attempts. +- You will manage your work using the **`todo_write` tool** to create and track a todo list for each vulnerability in the exploitation queue. This provides structured tracking of your exploitation attempts. @@ -136,18 +136,18 @@ You are the **Client-Side Impact Demonstrator** - converting theoretical XSS fla - **Browser Automation (playwright-cli skill):** Your primary tool for testing DOM-based and Stored XSS, confirming script execution in a real browser context, and interacting with the application post-exploitation. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. -- **Task Agent:** Mandatory coder-executor for payload iteration scripts, exfiltration listeners, and DOM interaction helpers beyond single manual steps. -- **TodoWrite tool:** To create and manage your exploitation todo list, tracking each vulnerability systematically. -- **Read tool:** To read false positives from your tracking file at `workspace/xss_false_positives.md`. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent:** Mandatory coder-executor for payload iteration scripts, exfiltration listeners, and DOM interaction helpers beyond single manual steps. +- **`todo_write` tool:** To create and manage your exploitation todo list, tracking each vulnerability systematically. +- **`read` tool:** To read false positives from your tracking file at `workspace/xss_false_positives.md`. **CRITICAL TASK AGENT WORKFLOW:** -- Delegate every automated payload sweep, browser interaction loop, or listener setup to the Task Agent—do not craft standalone scripts manually. +- Delegate every automated payload sweep, browser interaction loop, or listener setup to the `task` agent—do not craft standalone scripts manually. - Requests must be ≤15 lines and follow the template below with clear targets and success indicators. -## Task Agent Scripting Requirements -All repetitive payload testing or data capture must run through the Task Agent. +## `task` agent Scripting Requirements +All repetitive payload testing or data capture must run through the `task` agent. **MANDATORY TEMPLATE:** " @@ -163,7 +163,7 @@ Inputs: Success Criteria: Run each payload, log the indicator, and surface any captured data for my review. " -**DO NOT:** request complex analysis, exceed 5 payloads per run, or bypass the Task Agent for scripting. +**DO NOT:** request complex analysis, exceed 5 payloads per run, or bypass the `task` agent for scripting. @@ -183,7 +183,7 @@ For **every single vulnerability** in your queue, systematically work through th **Actions:** - Read `.shannon/deliverables/xss_exploitation_queue.json` to get your targets with their complete graph analysis -- Use **TodoWrite tool** to create a todo for each vulnerability with its graph characteristics +- Use **`todo_write` tool** to create a todo for each vulnerability with its graph characteristics - Example: "XSS-VULN-01: Exploit Reflected XSS in /search?q= (source: URL param → no sanitization → innerHTML sink)" - Study the provided intelligence for each vulnerability: - `source_detail`: The exact entry point for your payload @@ -322,8 +322,8 @@ POTENTIAL (Report in separate subsection): - -You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/xss_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. + +You emit your exploitation evidence through a single tool — `add_exploit`. The host renderer assembles `.shannon/deliverables/xss_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly. **When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission. @@ -334,7 +334,7 @@ You emit your exploitation evidence through a single MCP tool — `add_exploit` **Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting. **Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible. - + ### Evidence Completeness Verification diff --git a/apps/worker/prompts/pre-recon-code.txt b/apps/worker/prompts/pre-recon-code.txt index e0048dd..4bd6435 100644 --- a/apps/worker/prompts/pre-recon-code.txt +++ b/apps/worker/prompts/pre-recon-code.txt @@ -21,7 +21,7 @@ Filesystem: - Focus on SECURITY IMPLICATIONS and ACTIONABLE FINDINGS rather than just component listings - Identify trust boundaries, privilege escalation paths, and data flow security concerns - Include specific examples from the code when discussing security concerns -- **MANDATORY:** You MUST emit your complete analysis by calling all seven `set_*` MCP tools listed in `` before terminating. The host renders the deliverable Markdown from those calls. +- **MANDATORY:** You MUST emit your complete analysis by calling all seven `set_*` tools listed in `` before terminating. The host renders the deliverable Markdown from those calls. **GIT AWARENESS:** Read `.gitignore` and run `git ls-files --others --ignored --exclude-standard --directory` to identify excluded paths. To check a specific file, use `git ls-files ` — output means tracked, empty means untracked. Only flag tracked files as vulnerabilities. Untracked files relevant to security (e.g., secrets, credentials, sensitive configs) may be noted as informational. @@ -86,18 +86,18 @@ You are the **Code Intelligence Gatherer** and **Architectural Foundation Builde **CRITICAL TOOL USAGE GUIDANCE:** -- PREFER the Task Agent for comprehensive source code analysis to leverage specialized code review capabilities. -- Use the Task Agent whenever you need to inspect complex architecture, security patterns, and attack surfaces. -- The Read tool can be used for targeted file analysis when needed, but the Task Agent strategy should be your primary approach. +- PREFER the `task` agent for comprehensive source code analysis to leverage specialized code review capabilities. +- Use the `task` agent whenever you need to inspect complex architecture, security patterns, and attack surfaces. +- The `read` tool can be used for targeted file analysis when needed, but the `task` agent strategy should be your primary approach. **Available Tools:** -- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication mechanisms, map attack surfaces, and understand architectural patterns. MANDATORY for all source code analysis. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create todo items for each phase and agent that needs execution. Mark items as "in_progress" when working on them and "completed" when done. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication mechanisms, map attack surfaces, and understand architectural patterns. MANDATORY for all source code analysis. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create todo items for each phase and agent that needs execution. Mark items as "in_progress" when working on them and "completed" when done. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. -**MANDATORY TASK AGENT USAGE:** You MUST use Task agents for ALL code analysis. Direct file reading is PROHIBITED. +**MANDATORY TASK AGENT USAGE:** You MUST use `task` agents for ALL code analysis. Direct file reading is PROHIBITED. **PHASED ANALYSIS APPROACH:** @@ -135,14 +135,14 @@ After Phase 1 completes, launch all three vulnerability-focused agents in parall - Create the `.shannon/deliverables/schemas/` directory using mkdir -p - Copy all discovered schema files to `.shannon/deliverables/schemas/` with descriptive names - Include schema locations in your attack surface analysis -- **Emit findings via MCP tools:** Call every tool listed in `` exactly once. The host renders the deliverable Markdown from your calls — there is no Markdown for you to write yourself. +- **Emit findings via tools:** Call every tool listed in `` exactly once. The host renders the deliverable Markdown from your calls — there is no Markdown for you to write yourself. **EXECUTION PATTERN:** -1. **Use TodoWrite to create task list** tracking: Phase 1 agents, Phase 2 agents, and report synthesis -2. **Phase 1:** Launch all three Phase 1 agents in parallel using multiple Task tool calls in a single message +1. **Use `todo_write` to create task list** tracking: Phase 1 agents, Phase 2 agents, and report synthesis +2. **Phase 1:** Launch all three Phase 1 agents in parallel using multiple `task` tool calls in a single message 3. **Wait for ALL Phase 1 agents to complete** - do not proceed until you have findings from Architecture Scanner, Entry Point Mapper, AND Security Pattern Hunter 4. **Mark Phase 1 todos as completed** and review all findings -5. **Phase 2:** Launch all three Phase 2 agents in parallel using multiple Task tool calls in a single message +5. **Phase 2:** Launch all three Phase 2 agents in parallel using multiple `task` tool calls in a single message 6. **Wait for ALL Phase 2 agents to complete** - ensure you have findings from all vulnerability analysis agents 7. **Mark Phase 2 todos as completed** 8. **Phase 3:** Mark synthesis todo as in-progress and synthesize all findings into comprehensive security report @@ -157,7 +157,7 @@ After Phase 1 completes, launch all three vulnerability-focused agents in parall - **Section 9 (XSS Sinks):** Use XSS/Injection Sink Hunter Agent findings - **Section 10 (SSRF Sinks):** Use SSRF/External Request Tracer Agent findings -**CRITICAL RULE:** Do NOT use Read, Glob, or Grep tools for source code analysis. All code examination must be delegated to Task agents. +**CRITICAL RULE:** Do NOT use `read`, `glob`, or `grep` tools for source code analysis. All code examination must be delegated to `task` agents. @@ -177,8 +177,8 @@ After Phase 1 completes, launch all three vulnerability-focused agents in parall - Static files or scripts that require manual opening in a browser (not served by the application). - -**Emit your findings exclusively via the `pre-recon-collector` MCP tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself. + +**Emit your findings exclusively via the deliverable tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself. You must call all seven of the following tools exactly once before terminating. Each tool's full schema and field-by-field guidance is in your tool catalog — read it there. @@ -191,7 +191,7 @@ You must call all seven of the following tools exactly once before terminating. - `set_ssrf_sinks` — SSRF sinks grouped by sink category (Section 10). Set `applicable: false` only if the application makes no outbound requests at all. Each `set_*` tool is one-shot. Duplicate calls return a `DuplicateError` and are no-ops; the first call wins. Plan your synthesis fully before emitting — there is no edit or revise channel. - + **COMPLETION REQUIREMENTS (ALL must be satisfied):** @@ -201,11 +201,11 @@ Each `set_*` tool is one-shot. Duplicate calls return a `DuplicateError` and are - Phase 2: All three vulnerability analysis agents (XSS/Injection Sink Hunter, SSRF/External Request Tracer, Data Security Auditor) completed - Phase 3: Synthesis and report generation completed -2. **MCP Emission:** All seven `set_*` MCP tools listed in `` must have been called. +2. **Deliverable Emission:** All seven `set_*` tools listed in `` must have been called. 3. **Schemas Side Output:** `.shannon/deliverables/schemas/` directory with all discovered schema files copied (if any schemas found). -4. **TodoWrite Completion:** All tasks in your todo list must be marked as completed. +4. **`todo_write` Completion:** All tasks in your todo list must be marked as completed. **ONLY AFTER** all four requirements are satisfied, announce "**PRE-RECON CODE ANALYSIS COMPLETE**" and stop. diff --git a/apps/worker/prompts/recon.txt b/apps/worker/prompts/recon.txt index 3198ec5..4e829d7 100644 --- a/apps/worker/prompts/recon.txt +++ b/apps/worker/prompts/recon.txt @@ -73,11 +73,11 @@ A component is **out-of-scope** if it **cannot** be invoked through the running Please use these tools for the following use cases: -- Task tool: **MANDATORY for ALL source code analysis.** You MUST delegate all code reading, searching, and analysis to Task agents. DO NOT use Read, Glob, or Grep tools for source code. +- `task` tool: **MANDATORY for ALL source code analysis.** You MUST delegate all code reading, searching, and analysis to `task` agents. DO NOT use `read`, `glob`, or `grep` tools for source code. - **Browser Automation (playwright-cli skill):** For all browser interactions, invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. -**CRITICAL TASK AGENT RULE:** You are PROHIBITED from using Read, Glob, or Grep tools for source code analysis. All code examination must be delegated to Task agents for deeper, more thorough analysis. +**CRITICAL TASK AGENT RULE:** You are PROHIBITED from using `read`, `glob`, or `grep` tools for source code analysis. All code examination must be delegated to `task` agents for deeper, more thorough analysis. @@ -124,29 +124,29 @@ You must follow this methodical four-step process: - Map out all user-facing functionality: login forms, registration flows, password reset pages, etc. Document the multi-step processes. - Observe the network requests to identify primary API calls. -3. **Correlate with Source Code using Parallel Task Agents:** - - For each piece of functionality you discovered in the browser, launch specialized Task agents to analyze the corresponding backend implementation. - - Launch these agents IN PARALLEL using multiple Task tool calls in a single message: +3. **Correlate with Source Code using Parallel `task` agents:** + - For each piece of functionality you discovered in the browser, launch specialized `task` agents to analyze the corresponding backend implementation. + - Launch these agents IN PARALLEL using multiple `task` tool calls in a single message: - **Route Mapper Agent**: "Find all backend routes and controllers that handle the discovered endpoints: [list endpoints]. Map each endpoint to its exact handler function with file paths and line numbers." - **Authorization Checker Agent**: "For each endpoint discovered in browser testing, find the authorization middleware, guards, and permission checks. Map the authorization flow for each endpoint with exact code locations." - **Input Validator Agent**: "Analyze the input validation logic for all discovered form fields and API parameters. Find validation rules, sanitization, and data processing for each input with exact file paths." - **Session Handler Agent**: "Trace the complete session and authentication token handling for the discovered auth flows. Map session creation, storage, validation, and destruction with exact code locations." -3.5 **Authorization Architecture Analysis using Task Agents:** +3.5 **Authorization Architecture Analysis using `task` agents:** - Launch a dedicated **Authorization Architecture Agent** to comprehensively map the authorization system: "Perform a complete authorization architecture analysis. Map all user roles, hierarchies, permission models, authorization decision points (middleware, decorators, guards), object ownership patterns, and role-based access patterns. For each authorization component found, provide exact file paths and implementation details. Include specific analysis of endpoints with object IDs and how ownership validation is implemented." -4. **Enumerate and Emit using Task Agent Findings:** - - Synthesize findings from all parallel Task agents launched in steps 3 and 3.5 - - Use their exact file paths, code locations, and analysis to populate the MCP tool calls - - Cross-reference browser observations with Task agent source code findings to create comprehensive attack surface maps - - Emit findings via the MCP tools listed in `` — the renderer produces the deliverable Markdown from your tool calls +4. **Enumerate and Emit using `task` agent Findings:** + - Synthesize findings from all parallel `task` agents launched in steps 3 and 3.5 + - Use their exact file paths, code locations, and analysis to populate the tool calls + - Cross-reference browser observations with `task` agent source code findings to create comprehensive attack surface maps + - Emit findings via the tools listed in `` — the renderer produces the deliverable Markdown from your tool calls - -**Emit your findings exclusively via the `recon-collector` MCP tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself. + +**Emit your findings exclusively via the deliverable tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself. -**When to emit.** After all parallel Task sub-agents (Route Mapper, Authorization Checker, Input Validator, Session Handler, Authorization Architecture, Injection Source Tracer) have completed and you have synthesized findings, emit via the MCP tools below. +**When to emit.** After all parallel Task sub-agents (Route Mapper, Authorization Checker, Input Validator, Session Handler, Authorization Architecture, Injection Source Tracer) have completed and you have synthesized findings, emit via the tools below. **Required tools — call all nine before terminating.** Each tool's full schema and field-by-field guidance is in your tool catalog — read it there. @@ -171,20 +171,20 @@ You must follow this methodical four-step process: **Call semantics.** Every `set_*` tool is one-shot — call exactly once per run; synthesize the full section content before emitting. Duplicate `set_*` calls return `"already called"` and are no-ops. `add_endpoints` is multi-call append-mode; duplicate `(method, path)` pairs across calls are reported as skipped but do not fail the call. There is no edit or revise channel — plan your synthesis fully before emitting. -**Injection Source Tracer dispatch (for Section 9).** Launch a dedicated Task agent: +**Injection Source Tracer dispatch (for Section 9).** Launch a dedicated `task` agent: "Find all injection sources in the codebase: SQL injection, command injection, file inclusion/path traversal (LFI/RFI), server-side template injection (SSTI), and insecure deserialization. Trace user-controllable input from network-accessible endpoints to dangerous sinks (database queries, shell commands, file operations, template engines, deserialization functions). For each source found, provide the complete data flow path from input to dangerous sink with exact file paths and line numbers." **Network Surface Focus (applies to every tool):** Only emit components, endpoints, input vectors, and injection sources that are reachable through the target web application's network interface. Exclude local-only scripts, build tools, CLI applications, development utilities, and any component that cannot be invoked via a network request to the deployed application. - + **COMPLETION REQUIREMENTS (ALL must be satisfied):** 1. **Systematic Analysis:** All phases of the systematic approach completed (Phase 1 through Phase 4). -2. **MCP Emission:** All nine MCP tools listed in `` have been called (eight `set_*` tools plus `add_endpoints` with at least one endpoint). -3. **TodoWrite Completion:** All tasks in your todo list marked completed. +2. **Deliverable Emission:** All nine tools listed in `` have been called (eight `set_*` tools plus `add_endpoints` with at least one endpoint). +3. **`todo_write` Completion:** All tasks in your todo list marked completed. **ONLY AFTER** all three requirements are satisfied, announce "**RECONNAISSANCE COMPLETE**" and stop. -**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the host renders the deliverable from your MCP tool calls and it contains everything needed. +**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the host renders the deliverable from your tool calls and it contains everything needed. diff --git a/apps/worker/prompts/report-executive.txt b/apps/worker/prompts/report-executive.txt index d06cc7d..bcb3be1 100644 --- a/apps/worker/prompts/report-executive.txt +++ b/apps/worker/prompts/report-executive.txt @@ -21,7 +21,6 @@ IMPORTANT: You are MODIFYING an existing file, not creating a new one. URL: {{WEB_URL}} -{{DESCRIPTION}} Filesystem: - {{REPO_PATH}}/ (read only) diff --git a/apps/worker/prompts/shared/_code-path-rules.txt b/apps/worker/prompts/shared/_code-path-rules.txt index eb95b47..4fa83d4 100644 --- a/apps/worker/prompts/shared/_code-path-rules.txt +++ b/apps/worker/prompts/shared/_code-path-rules.txt @@ -2,8 +2,8 @@ Source-code routing. Each rule is tagged `[FILE]` (literal path) or `[GLOB]` (pattern). All paths are repository-relative. How to apply (focus rules): -- For `[FILE]` entries — delegate analysis to the Task tool. -- For `[GLOB]` entries — invoke the Glob tool to enumerate matches, then delegate analysis of every match to the Task tool. +- For `[FILE]` entries — delegate analysis to the `task` tool. +- For `[GLOB]` entries — use the `glob` tool to enumerate matches, then delegate analysis of every match to the `task` tool. Avoid — out of scope. Skip entirely; the tool layer will block any access attempts. {{CODE_RULES_AVOID}} diff --git a/apps/worker/prompts/shared/login-instructions.txt b/apps/worker/prompts/shared/login-instructions.txt index 01155fe..6a88756 100644 --- a/apps/worker/prompts/shared/login-instructions.txt +++ b/apps/worker/prompts/shared/login-instructions.txt @@ -16,7 +16,7 @@ Execute the login flow based on the login_type specified in the configuration: 2. Execute each step in the login_flow array sequentially: - Replace $username with the provided username credential - Replace $password with the provided password credential - - Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the Bash tool + - Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the `bash` tool - Perform the specified actions (type text, click buttons, etc.) 3. Wait for page navigation/loading to complete after each critical step 4. Handle any consent dialogs or "Continue as [user]" prompts by clicking appropriate buttons @@ -30,7 +30,7 @@ Execute the login flow based on the login_type specified in the configuration: - Handle account selection if prompted - Replace $username with the provided username credential in provider login - Replace $password with the provided password credential in provider login - - Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the Bash tool + - Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the `bash` tool - Handle OAuth consent screens by clicking "Allow", "Accept", or "Continue", and hitting check boxes as needed. - Handle "Continue as [username]" dialogs by clicking "Continue" 3. Wait for OAuth callback and final redirect to complete diff --git a/apps/worker/prompts/validate-authentication.txt b/apps/worker/prompts/validate-authentication.txt index 5fc5d1d..9dd9aa9 100644 --- a/apps/worker/prompts/validate-authentication.txt +++ b/apps/worker/prompts/validate-authentication.txt @@ -12,7 +12,7 @@ This runs as a preflight check for our AI pentester. The user supplies credentia - **Browser Automation (playwright-cli skill):** Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **generate-totp (CLI Tool):** Run `generate-totp --secret ` via the Bash tool to produce a current TOTP code when the login flow requires one. +- **generate-totp (CLI Tool):** Run `generate-totp --secret ` via the `bash` tool to produce a current TOTP code when the login flow requires one. diff --git a/apps/worker/prompts/vuln-auth.txt b/apps/worker/prompts/vuln-auth.txt index 36cbabd..7acaea3 100644 --- a/apps/worker/prompts/vuln-auth.txt +++ b/apps/worker/prompts/vuln-auth.txt @@ -75,15 +75,15 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a **CRITICAL TOOL USAGE RESTRICTIONS:** -- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent. -- ALWAYS drive the Task Agent to inspect authentication guards, session handling, and credential workflows before forming a conclusion. -- Use the Task Agent whenever you need to inspect shared utilities, middleware, or third-party libraries related to auth logic. +- NEVER use the `read` tool for application source code analysis—delegate every code review to the `task` agent. +- ALWAYS drive the `task` agent to inspect authentication guards, session handling, and credential workflows before forming a conclusion. +- Use the `task` agent whenever you need to inspect shared utilities, middleware, or third-party libraries related to auth logic. **Available Tools:** -- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication logic paths, and understand session/credential handling. MANDATORY for all source code analysis. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication logic paths, and understand session/credential handling. MANDATORY for all source code analysis. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. - **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows like password reset or registration), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint/flow that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint/flow that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. @@ -112,11 +112,11 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a # White-Box Vulnerability Analysis Procedure: Broken Authentication (AuthN-only) -NOTE ON "FOR ALL": whenever an item begins with "for all …", use the TodoWrite tool to create a separate task for each relevant endpoint/flow that needs to be analyzed. +NOTE ON "FOR ALL": whenever an item begins with "for all …", use the `todo_write` tool to create a separate task for each relevant endpoint/flow that needs to be analyzed. From `.shannon/deliverables/pre_recon_deliverable.md`, use Section 3 and 6 to help guide your tasks. ## 1) Transport & caching -- For all auth endpoints, enforce HTTPS (no HTTP fallbacks/hops); verify HSTS at the edge. (for all: use TodoWrite tool to add each endpoint as a task) +- For all auth endpoints, enforce HTTPS (no HTTP fallbacks/hops); verify HSTS at the edge. (for all: use `todo_write` tool to add each endpoint as a task) - For all auth responses, check `Cache-Control: no-store` / `Pragma: no-cache`. **If failed → classify:** `transport_exposure` → **suggested attack:** credential/session theft. @@ -194,35 +194,36 @@ For each check you perform from the list above (Transport, Rate Limiting, Sessio - -After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 3 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. + +After completing your `todo_write` tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot tools. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. **Tool catalog:** - `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns) - `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with auth-specific sub-fields: authentication method, session token details, password policy) - `set_safe_vectors` — Section 4 (Secure by Design: Validated Components) +- `set_blind_spots` — Section 5 (analysis constraints and blind spots) -The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. +The harness injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. -**Call semantics:** All 3 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. +**Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. **Required vs recommended:** - `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-auth` agent reads. -- `set_safe_vectors` is recommended. An empty array is acceptable on runs with no validated-secure components, but explicit emission is preferred over skipping. +- `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure components or no constraint gaps, but explicit emission is preferred over skipping. -**Relationship to the exploitation queue:** The exploitation queue (`auth_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 3 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. - +**Relationship to the exploitation queue:** The exploitation queue (`auth_exploitation_queue.json`) is produced by calling the `submit_exploitation_queue` tool when your analysis is complete. The 4 tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. + **COMPLETION REQUIREMENTS (ALL must be satisfied):** 1. **Systematic Analysis:** ALL relevant API endpoints and user-facing features identified in the reconnaissance deliverable must be analyzed for AuthN/AuthZ flaws. -2. **Deliverable Emission:** Call the 3 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` is recommended (an empty array is acceptable but explicit emission is preferred). +2. **Deliverable Emission:** Call the 4 tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays are acceptable but explicit emission is preferred). -**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls. +**Note:** The exploitation queue is produced by calling the `submit_exploitation_queue` tool when your analysis is complete — separate from the tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the tool calls. -**ONLY AFTER** both systematic analysis AND the required MCP tool calls have been completed, announce "**AUTH ANALYSIS COMPLETE**" and stop. +**ONLY AFTER** both systematic analysis AND the required tool calls have been completed, announce "**AUTH ANALYSIS COMPLETE**" and stop. **CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed. diff --git a/apps/worker/prompts/vuln-authz.txt b/apps/worker/prompts/vuln-authz.txt index 2059801..fd32e34 100644 --- a/apps/worker/prompts/vuln-authz.txt +++ b/apps/worker/prompts/vuln-authz.txt @@ -80,15 +80,15 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a **CRITICAL TOOL USAGE RESTRICTIONS:** -- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent. -- ALWAYS direct the Task Agent to follow authorization guard placement, role checks, and ownership validation before you reach a verdict. -- Use the Task Agent whenever you need to inspect shared middleware, decorators, or policy modules involved in access control. +- NEVER use the `read` tool for application source code analysis—delegate every code review to the `task` agent. +- ALWAYS direct the `task` agent to follow authorization guard placement, role checks, and ownership validation before you reach a verdict. +- Use the `task` agent whenever you need to inspect shared middleware, decorators, or policy modules involved in access control. **Available Tools:** -- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authorization logic paths, and understand permission models. MANDATORY for all source code analysis. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authorization logic paths, and understand permission models. MANDATORY for all source code analysis. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. - **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows and role-based access controls), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint that needs authorization analysis. Mark items as "in_progress" when working on them and "completed" when done. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint that needs authorization analysis. Mark items as "in_progress" when working on them and "completed" when done. @@ -126,7 +126,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a ### 1) Horizontal Authorization Analysis - **Create To Dos:** - For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Horizontal"*, use the TodoWrite tool to create a task entry. + For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Horizontal"*, use the `todo_write` tool to create a task entry. - **Process:** - Start at the identified endpoint. @@ -158,7 +158,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a ### 2) Vertical Authorization Analysis - **Create To Dos:** - For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Vertical"**, use the TodoWrite tool to create a task entry. + For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Vertical"**, use the `todo_write` tool to create a task entry. - **Process:** - Start at the identified endpoint. @@ -184,7 +184,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a ### 3) Context / Workflow Authorization Analysis - **Create To Dos:** - For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Context"**, use the TodoWrite tool to create a task entry. + For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Context"**, use the `todo_write` tool to create a task entry. - **Process:** - Start at the endpoint that represents a step in a workflow. @@ -272,8 +272,8 @@ For each analysis you perform from the lists above, you must make a final **verd - -After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. + +After completing your `todo_write` tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot tools. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. **Tool catalog:** - `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns) @@ -281,7 +281,7 @@ After completing your TodoWrite tasks and synthesizing findings, emit your speci - `set_safe_vectors` — Section 4 (vectors confirmed secure) - `set_blind_spots` — Section 5 (analysis constraints and blind spots) -The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For authz specifically, when populating `set_safe_vectors`, the renderer maps `subject` to the "Endpoint" column header and `location` to the "Guard Location" column header. +The harness injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For authz specifically, when populating `set_safe_vectors`, the renderer maps `subject` to the "Endpoint" column header and `location` to the "Guard Location" column header. **Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. @@ -289,21 +289,21 @@ The MCP SDK injects each tool's complete description and per-field guidance into - `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-authz` agent reads. - `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure endpoints or no constraint gaps, but explicit emission is preferred over skipping. -**Relationship to the exploitation queue:** The exploitation queue (`authz_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. - +**Relationship to the exploitation queue:** The exploitation queue (`authz_exploitation_queue.json`) is produced by calling the `submit_exploitation_queue` tool when your analysis is complete. The 4 tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. + **COMPLETION REQUIREMENTS (ALL must be satisfied):** -1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed". -2. **Deliverable Emission:** Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). +1. **Todo Completion:** ALL tasks in your `todo_write` list must be marked as "completed". +2. **Deliverable Emission:** Call the 4 tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). -**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls. +**Note:** The exploitation queue is produced by calling the `submit_exploitation_queue` tool when your analysis is complete — separate from the tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the tool calls. -**ONLY AFTER** both todo completion AND the required MCP tool calls have been completed, announce "**AUTHORIZATION ANALYSIS COMPLETE**" and stop. +**ONLY AFTER** both todo completion AND the required tool calls have been completed, announce "**AUTHORIZATION ANALYSIS COMPLETE**" and stop. **CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed. -**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit MCP deliverable calls before completing comprehensive testing of all authorization vectors. +**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit deliverable calls before completing comprehensive testing of all authorization vectors. diff --git a/apps/worker/prompts/vuln-injection.txt b/apps/worker/prompts/vuln-injection.txt index 464b7fb..bf9008e 100644 --- a/apps/worker/prompts/vuln-injection.txt +++ b/apps/worker/prompts/vuln-injection.txt @@ -35,7 +35,7 @@ Success criterion: Complete source-to-sink traces detailing path, sanitizers, si - **Severity Context:** A structural flaw in a backend command, whether an SQL query or a shell command, is one of the most severe classes of vulnerability in a SaaS application. It undermines the foundational trust of the system by creating the *potential* for data exposure (SQLi) or direct server compromise (Command Injection). - **Your Role is Precise:** Your sole responsibility is to identify and precisely document these structural command flaws. You prove the *potential* for compromise; the Exploitation phase confirms the *realized* compromise. Do not cross this boundary. - **Code is Ground Truth:** Your analysis must be rooted in the application's code. An observed behavior is only a symptom; the insecure data flow within the code is the root cause you must identify. -- **Thoroughness is Non-Negotiable:** An incomplete analysis is a failed analysis. Finding one flaw is merely the first data point. Your mission is only complete when **every potential data entry point** from the reconnaissance deliverable has been systematically analyzed and documented using the TodoWrite tool. **Do not terminate early.** +- **Thoroughness is Non-Negotiable:** An incomplete analysis is a failed analysis. Finding one flaw is merely the first data point. Your mission is only complete when **every potential data entry point** from the reconnaissance deliverable has been systematically analyzed and documented using the `todo_write` tool. **Do not terminate early.** @@ -80,15 +80,15 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en **CRITICAL TOOL USAGE RESTRICTIONS:** -- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent. -- ALWAYS direct the Task Agent to trace tainted data flow, sanitization/encoding steps, and sink construction before you reach a verdict. -- Use the Task Agent instead of Bash or Playwright when you need to inspect handlers, middleware, or shared utilities to follow an injection path. +- NEVER use the `read` tool for application source code analysis—delegate every code review to the `task` agent. +- ALWAYS direct the `task` agent to trace tainted data flow, sanitization/encoding steps, and sink construction before you reach a verdict. +- Use the `task` agent instead of Bash or Playwright when you need to inspect handlers, middleware, or shared utilities to follow an injection path. **Available Tools:** -- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, map query/command construction paths, and verify sanitization coverage. MANDATORY for all source code analysis. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, map query/command construction paths, and verify sanitization coverage. MANDATORY for all source code analysis. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. - **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows like password reset or registration), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each injection source that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create a todo item for each injection source that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. @@ -125,7 +125,7 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en - **Goal:** Prove whether untrusted input can influence the **structure** of a backend command (SQL or Shell) or reach sensitive **slots** without the correct defense. No live exploitation in this phase. - **1) Create a To Do for each Injection Source found in the Pre-Recon Deliverable - - inside of .shannon/deliverables/pre_recon_deliverable.md under the section "7. Injection Sources (Command Injection and SQL Injection)" use the TodoWrite tool to create a task for each discovered Injection Source. + - inside of .shannon/deliverables/pre_recon_deliverable.md under the section "7. Injection Sources (Command Injection and SQL Injection)" use the `todo_write` tool to create a task for each discovered Injection Source. - Note: All sources are marked as Tainted until they Hit a Santiization that matches the sink context. normalizers (lowercasing, trimming, JSON parse, schema decode) — still **tainted**. - **2) Trace Data Flow Paths from Source to Sink** - For each source, your goal is to identify every unique "Data Flow Path" to a database sink. A path is a distinct route the data takes through the code. @@ -283,8 +283,8 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en - -After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. + +After completing your `todo_write` tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot tools. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. **Tool catalog:** - `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns) @@ -292,7 +292,7 @@ After completing your TodoWrite tasks and synthesizing findings, emit your speci - `set_safe_vectors` — Section 4 (vectors confirmed secure) - `set_blind_spots` — Section 5 (analysis constraints and blind spots) -The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. +The harness injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. **Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. @@ -300,21 +300,21 @@ The MCP SDK injects each tool's complete description and per-field guidance into - `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-injection` agent reads. - `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure vectors or no constraint gaps, but explicit emission is preferred over skipping. -**Relationship to the exploitation queue:** The exploitation queue (`injection_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. - +**Relationship to the exploitation queue:** The exploitation queue (`injection_exploitation_queue.json`) is produced by calling the `submit_exploitation_queue` tool when your analysis is complete. The 4 tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. + **COMPLETION REQUIREMENTS (ALL must be satisfied):** -1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed". -2. **Deliverable Emission:** Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). +1. **Todo Completion:** ALL tasks in your `todo_write` list must be marked as "completed". +2. **Deliverable Emission:** Call the 4 tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). -**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls. +**Note:** The exploitation queue is produced by calling the `submit_exploitation_queue` tool when your analysis is complete — separate from the tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the tool calls. -**ONLY AFTER** both todo completion AND the required MCP tool calls have been completed, announce "**INJECTION ANALYSIS COMPLETE**" and stop. +**ONLY AFTER** both todo completion AND the required tool calls have been completed, announce "**INJECTION ANALYSIS COMPLETE**" and stop. **CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed. -**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit MCP deliverable calls before completing comprehensive testing of all input vectors. +**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit deliverable calls before completing comprehensive testing of all input vectors. diff --git a/apps/worker/prompts/vuln-ssrf.txt b/apps/worker/prompts/vuln-ssrf.txt index 2776ea9..5063cec 100644 --- a/apps/worker/prompts/vuln-ssrf.txt +++ b/apps/worker/prompts/vuln-ssrf.txt @@ -76,15 +76,15 @@ An **exploitable vulnerability** is a data flow where user-controlled input infl **CRITICAL TOOL USAGE RESTRICTIONS:** -- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent. -- ALWAYS drive the Task Agent to map user-controlled input to outbound HTTP clients, validation layers, and network controls before declaring a result. -- Use the Task Agent to inspect shared utilities, proxy helpers, and request builders instead of reading files directly. +- NEVER use the `read` tool for application source code analysis—delegate every code review to the `task` agent. +- ALWAYS drive the `task` agent to map user-controlled input to outbound HTTP clients, validation layers, and network controls before declaring a result. +- Use the `task` agent to inspect shared utilities, proxy helpers, and request builders instead of reading files directly. **Available Tools:** -- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace data flows, and understand HTTP client usage. MANDATORY for all source code analysis. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`task` agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace data flows, and understand HTTP client usage. MANDATORY for all source code analysis. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. - **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows involving URL redirection or proxy functionality), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each SSRF sink that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create a todo item for each SSRF sink that needs analysis. Mark items as "in_progress" when working on them and "completed" when done. @@ -114,7 +114,7 @@ An **exploitable vulnerability** is a data flow where user-controlled input infl # White-Box Vulnerability Analysis Procedure: Server-Side Request Forgery (SSRF) -NOTE ON "FOR ALL": whenever an item begins with "for all …", use the TodoWrite tool to create a separate task for each relevant endpoint/flow that needs to be analyzed. +NOTE ON "FOR ALL": whenever an item begins with "for all …", use the `todo_write` tool to create a separate task for each relevant endpoint/flow that needs to be analyzed. From `.shannon/deliverables/pre_recon_deliverable.md`, use Section 10 (SSRF Sinks) to guide your tasks. ## 1) Identify HTTP Client Usage Patterns @@ -169,7 +169,7 @@ From `.shannon/deliverables/pre_recon_deliverable.md`, use Section 10 (SSRF Sink Inside `.shannon/deliverables/pre_recon_deliverable.md` under section `##10. SSRF Sinks##`. -Use the TodoWrite tool to create a task for each discovered sink (any server-side request composed even partially from user input). +Use the `todo_write` tool to create a task for each discovered sink (any server-side request composed even partially from user input). --- @@ -243,35 +243,36 @@ For each check you perform from the list above, you must make a final **verdict* - -After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 3 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. + +After completing your `todo_write` tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot tools. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. **Tool catalog:** - `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns) - `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with SSRF-specific sub-fields: HTTP client library, request architecture, internal services) - `set_safe_vectors` — Section 4 (Secure by Design: Validated Components) +- `set_blind_spots` — Section 5 (analysis constraints and blind spots) -The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. +The harness injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. -**Call semantics:** All 3 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. +**Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. **Required vs recommended:** - `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-ssrf` agent reads. -- `set_safe_vectors` is recommended. An empty array is acceptable on runs with no validated-secure components, but explicit emission is preferred over skipping. +- `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure components or no constraint gaps, but explicit emission is preferred over skipping. -**Relationship to the exploitation queue:** The exploitation queue (`ssrf_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 3 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. - +**Relationship to the exploitation queue:** The exploitation queue (`ssrf_exploitation_queue.json`) is produced by calling the `submit_exploitation_queue` tool when your analysis is complete. The 4 tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. + **COMPLETION REQUIREMENTS (ALL must be satisfied):** 1. **Systematic Analysis:** ALL relevant API endpoints and request-making features identified in the reconnaissance deliverable must be analyzed for SSRF vulnerabilities. -2. **Deliverable Emission:** Call the 3 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` is recommended (an empty array is acceptable but explicit emission is preferred). +2. **Deliverable Emission:** Call the 4 tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays are acceptable but explicit emission is preferred). -**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls. +**Note:** The exploitation queue is produced by calling the `submit_exploitation_queue` tool when your analysis is complete — separate from the tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the tool calls. -**ONLY AFTER** both systematic analysis AND the required MCP tool calls have been completed, announce "**SSRF ANALYSIS COMPLETE**" and stop. +**ONLY AFTER** both systematic analysis AND the required tool calls have been completed, announce "**SSRF ANALYSIS COMPLETE**" and stop. **CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed. diff --git a/apps/worker/prompts/vuln-xss.txt b/apps/worker/prompts/vuln-xss.txt index 52d5ea3..e73fdc5 100644 --- a/apps/worker/prompts/vuln-xss.txt +++ b/apps/worker/prompts/vuln-xss.txt @@ -77,17 +77,17 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en **CRITICAL TOOL USAGE RESTRICTIONS:** -- NEVER use the Read tool for application source code analysis - ALWAYS delegate to Task agents for examining .js, .ts, .py, .php files and application logic. You MAY use Read +- NEVER use the `read` tool for application source code analysis - ALWAYS delegate to `task` agents for examining .js, .ts, .py, .php files and application logic. You MAY use Read tool directly for these files: `.shannon/deliverables/pre_recon_deliverable.md`, `.shannon/deliverables/recon_deliverable.md` -- Direct the Task Agent to trace render contexts, sanitization coverage, and template/component boundaries before deciding on exploitability. -- **ALWAYS delegate code analysis to Task agents** +- Direct the `task` agent to trace render contexts, sanitization coverage, and template/component boundaries before deciding on exploitability. +- **ALWAYS delegate code analysis to `task` agents** **Available Tools:** -- **Task Agent (Code Analysis):** MANDATORY for all source code analysis and data flow tracing. Use this instead of Read tool for examining application code, models, controllers, and templates. +- **`task` agent (Code Analysis):** MANDATORY for all source code analysis and data flow tracing. Use this instead of `read` tool for examining application code, models, controllers, and templates. - **Terminal (curl):** MANDATORY for testing HTTP-based XSS vectors and observing raw HTML responses. Use for reflected XSS testing and JSONP injection testing. - **Browser Automation (playwright-cli skill):** MANDATORY for testing DOM-based XSS and form submission vectors. Invoke the `playwright-cli` skill to learn available commands. Use for stored XSS testing and client-side payload execution verification. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation. -- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each sink you need to analyze. -- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed. +- **`todo_write` Tool:** Use this to create and manage your analysis task list. Create a todo item for each sink you need to analyze. +- **`bash` tool:** Use for creating directories, copying files, and other shell commands as needed. @@ -124,11 +124,11 @@ Structure: The vulnerability JSON object MUST follow this exact format: - **Goal:** Identify vulnerable data flow paths by starting at the XSS sinks received from the recon phase and tracing backward to their sanitizations and sources. This approach is optimized for finding all types of XSS, especially complex Stored XSS patterns. - **Core Principle:** Data is assumed to be tainted until a context-appropriate output encoder (sanitization) is encountered on its path to the sink. -### **1) Create a todo item for each XSS sink using the TodoWrite tool** -Read .shannon/deliverables/pre_recon_deliverable.md section ##9. XSS Sinks and Render Contexts## and use the **TodoWrite tool** to create a todo item for each discovered sink-context pair that needs analysis. +### **1) Create a todo item for each XSS sink using the `todo_write` tool** +Read .shannon/deliverables/pre_recon_deliverable.md section ##9. XSS Sinks and Render Contexts## and use the **`todo_write` tool** to create a todo item for each discovered sink-context pair that needs analysis. ### **2) Trace Each Sink Backward (Backward Taint Analysis)** -For each pending item in your todo list (managed via TodoWrite tool), trace the origin of the data variable backward from the sink through the application logic. Your goal is to find either a valid sanitizer or an untrusted source. Mark each todo item as completed after you've fully analyzed that sink. +For each pending item in your todo list (managed via `todo_write` tool), trace the origin of the data variable backward from the sink through the application logic. Your goal is to find either a valid sanitizer or an untrusted source. Mark each todo item as completed after you've fully analyzed that sink. - **Early Termination for Secure Paths (Efficiency Rule):** - As you trace backward, if you encounter a sanitization/encoding function, immediately perform two checks: @@ -205,8 +205,8 @@ This rulebook is used for the **Early Termination** check in Step 2. - -After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. + +After completing your `todo_write` tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot tools. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content. **Tool catalog:** - `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns) @@ -214,7 +214,7 @@ After completing your TodoWrite tasks and synthesizing findings, emit your speci - `set_safe_vectors` — Section 4 (vectors confirmed secure) - `set_blind_spots` — Section 5 (analysis constraints and blind spots) -The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For XSS specifically, when populating `set_safe_vectors`, include the optional `render_context` field on each entry (HTML_BODY, HTML_ATTRIBUTE, JAVASCRIPT_STRING, URL_PARAM, or CSS_VALUE). +The harness injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For XSS specifically, when populating `set_safe_vectors`, include the optional `render_context` field on each entry (HTML_BODY, HTML_ATTRIBUTE, JAVASCRIPT_STRING, URL_PARAM, or CSS_VALUE). **Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting. @@ -222,19 +222,19 @@ The MCP SDK injects each tool's complete description and per-field guidance into - `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-xss` agent reads. - `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure vectors or no constraint gaps, but explicit emission is preferred over skipping. -**Relationship to the exploitation queue:** The exploitation queue (`xss_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. - +**Relationship to the exploitation queue:** The exploitation queue (`xss_exploitation_queue.json`) is produced by calling the `submit_exploitation_queue` tool when your analysis is complete. The 4 tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above. + COMPLETION REQUIREMENTS (ALL must be satisfied): 1. Systematic Analysis: ALL input vectors identified from the reconnaissance deliverable must be analyzed. -2. Deliverable Emission: Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). +2. Deliverable Emission: Call the 4 tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred). -**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls. +**Note:** The exploitation queue is produced by calling the `submit_exploitation_queue` tool when your analysis is complete — separate from the tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the tool calls. -ONLY AFTER both systematic analysis AND the required MCP tool calls have been completed, announce "XSS ANALYSIS COMPLETE" and stop. +ONLY AFTER both systematic analysis AND the required tool calls have been completed, announce "XSS ANALYSIS COMPLETE" and stop. **CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed. diff --git a/apps/worker/src/ai/claude-executor.ts b/apps/worker/src/ai/claude-executor.ts deleted file mode 100644 index 622158e..0000000 --- a/apps/worker/src/ai/claude-executor.ts +++ /dev/null @@ -1,404 +0,0 @@ -// 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. - -// Production Claude agent execution with retry, git checkpoints, and audit logging - -import { type JsonSchemaOutputFormat, query } from '@anthropic-ai/claude-agent-sdk'; -import { fs, path } from 'zx'; -import type { AuditSession } from '../audit/index.js'; -import { deliverablesDir } from '../paths.js'; -import { isRetryableError, PentestError } from '../services/error-handling.js'; -import { AGENT_VALIDATORS } from '../session-manager.js'; -import type { ActivityLogger } from '../types/activity-logger.js'; -import { isSpendingCapBehavior } from '../utils/billing-detection.js'; -import { formatTimestamp } from '../utils/formatting.js'; -import { Timer } from '../utils/metrics.js'; -import { createAuditLogger } from './audit-logger.js'; -import { dispatchMessage } from './message-handlers.js'; -import { type ModelTier, resolveModel, supportsAdaptiveThinking } from './models.js'; -import { detectExecutionContext, formatCompletionMessage, formatErrorOutput } from './output-formatters.js'; -import { createProgressManager } from './progress-manager.js'; - -declare global { - var SHANNON_DISABLE_LOADER: boolean | undefined; -} - -export interface ClaudePromptResult { - result?: string | null | undefined; - success: boolean; - duration: number; - turns?: number | undefined; - cost: number; - model?: string | undefined; - partialCost?: number | undefined; - apiErrorDetected?: boolean | undefined; - error?: string | undefined; - errorType?: string | undefined; - prompt?: string | undefined; - retryable?: boolean | undefined; - structuredOutput?: unknown; -} - -function outputLines(lines: string[]): void { - for (const line of lines) { - console.log(line); - } -} - -async function writeErrorLog( - err: Error & { code?: string; status?: number }, - sourceDir: string, - fullPrompt: string, - duration: number, -): Promise { - try { - const errorLog = { - timestamp: formatTimestamp(), - agent: 'claude-executor', - error: { - name: err.constructor.name, - message: err.message, - code: err.code, - status: err.status, - stack: err.stack, - }, - context: { - sourceDir, - prompt: `${fullPrompt.slice(0, 200)}...`, - retryable: isRetryableError(err), - }, - duration, - }; - const logPath = path.join(deliverablesDir(sourceDir), 'error.log'); - await fs.appendFile(logPath, `${JSON.stringify(errorLog)}\n`); - } catch { - // Best-effort error log writing - don't propagate failures - } -} - -export async function validateAgentOutput( - result: ClaudePromptResult, - agentName: string | null, - sourceDir: string, - logger: ActivityLogger, -): Promise { - logger.info(`Validating ${agentName} agent output`); - - try { - // Check if agent completed successfully (text result OR structured output) - if (!result.success || (!result.result && result.structuredOutput === undefined)) { - logger.error('Validation failed: Agent execution was unsuccessful'); - return false; - } - - // Get validator function for this agent - const validator = agentName ? AGENT_VALIDATORS[agentName as keyof typeof AGENT_VALIDATORS] : undefined; - - if (!validator) { - logger.warn(`No validator found for agent "${agentName}" - assuming success`); - logger.info('Validation passed: Unknown agent with successful result'); - return true; - } - - logger.info(`Using validator for agent: ${agentName}`, { sourceDir }); - - // Apply validation function - const validationResult = await validator(sourceDir, logger); - - if (validationResult) { - logger.info('Validation passed: Required files/structure present'); - } else { - logger.error('Validation failed: Missing required deliverable files'); - } - - return validationResult; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - logger.error(`Validation failed with error: ${errMsg}`); - return false; - } -} - -// Low-level SDK execution. Handles message streaming, progress, and audit logging. -// Exported for Temporal activities to call single-attempt execution. -export async function runClaudePrompt( - prompt: string, - sourceDir: string, - context: string = '', - description: string = 'Claude analysis', - _agentName: string | null = null, - auditSession: AuditSession | null = null, - logger: ActivityLogger, - modelTier: ModelTier = 'medium', - outputFormat?: JsonSchemaOutputFormat, - apiKey?: string, - deliverablesSubdir?: string, - providerConfig?: import('../types/config.js').ProviderConfig, - mcpServers?: Record, -): Promise { - // 1. Initialize timing and prompt - const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`); - const fullPrompt = context ? `${context}\n\n${prompt}` : prompt; - - // 2. Set up progress and audit infrastructure - const execContext = detectExecutionContext(description); - const progress = createProgressManager( - { description, useCleanOutput: execContext.useCleanOutput }, - global.SHANNON_DISABLE_LOADER ?? false, - ); - const auditLogger = createAuditLogger(auditSession); - - logger.info(`Running Claude Code: ${description}...`); - - // 3. Build env vars to pass to SDK subprocesses - const sdkEnv: Record = { - CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || '64000', - PLAYWRIGHT_MCP_OUTPUT_DIR: deliverablesSubdir - ? path.join(sourceDir, path.dirname(deliverablesSubdir), '.playwright-cli') - : path.join(sourceDir, '.shannon', '.playwright-cli'), - // apiKey from ContainerConfig takes precedence over process.env - ...(apiKey && { ANTHROPIC_API_KEY: apiKey }), - // Deliverables subdir for save-deliverable CLI tool - ...(deliverablesSubdir && { SHANNON_DELIVERABLES_SUBDIR: deliverablesSubdir }), - }; - - // 3a. Apply structured provider config directly to sdkEnv (no process.env mutation) - if (providerConfig) { - switch (providerConfig.providerType) { - case 'bedrock': - sdkEnv.CLAUDE_CODE_USE_BEDROCK = '1'; - if (providerConfig.awsRegion) sdkEnv.AWS_REGION = providerConfig.awsRegion; - if (providerConfig.awsAccessKeyId) sdkEnv.AWS_ACCESS_KEY_ID = providerConfig.awsAccessKeyId; - if (providerConfig.awsSecretAccessKey) sdkEnv.AWS_SECRET_ACCESS_KEY = providerConfig.awsSecretAccessKey; - break; - case 'vertex': - sdkEnv.CLAUDE_CODE_USE_VERTEX = '1'; - if (providerConfig.gcpRegion) sdkEnv.CLOUD_ML_REGION = providerConfig.gcpRegion; - if (providerConfig.gcpProjectId) sdkEnv.ANTHROPIC_VERTEX_PROJECT_ID = providerConfig.gcpProjectId; - if (providerConfig.gcpCredentialsPath) - sdkEnv.GOOGLE_APPLICATION_CREDENTIALS = providerConfig.gcpCredentialsPath; - break; - case 'litellm_router': - if (providerConfig.baseUrl) sdkEnv.ANTHROPIC_BASE_URL = providerConfig.baseUrl; - if (providerConfig.authToken) sdkEnv.ANTHROPIC_AUTH_TOKEN = providerConfig.authToken; - break; - default: - // 'anthropic_api' or unset — apiKey already handled above - if (providerConfig.apiKey && !apiKey) sdkEnv.ANTHROPIC_API_KEY = providerConfig.apiKey; - break; - } - } - - // 3b. Passthrough env vars not already set by providerConfig or apiKey - const passthroughVars = [ - ...(!sdkEnv.ANTHROPIC_API_KEY ? ['ANTHROPIC_API_KEY'] : []), - 'CLAUDE_CODE_OAUTH_TOKEN', - ...(!sdkEnv.ANTHROPIC_BASE_URL ? ['ANTHROPIC_BASE_URL'] : []), - ...(!sdkEnv.ANTHROPIC_AUTH_TOKEN ? ['ANTHROPIC_AUTH_TOKEN'] : []), - ...(!sdkEnv.CLAUDE_CODE_USE_BEDROCK ? ['CLAUDE_CODE_USE_BEDROCK'] : []), - ...(!sdkEnv.AWS_REGION ? ['AWS_REGION'] : []), - 'AWS_BEARER_TOKEN_BEDROCK', - ...(!sdkEnv.CLAUDE_CODE_USE_VERTEX ? ['CLAUDE_CODE_USE_VERTEX'] : []), - ...(!sdkEnv.CLOUD_ML_REGION ? ['CLOUD_ML_REGION'] : []), - ...(!sdkEnv.ANTHROPIC_VERTEX_PROJECT_ID ? ['ANTHROPIC_VERTEX_PROJECT_ID'] : []), - ...(!sdkEnv.GOOGLE_APPLICATION_CREDENTIALS ? ['GOOGLE_APPLICATION_CREDENTIALS'] : []), - 'HOME', - 'PATH', - 'PLAYWRIGHT_MCP_EXECUTABLE_PATH', - ]; - for (const name of passthroughVars) { - const val = process.env[name]; - if (val) { - sdkEnv[name] = val; - } - } - - // 4. Configure SDK options - // Model override from providerConfig takes precedence over env-based resolveModel - const model = providerConfig?.modelOverrides?.[modelTier] ?? resolveModel(modelTier); - const adaptiveThinking = supportsAdaptiveThinking(model) && process.env.CLAUDE_ADAPTIVE_THINKING !== 'false'; - const options = { - model, - maxTurns: 10_000, - cwd: sourceDir, - permissionMode: 'bypassPermissions' as const, - allowDangerouslySkipPermissions: true, - settingSources: ['user'] as ('user' | 'project' | 'local')[], - env: sdkEnv, - ...(adaptiveThinking && { thinking: { type: 'adaptive' as const } }), - ...(outputFormat && { outputFormat }), - ...(mcpServers && Object.keys(mcpServers).length > 0 && { mcpServers }), - }; - - if (!execContext.useCleanOutput) { - logger.info(`SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=BYPASS`); - } - - let turnCount = 0; - let result: string | null = null; - let apiErrorDetected = false; - let totalCost = 0; - - progress.start(); - - try { - // 6. Process the message stream - const messageLoopResult = await processMessageStream( - fullPrompt, - options, - { execContext, description, progress, auditLogger, logger }, - timer, - ); - - turnCount = messageLoopResult.turnCount; - result = messageLoopResult.result; - apiErrorDetected = messageLoopResult.apiErrorDetected; - totalCost = messageLoopResult.cost; - const model = messageLoopResult.model; - - // === SPENDING CAP SAFEGUARD === - // 7. Defense-in-depth: Detect spending cap that slipped through detectApiError(). - // Uses consolidated billing detection from utils/billing-detection.ts - if (isSpendingCapBehavior(turnCount, totalCost, result || '')) { - throw new PentestError( - `Spending cap likely reached (turns=${turnCount}, cost=$0): ${result?.slice(0, 100)}`, - 'billing', - true, // Retryable - Temporal will use 5-30 min backoff - ); - } - - // 8. Finalize successful result - const duration = timer.stop(); - - if (apiErrorDetected) { - logger.warn(`API Error detected in ${description} - will validate deliverables before failing`); - } - - progress.finish(formatCompletionMessage(execContext, description, turnCount, duration)); - - return { - result, - success: true, - duration, - turns: turnCount, - cost: totalCost, - model, - partialCost: totalCost, - apiErrorDetected, - ...(messageLoopResult.structuredOutput !== undefined && { - structuredOutput: messageLoopResult.structuredOutput, - }), - }; - } catch (error) { - // 9. Handle errors — log, write error file, return failure - const duration = timer.stop(); - - const err = error as Error & { code?: string; status?: number }; - - await auditLogger.logError(err, duration, turnCount); - progress.stop(); - outputLines(formatErrorOutput(err, execContext, description, duration, sourceDir, isRetryableError(err))); - await writeErrorLog(err, sourceDir, fullPrompt, duration); - - return { - error: err.message, - errorType: err.constructor.name, - prompt: `${fullPrompt.slice(0, 100)}...`, - success: false, - duration, - cost: totalCost, - retryable: isRetryableError(err), - }; - } -} - -interface MessageLoopResult { - turnCount: number; - result: string | null; - apiErrorDetected: boolean; - cost: number; - model?: string | undefined; - structuredOutput?: unknown; -} - -interface MessageLoopDeps { - execContext: ReturnType; - description: string; - progress: ReturnType; - auditLogger: ReturnType; - logger: ActivityLogger; -} - -async function processMessageStream( - fullPrompt: string, - options: NonNullable[0]['options']>, - deps: MessageLoopDeps, - timer: Timer, -): Promise { - const { execContext, description, progress, auditLogger, logger } = deps; - const HEARTBEAT_INTERVAL = 30000; - - let turnCount = 0; - let result: string | null = null; - let apiErrorDetected = false; - let cost = 0; - let model: string | undefined; - let structuredOutput: unknown | undefined; - let lastHeartbeat = Date.now(); - - for await (const message of query({ prompt: fullPrompt, options })) { - // Heartbeat logging when loader is disabled - const now = Date.now(); - if (global.SHANNON_DISABLE_LOADER && now - lastHeartbeat > HEARTBEAT_INTERVAL) { - logger.info(`[${Math.floor((now - timer.startTime) / 1000)}s] ${description} running... (Turn ${turnCount})`); - lastHeartbeat = now; - } - - // Increment turn count for assistant messages - if (message.type === 'assistant') { - turnCount++; - } - - const dispatchResult = await dispatchMessage(message as { type: string; subtype?: string }, turnCount, { - execContext, - description, - progress, - auditLogger, - logger, - }); - - if (dispatchResult.type === 'throw') { - throw dispatchResult.error; - } - - if (dispatchResult.type === 'complete') { - result = dispatchResult.result; - cost = dispatchResult.cost; - if (dispatchResult.structuredOutput !== undefined) { - structuredOutput = dispatchResult.structuredOutput; - } - break; - } - - if (dispatchResult.type === 'continue') { - if (dispatchResult.apiErrorDetected) { - apiErrorDetected = true; - } - if (dispatchResult.model) { - model = dispatchResult.model; - } - } - } - - return { - turnCount, - result, - apiErrorDetected, - cost, - model, - ...(structuredOutput !== undefined && { structuredOutput }), - }; -} diff --git a/apps/worker/src/ai/extensions/bash-timeout/index.ts b/apps/worker/src/ai/extensions/bash-timeout/index.ts new file mode 100644 index 0000000..138882e --- /dev/null +++ b/apps/worker/src/ai/extensions/bash-timeout/index.ts @@ -0,0 +1,47 @@ +/** + * pi extension: enforce a bounded timeout on every `bash` tool call. + * + * pi's built-in bash tool accepts an optional `timeout` (in seconds) but applies + * NO default and NO upper bound — an unbounded command (e.g. a `playwright-cli` + * browser action that never returns) hangs the agent indefinitely. This extension + * registers a `tool_call` pre-execution handler that blocks any `bash` invocation + * that omits `timeout` or sets it above the maximum, returning a message that tells + * the model how to re-run the command correctly. + */ + +import type { ExtensionAPI, ToolCallEvent, ToolCallEventResult } from '@earendil-works/pi-coding-agent'; +import { isToolCallEventType } from '@earendil-works/pi-coding-agent'; + +/** Recommended timeout (seconds) suggested to the model when it omits one. */ +const DEFAULT_TIMEOUT_SECONDS = 120; + +/** Hard upper bound (seconds) a single bash command may run. */ +const MAX_TIMEOUT_SECONDS = 600; + +function evaluateBashTimeout(timeout: number | undefined): ToolCallEventResult | undefined { + const hasValidTimeout = typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0; + if (!hasValidTimeout) { + return { + block: true, + reason: `Set bash 'timeout' (seconds). Default ${DEFAULT_TIMEOUT_SECONDS}s, max ${MAX_TIMEOUT_SECONDS}s.`, + }; + } + + if (timeout > MAX_TIMEOUT_SECONDS) { + return { + block: true, + reason: `bash 'timeout' ${timeout}s exceeds max ${MAX_TIMEOUT_SECONDS}s. Default ${DEFAULT_TIMEOUT_SECONDS}s, max ${MAX_TIMEOUT_SECONDS}s.`, + }; + } + + return undefined; +} + +export default function bashTimeoutExtension(pi: ExtensionAPI): void { + pi.on('tool_call', (event: ToolCallEvent): ToolCallEventResult | undefined => { + if (!isToolCallEventType('bash', event)) { + return undefined; + } + return evaluateBashTimeout(event.input.timeout); + }); +} diff --git a/apps/worker/src/ai/message-handlers.ts b/apps/worker/src/ai/message-handlers.ts deleted file mode 100644 index 68a87ea..0000000 --- a/apps/worker/src/ai/message-handlers.ts +++ /dev/null @@ -1,408 +0,0 @@ -// 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 type { SDKAssistantMessageError } from '@anthropic-ai/claude-agent-sdk'; -import { PentestError } from '../services/error-handling.js'; -import type { ActivityLogger } from '../types/activity-logger.js'; -import { ErrorCode } from '../types/errors.js'; -import { matchesBillingTextPattern } from '../utils/billing-detection.js'; -import { formatTimestamp } from '../utils/formatting.js'; -import type { AuditLogger } from './audit-logger.js'; -import { - filterJsonToolCalls, - formatAssistantOutput, - formatResultOutput, - formatToolResultOutput, - formatToolUseOutput, -} from './output-formatters.js'; -import type { ProgressManager } from './progress-manager.js'; -import type { - ApiErrorDetection, - AssistantMessage, - AssistantResult, - ContentBlock, - ExecutionContext, - ModelRefusalFallbackMessage, - ResultData, - ResultMessage, - SystemInitMessage, - ToolResultData, - ToolResultMessage, - ToolUseData, - ToolUseMessage, -} from './types.js'; - -// Handles both array and string content formats from SDK -function extractMessageContent(message: AssistantMessage): string { - const messageContent = message.message; - - if (Array.isArray(messageContent.content)) { - return messageContent.content - .filter((c: ContentBlock) => c.type !== 'thinking' && c.type !== 'redacted_thinking') - .map((c: ContentBlock) => c.text || JSON.stringify(c)) - .join('\n'); - } - - return String(messageContent.content); -} - -// Extracts only text content (no tool_use JSON) to avoid false positives in error detection -function extractTextOnlyContent(message: AssistantMessage): string { - const messageContent = message.message; - - if (Array.isArray(messageContent.content)) { - return messageContent.content - .filter((c: ContentBlock) => c.type === 'text' || c.text) - .map((c: ContentBlock) => c.text || '') - .join('\n'); - } - - return String(messageContent.content); -} - -function detectApiError(content: string): ApiErrorDetection { - if (!content || typeof content !== 'string') { - return { detected: false }; - } - - const lowerContent = content.toLowerCase(); - - // === BILLING/SPENDING CAP ERRORS (Retryable with long backoff) === - // When Claude Code hits its spending cap, it returns a short message like - // "Spending cap reached resets 8am" instead of throwing an error. - // These should retry with 5-30 min backoff so workflows can recover when cap resets. - if (matchesBillingTextPattern(content)) { - return { - detected: true, - shouldThrow: new PentestError( - `Billing limit reached: ${content.slice(0, 100)}`, - 'billing', - true, // RETRYABLE - Temporal will use 5-30 min backoff - {}, - ErrorCode.SPENDING_CAP_REACHED, - ), - }; - } - - // === SESSION LIMIT (Non-retryable) === - // Different from spending cap - usually means something is fundamentally wrong - if (lowerContent.includes('session limit reached')) { - return { - detected: true, - shouldThrow: new PentestError('Session limit reached', 'billing', false), - }; - } - - // Non-fatal API errors - detected but continue - if (lowerContent.includes('api error') || lowerContent.includes('terminated')) { - return { detected: true }; - } - - return { detected: false }; -} - -// Maps SDK structured error types to our error handling. -function handleStructuredError(errorType: SDKAssistantMessageError, content: string): ApiErrorDetection { - switch (errorType) { - case 'billing_error': - return { - detected: true, - shouldThrow: new PentestError( - `Billing error (structured): ${content.slice(0, 100)}`, - 'billing', - true, // Retryable with backoff - {}, - ErrorCode.INSUFFICIENT_CREDITS, - ), - }; - case 'rate_limit': - return { - detected: true, - shouldThrow: new PentestError( - `Rate limit hit (structured): ${content.slice(0, 100)}`, - 'network', - true, // Retryable with backoff - {}, - ErrorCode.API_RATE_LIMITED, - ), - }; - case 'authentication_failed': - return { - detected: true, - shouldThrow: new PentestError( - `Authentication failed: ${content.slice(0, 100)}`, - 'config', - false, // Not retryable - needs API key fix - ), - }; - case 'server_error': - return { - detected: true, - shouldThrow: new PentestError( - `Server error (structured): ${content.slice(0, 100)}`, - 'network', - true, // Retryable - ), - }; - case 'invalid_request': - return { - detected: true, - shouldThrow: new PentestError( - `Invalid request: ${content.slice(0, 100)}`, - 'config', - false, // Not retryable - needs code fix - ), - }; - case 'max_output_tokens': - return { - detected: true, - shouldThrow: new PentestError( - `Max output tokens reached: ${content.slice(0, 100)}`, - 'billing', - true, // Retryable - may succeed with different content - ), - }; - case 'overloaded': - return { - detected: true, - shouldThrow: new PentestError( - `Anthropic API overloaded (structured): ${content.slice(0, 100)}`, - 'network', - true, // Retryable with backoff - ), - }; - case 'model_not_found': - return { - detected: true, - shouldThrow: new PentestError( - `Model not found: ${content.slice(0, 100)}`, - 'config', - false, // Not retryable - model ID is misconfigured - ), - }; - case 'oauth_org_not_allowed': - return { - detected: true, - shouldThrow: new PentestError( - `Organization not allowed for this credential: ${content.slice(0, 100)}`, - 'config', - false, // Not retryable - needs credential/org fix - ), - }; - default: - return { detected: true }; - } -} - -function handleAssistantMessage(message: AssistantMessage, turnCount: number): AssistantResult { - const content = extractMessageContent(message); - const cleanedContent = filterJsonToolCalls(content); - - // Prefer structured error field from SDK, fall back to text-sniffing - // Use text-only content for error detection to avoid false positives - // from tool_use JSON (e.g. security reports containing "usage limit") - let errorDetection: ApiErrorDetection; - if (message.error) { - errorDetection = handleStructuredError(message.error, content); - } else { - const textOnlyContent = extractTextOnlyContent(message); - errorDetection = detectApiError(textOnlyContent); - } - - const result: AssistantResult = { - content, - cleanedContent, - apiErrorDetected: errorDetection.detected, - logData: { - turn: turnCount, - content, - timestamp: formatTimestamp(), - }, - }; - - // Only add shouldThrow if it exists (exactOptionalPropertyTypes compliance) - if (errorDetection.shouldThrow) { - result.shouldThrow = errorDetection.shouldThrow; - } - - return result; -} - -// Final message of a query with cost/duration info -function handleResultMessage(message: ResultMessage): ResultData { - const result: ResultData = { - result: message.result || null, - cost: message.total_cost_usd || 0, - duration_ms: message.duration_ms || 0, - permissionDenials: message.permission_denials?.length || 0, - }; - - // Only add subtype if it exists (exactOptionalPropertyTypes compliance) - if (message.subtype) { - result.subtype = message.subtype; - } - - // Capture stop_reason for diagnostics (helps debug early stops, budget exceeded, etc.) - if (message.stop_reason !== undefined) { - result.stop_reason = message.stop_reason; - if (message.stop_reason && message.stop_reason !== 'end_turn') { - console.log(` Stop reason: ${message.stop_reason}`); - } - } - - if (message.structured_output !== undefined) { - result.structuredOutput = message.structured_output; - } - - return result; -} - -function handleToolUseMessage(message: ToolUseMessage): ToolUseData { - return { - toolName: message.name, - parameters: message.input || {}, - timestamp: formatTimestamp(), - }; -} - -// Truncates long results for display (500 char limit), preserves full content for logging -function handleToolResultMessage(message: ToolResultMessage): ToolResultData { - const content = message.content; - const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2); - - const displayContent = - contentStr.length > 500 - ? `${contentStr.slice(0, 500)}...\n[Result truncated - ${contentStr.length} total chars]` - : contentStr; - - return { - content, - displayContent, - timestamp: formatTimestamp(), - }; -} - -function outputLines(lines: string[]): void { - for (const line of lines) { - console.log(line); - } -} - -export type MessageDispatchAction = - | { type: 'continue'; apiErrorDetected?: boolean | undefined; model?: string | undefined } - | { type: 'complete'; result: string | null; cost: number; structuredOutput?: unknown } - | { type: 'throw'; error: Error }; - -export interface MessageDispatchDeps { - execContext: ExecutionContext; - description: string; - progress: ProgressManager; - auditLogger: AuditLogger; - logger: ActivityLogger; -} - -// Dispatches SDK messages to appropriate handlers and formatters -export async function dispatchMessage( - message: { type: string; subtype?: string }, - turnCount: number, - deps: MessageDispatchDeps, -): Promise { - const { execContext, description, progress, auditLogger, logger } = deps; - - switch (message.type) { - case 'assistant': { - const assistantResult = handleAssistantMessage(message as AssistantMessage, turnCount); - - if (assistantResult.shouldThrow) { - return { type: 'throw', error: assistantResult.shouldThrow }; - } - - if (assistantResult.cleanedContent.trim()) { - progress.stop(); - outputLines(formatAssistantOutput(assistantResult.cleanedContent, execContext, turnCount, description)); - progress.start(); - } - - await auditLogger.logLlmResponse(turnCount, assistantResult.content); - - if (assistantResult.apiErrorDetected) { - logger.warn('API Error detected in assistant response'); - return { type: 'continue', apiErrorDetected: true }; - } - - return { type: 'continue' }; - } - - case 'system': { - if (message.subtype === 'init') { - const initMsg = message as SystemInitMessage; - if (!execContext.useCleanOutput) { - logger.info(`Model: ${initMsg.model}, Permission: ${initMsg.permissionMode}`); - } - return { type: 'continue', model: initMsg.model }; - } - if (message.subtype === 'model_refusal_fallback') { - const fallback = message as ModelRefusalFallbackMessage; - const category = fallback.api_refusal_category ?? 'policy'; - await auditLogger.logNote( - 'model-fallback', - `Model refused (${category}); fell back ${fallback.original_model} → ${fallback.fallback_model}`, - ); - return { type: 'continue' }; - } - return { type: 'continue' }; - } - - case 'user': - case 'tool_progress': - case 'tool_use_summary': - case 'auth_status': - return { type: 'continue' }; - - case 'tool_use': { - const toolData = handleToolUseMessage(message as unknown as ToolUseMessage); - outputLines(formatToolUseOutput(toolData.toolName, toolData.parameters)); - await auditLogger.logToolStart(toolData.toolName, toolData.parameters); - return { type: 'continue' }; - } - - case 'tool_result': { - const toolResultData = handleToolResultMessage(message as unknown as ToolResultMessage); - outputLines(formatToolResultOutput(toolResultData.displayContent)); - await auditLogger.logToolEnd(toolResultData.content); - return { type: 'continue' }; - } - - case 'result': { - const resultData = handleResultMessage(message as ResultMessage); - outputLines(formatResultOutput(resultData, !execContext.useCleanOutput)); - - if (resultData.subtype === 'error_max_structured_output_retries') { - return { - type: 'throw', - error: new PentestError( - 'Structured output validation failed after max retries', - 'validation', - true, - {}, - ErrorCode.OUTPUT_VALIDATION_FAILED, - ), - }; - } - - return { - type: 'complete' as const, - result: resultData.result, - cost: resultData.cost, - ...(resultData.structuredOutput !== undefined && { structuredOutput: resultData.structuredOutput }), - }; - } - - default: - logger.info(`Unhandled message type: ${message.type}`); - return { type: 'continue' }; - } -} diff --git a/apps/worker/src/ai/models.ts b/apps/worker/src/ai/models.ts index 9f2d73d..0bbadbb 100644 --- a/apps/worker/src/ai/models.ts +++ b/apps/worker/src/ai/models.ts @@ -5,17 +5,28 @@ // as published by the Free Software Foundation. /** - * Model tier definitions and resolution. + * Model tier definitions and resolution for the pi harness. * * Three tiers mapped to capability levels: * - "small" (Haiku — summarization, structured extraction) * - "medium" (Sonnet — tool use, general analysis) * - "large" (Opus — deep reasoning, complex analysis) * - * Users override via ANTHROPIC_SMALL_MODEL / ANTHROPIC_MEDIUM_MODEL / ANTHROPIC_LARGE_MODEL, - * which works across all providers (direct, Bedrock, Vertex). + * Users override per tier via ANTHROPIC_SMALL_MODEL / ANTHROPIC_MEDIUM_MODEL / + * ANTHROPIC_LARGE_MODEL, which works across all providers (Anthropic, Bedrock, + * custom base URL). + * + * The active provider is chosen from the env-var contract the CLI forwards + * (`CLAUDE_CODE_USE_BEDROCK`, `ANTHROPIC_BASE_URL`+`ANTHROPIC_AUTH_TOKEN`, else + * direct Anthropic). Resolution returns a pi `Model` via `ModelRegistry.find`, the + * `thinkingLevel`, and an `AuthStorage` primed with the right credential. Bedrock + * authenticates from the AWS_ env vars via pi-ai. */ +import type { ThinkingLevel } from '@earendil-works/pi-agent-core'; +import type { Api, Model } from '@earendil-works/pi-ai'; +import { AuthStorage, type ModelRegistry } from '@earendil-works/pi-coding-agent'; + export type ModelTier = 'small' | 'medium' | 'large'; const DEFAULT_MODELS: Readonly> = { @@ -24,8 +35,46 @@ const DEFAULT_MODELS: Readonly> = { large: 'claude-opus-4-8', }; -/** Resolve a model tier to a concrete model ID. */ -export function resolveModel(tier: ModelTier = 'medium'): string { +export interface EffectiveProvider { + /** pi-ai provider id: 'anthropic' or 'amazon-bedrock'. */ + providerId: string; + /** Custom-base-URL override applied to the resolved anthropic model. */ + baseUrl?: string; + /** Runtime credential to prime on AuthStorage for the 'anthropic' provider. */ + anthropicToken?: string; +} + +/** + * Determine the active provider + auth from the env-var contract the CLI forwards: + * `CLAUDE_CODE_USE_BEDROCK` → Bedrock; `ANTHROPIC_BASE_URL`+`ANTHROPIC_AUTH_TOKEN` + * → custom base URL; else direct Anthropic (`ANTHROPIC_API_KEY`, or + * `CLAUDE_CODE_OAUTH_TOKEN`). Bedrock authenticates from the AWS_ env vars via + * pi-ai, so it needs no anthropic token. + */ +export function resolveEffectiveProvider(): EffectiveProvider { + // Bedrock — env flag. + if (process.env.CLAUDE_CODE_USE_BEDROCK === '1') { + return { providerId: 'amazon-bedrock' }; + } + + // Custom base URL — env contract. + if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN) { + return { + providerId: 'anthropic', + baseUrl: process.env.ANTHROPIC_BASE_URL, + anthropicToken: process.env.ANTHROPIC_AUTH_TOKEN, + }; + } + + // Direct Anthropic (API key, or OAuth token). + const eff: EffectiveProvider = { providerId: 'anthropic' }; + const token = process.env.ANTHROPIC_API_KEY ?? process.env.CLAUDE_CODE_OAUTH_TOKEN; + if (token) eff.anthropicToken = token; + return eff; +} + +/** Resolve a model tier to a concrete model ID (env override → default). */ +export function resolveModelId(tier: ModelTier = 'medium'): string { switch (tier) { case 'small': return process.env.ANTHROPIC_SMALL_MODEL || DEFAULT_MODELS.small; @@ -41,6 +90,67 @@ export function supportsAdaptiveThinking(model: string): boolean { return /opus-4-[678]/.test(model); } +/** + * Resolve the thinking level for a run. + * + * Adaptive thinking is enabled only on capable models (Opus 4.6/4.7/4.8), mapped to + * pi's 'medium' level; every other model runs with thinking 'off'. The + * CLAUDE_ADAPTIVE_THINKING=false kill switch forces 'off' regardless of model. + */ +export function resolveThinkingLevel(modelId: string): ThinkingLevel { + if (process.env.CLAUDE_ADAPTIVE_THINKING === 'false') return 'off'; + return supportsAdaptiveThinking(modelId) ? 'medium' : 'off'; +} + +export interface ModelSelection { + model: Model; + thinkingLevel: ThinkingLevel; + authStorage: AuthStorage; + modelId: string; + providerId: string; +} + +/** + * Resolve the active provider (see resolveEffectiveProvider), prime an AuthStorage + * with its credential, and resolve the tier's model from a fresh ModelRegistry. + * Anthropic / custom-base-URL use a runtime anthropic key; Bedrock authenticates + * from the AWS_ env vars (bearer token primed explicitly as a belt-and-suspenders). + */ +export function resolveModelSelection( + registryFactory: (authStorage: AuthStorage) => ModelRegistry, + modelTier: ModelTier, +): ModelSelection { + const eff = resolveEffectiveProvider(); + const modelId = resolveModelId(modelTier); + + const authStorage = AuthStorage.inMemory(); + if (eff.providerId === 'anthropic' && eff.anthropicToken) { + authStorage.setRuntimeApiKey('anthropic', eff.anthropicToken); + } + // Bedrock auth flows from the AWS_ env vars; prime the bearer token explicitly so + // it resolves via AuthStorage in addition to pi-ai's own env fallback. + if (eff.providerId === 'amazon-bedrock' && process.env.AWS_BEARER_TOKEN_BEDROCK) { + authStorage.setRuntimeApiKey('amazon-bedrock', process.env.AWS_BEARER_TOKEN_BEDROCK); + } + + const registry = registryFactory(authStorage); + const found = registry.find(eff.providerId, modelId); + if (!found) { + throw new Error(`Model not found in pi registry: provider="${eff.providerId}" model="${modelId}"`); + } + + // Custom base URL: override the resolved model's endpoint. + const model: Model = eff.baseUrl ? { ...found, baseUrl: eff.baseUrl } : found; + + return { + model, + thinkingLevel: resolveThinkingLevel(modelId), + authStorage, + modelId, + providerId: eff.providerId, + }; +} + /** * Whether a model is in the Fable family. Fable's safety classifiers flag * cybersecurity tasks and route them to Opus 4.8, so a security scan on Fable diff --git a/apps/worker/src/ai/output-formatters.ts b/apps/worker/src/ai/output-formatters.ts index c960fb4..0268cc1 100644 --- a/apps/worker/src/ai/output-formatters.ts +++ b/apps/worker/src/ai/output-formatters.ts @@ -4,36 +4,31 @@ // it under the terms of the GNU Affero General Public License version 3 // as published by the Free Software Foundation. +/** + * Human-readable console formatting for the agent executor. + * + * Driven by the pi harness event stream: `turn_end` (assistant text) and + * `tool_execution_start` (structured tool calls). Unlike the previous harness — + * where tool calls were tool_use JSON embedded in assistant text and had to be + * parsed out — pi delivers tool name + args as discrete events, so formatting is + * a direct mapping. + */ + import { AGENTS } from '../session-manager.js'; import { extractAgentType, formatDuration } from '../utils/formatting.js'; -import type { ExecutionContext, ResultData } from './types.js'; +import type { ExecutionContext } from './types.js'; interface ToolCallInput { url?: string; - element?: string; - key?: string; - fields?: unknown[]; - text?: string; - action?: string; - description?: string; command?: string; - todos?: Array<{ - status: string; - content: string; - }>; + description?: string; + path?: string; + todos?: Array<{ status: string; content: string }>; [key: string]: unknown; } -interface ToolCall { - name: string; - input?: ToolCallInput; -} - -/** - * Get agent prefix for parallel execution - */ +/** Agent prefix used to attribute output when parallel agents interleave on one stream. */ export function getAgentPrefix(description: string): string { - // Map agent names to their prefixes const agentPrefixes: Record = { 'injection-vuln': '[Injection]', 'xss-vuln': '[XSS]', @@ -47,7 +42,6 @@ export function getAgentPrefix(description: string): string { 'ssrf-exploit': '[SSRF]', }; - // First try to match by agent name directly for (const [agentName, prefix] of Object.entries(agentPrefixes)) { const agent = AGENTS[agentName as keyof typeof AGENTS]; if (agent && description.includes(agent.displayName)) { @@ -55,7 +49,6 @@ export function getAgentPrefix(description: string): string { } } - // Fallback to partial matches for backwards compatibility if (description.includes('injection')) return '[Injection]'; if (description.includes('xss')) return '[XSS]'; if (description.includes('authz')) return '[Authz]'; // Check authz before auth @@ -65,9 +58,7 @@ export function getAgentPrefix(description: string): string { return '[Agent]'; } -/** - * Extract domain from URL for display - */ +/** Extract domain from URL for display. */ function extractDomain(url: string): string { try { const urlObj = new URL(url); @@ -77,11 +68,8 @@ function extractDomain(url: string): string { } } -/** - * Format playwright-cli commands into clean progress indicators - */ +/** Format a playwright-cli command (run via the bash tool) into a clean progress indicator. */ function formatBrowserAction(command: string): string | null { - // Extract subcommand after optional session flag (e.g., "playwright-cli -s=session1 navigate https://example.com") const match = command.match(/playwright-cli\s+(?:-s=\S+\s+)?(\S+)(?:\s+(.*))?/); if (!match) return null; @@ -151,26 +139,19 @@ function formatBrowserAction(command: string): string | null { } } -/** - * Summarize TodoWrite updates into clean progress indicators - */ +/** Summarize a todo_write update into a clean progress indicator. */ function summarizeTodoUpdate(input: ToolCallInput | undefined): string | null { if (!input?.todos || !Array.isArray(input.todos)) { return null; } const todos = input.todos; - const completed = todos.filter((t) => t.status === 'completed'); - const inProgress = todos.filter((t) => t.status === 'in_progress'); - - // Show recently completed tasks - const recent = completed.at(-1); + const recent = todos.filter((t) => t.status === 'completed').at(-1); if (recent) { return `✅ ${recent.content}`; } - // Show current in-progress task - const current = inProgress.at(0); + const current = todos.filter((t) => t.status === 'in_progress').at(0); if (current) { return `🔄 ${current.content}`; } @@ -178,69 +159,6 @@ function summarizeTodoUpdate(input: ToolCallInput | undefined): string | null { return null; } -/** - * Filter out JSON tool calls from content, with special handling for Task calls - */ -export function filterJsonToolCalls(content: string | null | undefined): string { - if (!content || typeof content !== 'string') { - return content || ''; - } - - const lines = content.split('\n'); - const processedLines: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - - // Skip empty lines - if (trimmed === '') { - continue; - } - - // Check if this is a JSON tool call - if (trimmed.startsWith('{"type":"tool_use"')) { - try { - const toolCall = JSON.parse(trimmed) as ToolCall; - - // Special handling for Task tool calls - if (toolCall.name === 'Task') { - const description = toolCall.input?.description || 'analysis agent'; - processedLines.push(`🚀 Launching ${description}`); - continue; - } - - // Special handling for TodoWrite tool calls - if (toolCall.name === 'TodoWrite') { - const summary = summarizeTodoUpdate(toolCall.input); - if (summary) { - processedLines.push(summary); - } - continue; - } - - // Special handling for browser tool calls (playwright-cli via Bash) - if (toolCall.name === 'Bash') { - const command = toolCall.input?.command || ''; - if (command.includes('playwright-cli')) { - const browserAction = formatBrowserAction(command); - if (browserAction) { - processedLines.push(browserAction); - } - } - } - } catch { - // If JSON parsing fails, treat as regular text - processedLines.push(line); - } - } else { - // Keep non-JSON lines (assistant text) - processedLines.push(line); - } - } - - return processedLines.join('\n'); -} - export function detectExecutionContext(description: string): ExecutionContext { const isParallelExecution = description.includes('vuln agent') || description.includes('exploit agent'); @@ -252,62 +170,69 @@ export function detectExecutionContext(description: string): ExecutionContext { description.includes('exploit agent'); const agentType = extractAgentType(description); - const agentKey = description.toLowerCase().replace(/\s+/g, '-'); return { isParallelExecution, useCleanOutput, agentType, agentKey }; } +/** Format assistant turn text (from a pi `turn_end` event). */ export function formatAssistantOutput( - cleanedContent: string, + text: string, context: ExecutionContext, turnCount: number, description: string, ): string[] { - if (!cleanedContent.trim()) { + if (!text.trim()) { return []; } - const lines: string[] = []; - if (context.isParallelExecution) { - // Compact output for parallel agents with prefixes - const prefix = getAgentPrefix(description); - lines.push(`${prefix} ${cleanedContent}`); - } else { - // Full turn output for sequential agents - lines.push(`\n Turn ${turnCount} (${description}):`); - lines.push(` ${cleanedContent}`); + // Compact, attributed output for interleaved parallel agents. + return [`${getAgentPrefix(description)} ${text}`]; } - - return lines; + // Full turn output for sequential agents. + return [`\n Turn ${turnCount} (${description}):`, ` ${text}`]; } -export function formatResultOutput(data: ResultData, showFullResult: boolean): string[] { - const lines: string[] = []; +/** + * Format a pi `tool_execution_start` event into a clean one-line progress indicator. + * + * Maps the common tool surfaces — `task` (sub-agent delegation), `todo_write` + * (plan updates), `bash` (incl. playwright-cli browser actions), read-only file + * tools, and the structured collector/submit tools — to friendly lines. Returns + * `[]` when there's nothing worth surfacing (e.g. a todo update with no active item). + */ +export function formatToolCall( + toolName: string, + args: Record | undefined, + context: ExecutionContext, + description: string, +): string[] { + const input = (args ?? {}) as ToolCallInput; + let line: string | null; - lines.push(`\n COMPLETED:`); - lines.push(` Duration: ${(data.duration_ms / 1000).toFixed(1)}s, Cost: $${data.cost.toFixed(4)}`); - - if (data.subtype === 'error_max_turns') { - lines.push(` Stopped: Hit maximum turns limit`); - } else if (data.subtype === 'error_during_execution') { - lines.push(` Stopped: Execution error`); + if (toolName === 'task') { + line = `🚀 Launching ${input.description ?? 'sub-agent'}`; + } else if (toolName === 'todo_write') { + line = summarizeTodoUpdate(input); + } else if (toolName === 'bash') { + const command = typeof input.command === 'string' ? input.command : ''; + line = command.includes('playwright-cli') ? formatBrowserAction(command) : `💻 ${command.slice(0, 60)}`; + } else if (toolName === 'read' || toolName === 'grep' || toolName === 'find' || toolName === 'ls') { + const path = typeof input.path === 'string' ? ` ${input.path.slice(0, 60)}` : ''; + line = `📖 ${toolName}${path}`; + } else if (toolName.startsWith('set_') || toolName.startsWith('add_') || toolName.startsWith('submit_')) { + line = `📊 ${toolName.replace(/_/g, ' ')}`; + } else { + line = `🔧 ${toolName}`; } - if (data.permissionDenials > 0) { - lines.push(` ${data.permissionDenials} permission denials`); - } + if (!line) return []; - if (showFullResult && data.result && typeof data.result === 'string') { - if (data.result.length > 1000) { - lines.push(` ${data.result.slice(0, 1000)}... [${data.result.length} total chars]`); - } else { - lines.push(` ${data.result}`); - } + if (context.isParallelExecution) { + return [`${getAgentPrefix(description)} ${line}`]; } - - return lines; + return [` ${line}`]; } export function formatErrorOutput( @@ -321,12 +246,11 @@ export function formatErrorOutput( const lines: string[] = []; if (context.isParallelExecution) { - const prefix = getAgentPrefix(description); - lines.push(`${prefix} Failed (${formatDuration(duration)})`); + lines.push(`${getAgentPrefix(description)} Failed (${formatDuration(duration)})`); } else if (context.useCleanOutput) { lines.push(`${context.agentType} failed (${formatDuration(duration)})`); } else { - lines.push(` Claude Code failed: ${description} (${formatDuration(duration)})`); + lines.push(` pi agent failed: ${description} (${formatDuration(duration)})`); } lines.push(` Error Type: ${error.constructor.name}`); @@ -352,35 +276,12 @@ export function formatCompletionMessage( duration: number, ): string { if (context.isParallelExecution) { - const prefix = getAgentPrefix(description); - return `${prefix} Complete (${turnCount} turns, ${formatDuration(duration)})`; + return `${getAgentPrefix(description)} Complete (${turnCount} turns, ${formatDuration(duration)})`; } if (context.useCleanOutput) { return `${context.agentType.charAt(0).toUpperCase() + context.agentType.slice(1)} complete! (${turnCount} turns, ${formatDuration(duration)})`; } - return ` Claude Code completed: ${description} (${turnCount} turns) in ${formatDuration(duration)}`; -} - -export function formatToolUseOutput(toolName: string, input: Record | undefined): string[] { - const lines: string[] = []; - - lines.push(`\n Using Tool: ${toolName}`); - if (input && Object.keys(input).length > 0) { - lines.push(` Input: ${JSON.stringify(input, null, 2)}`); - } - - return lines; -} - -export function formatToolResultOutput(displayContent: string): string[] { - const lines: string[] = []; - - lines.push(` Tool Result:`); - if (displayContent) { - lines.push(` ${displayContent}`); - } - - return lines; + return ` pi agent completed: ${description} (${turnCount} turns) in ${formatDuration(duration)}`; } diff --git a/apps/worker/src/ai/pi/permission-system.ts b/apps/worker/src/ai/pi/permission-system.ts new file mode 100644 index 0000000..a1febac --- /dev/null +++ b/apps/worker/src/ai/pi/permission-system.ts @@ -0,0 +1,141 @@ +// 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. + +/** + * code_path "avoid" enforcement for the pi harness, delegated to the + * @gotgenes/pi-permission-system extension. + * + * Each `code_path` avoid is translated into the extension's cross-cutting `path` + * deny surface — the strongest gate, blocking file access (read/edit/write/grep/ + * find/ls) AND recognized bash file commands (cat/grep/sed/…) on any matching path, + * across every tool and child `task` session, not overridable by a per-tool allow. + * + * `external_directory: allow` keeps the extension from gating the agent's legitimate + * access outside the working directory once it is loaded (the pentest agent shells + * out to tools/paths outside the mounted repo). When there are no avoids the config + * is removed so the executor skips loading the extension entirely. + */ + +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { getAgentDir } from '@earendil-works/pi-coding-agent'; +import type { DistributedConfig } from '../../types/config.js'; + +const PERMISSION_EXTENSION_ID = 'pi-permission-system'; + +/** + * Translate one avoid value into the extension's flat-wildcard `path` patterns. + * + * The extension's `*` already spans path separators (no `**` globstar), and tool + * paths are compared as absolute. A plain directory value is expanded to cover the + * directory itself and everything under it, in both cwd-relative and prefixed + * (absolute) positions. Glob values fold `**`→`*`; a `dir/*` contents glob also + * denies the directory entry itself. + */ +export function toPathPatterns(value: string): string[] { + // Strip only leading path prefixes ("/", "./", "../"); preserve a dotfile's dot + // (so `.env` stays `.env`, not `env`). + const base = value.replace(/^(?:\.{0,2}\/)+/, '').replace(/\/+$/, ''); + if (!base) return []; + + if (base.includes('*') || base.includes('?')) { + // The extension's `*` already spans path separators, so fold `**` to `*`. + const flat = base.replace(/\*\*\//g, '*/').replace(/\*\*/g, '*'); + const tail = flat.replace(/^(?:\*\/)+/, ''); + const patterns = [flat, `*/${tail}`]; + // Depth-agnostic catch-all only for a bare-name tail (so `**/*.env` hits a + // root-level `.env`); a structured tail would over-match sibling names. + if (!tail.includes('/')) { + patterns.push(tail.startsWith('*') ? tail : `*${tail}`); + } + // A `dir/*` contents glob should also deny the directory entry itself — the + // contents patterns require a trailing segment and wouldn't match the folder. + if (flat.endsWith('/*')) { + const folder = flat.slice(0, -2); + if (folder && !folder.includes('*')) { + patterns.push(folder, `*/${folder}`); + } + } + return [...new Set(patterns)]; + } + + return [base, `${base}/*`, `*/${base}`, `*/${base}/*`]; +} + +interface PermissionSystemConfig { + permission: { + '*': 'allow'; + path: Record; + external_directory: 'allow'; + }; +} + +/** Build the extension config that denies every avoid pattern across all tools. */ +export function buildPermissionConfig(patterns: readonly string[]): PermissionSystemConfig { + // Default allow first; deny entries are appended so they win (last match wins). + const pathRules: Record = { '*': 'allow' }; + for (const pattern of patterns) { + for (const expanded of toPathPatterns(pattern)) { + pathRules[expanded] = 'deny'; + } + } + return { + permission: { + '*': 'allow', + path: pathRules, + external_directory: 'allow', + }, + }; +} + +/** Path to the extension's global config under the agent directory. */ +export function permissionSystemConfigPath(agentDir: string): string { + return path.join(agentDir, 'extensions', PERMISSION_EXTENSION_ID, 'config.json'); +} + +/** True when a pi-permission-system config has been written (avoid rules exist). */ +export function permissionSystemConfigExists(agentDir: string): boolean { + return fs.existsSync(permissionSystemConfigPath(agentDir)); +} + +/** + * Sync the distributed config's `code_path` avoids into the extension's global + * config (`/extensions/pi-permission-system/config.json`). When there + * are no avoids the config is removed so the executor skips loading the extension. + * + * Global (not project) config is used deliberately: it loads synchronously at + * extension init without depending on a session_start/ctx, it keeps the config + * out of the scanned repo, and it is idempotent across the agents of one run. + */ +export function syncPermissionSystemConfig(config: DistributedConfig | null): void { + const configPath = permissionSystemConfigPath(getAgentDir()); + const avoidRules = (config?.avoid ?? []).filter((r) => r.type === 'code_path'); + + if (avoidRules.length === 0) { + fs.rmSync(configPath, { force: true }); + return; + } + + // Single-repo (fixed mount): patterns are the raw avoid values. + const patterns = avoidRules.map((r) => r.value); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify(buildPermissionConfig(patterns), null, 2)); +} + +/** + * Absolute path to the installed @gotgenes/pi-permission-system package directory, + * suitable for `DefaultResourceLoader`'s `additionalExtensionPaths`. The loader + * reads the package's `pi.extensions` manifest and loads the extension itself. + * + * The package's `.` export points at its service module, so we resolve that and + * walk up to the package root. Throws if the package is not resolvable. + */ +export function permissionSystemPackageDir(): string { + const require = createRequire(import.meta.url); + const servicePath = require.resolve('@gotgenes/pi-permission-system'); + return path.resolve(path.dirname(servicePath), '..'); +} diff --git a/apps/worker/src/ai/pi/pi-executor.ts b/apps/worker/src/ai/pi/pi-executor.ts new file mode 100644 index 0000000..acc7ded --- /dev/null +++ b/apps/worker/src/ai/pi/pi-executor.ts @@ -0,0 +1,418 @@ +// 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. + +// Production agent execution on the pi harness, with git checkpoints and audit logging. + +import os from 'node:os'; +import type { AgentMessage } from '@earendil-works/pi-agent-core'; +import { + type AgentSessionEvent, + createAgentSession, + DefaultResourceLoader, + getAgentDir, + ModelRegistry, + type ResourceLoader, + SessionManager, + SettingsManager, + type Skill, + type ToolDefinition, +} from '@earendil-works/pi-coding-agent'; +import { fs, path } from 'zx'; +import type { AuditSession } from '../../audit/index.js'; +import { BASH_TIMEOUT_EXTENSION_DIR, deliverablesDir } from '../../paths.js'; +import { isRetryableError, PentestError } from '../../services/error-handling.js'; +import { AGENT_VALIDATORS } from '../../session-manager.js'; +import type { ActivityLogger } from '../../types/activity-logger.js'; +import { ErrorCode } from '../../types/errors.js'; +import { isSpendingCapBehavior, matchesBillingTextPattern } from '../../utils/billing-detection.js'; +import { isBrowserAgent } from '../../utils/browser-agents.js'; +import { formatTimestamp } from '../../utils/formatting.js'; +import { Timer } from '../../utils/metrics.js'; +import { createAuditLogger } from '../audit-logger.js'; +import { type ModelTier, resolveModelSelection } from '../models.js'; +import { + detectExecutionContext, + formatAssistantOutput, + formatCompletionMessage, + formatErrorOutput, + formatToolCall, +} from '../output-formatters.js'; +import { createProgressManager } from '../progress-manager.js'; +import type { CapturedSubmitTool } from '../submit-tool.js'; +import { permissionSystemConfigExists, permissionSystemPackageDir } from './permission-system.js'; +import { createGlobTool, createTodoWriteTool } from './session-tools.js'; +import { createTaskTool } from './task-tool.js'; + +declare global { + var SHANNON_DISABLE_LOADER: boolean | undefined; +} + +/** Built-in pi tools enabled for every agent (custom tool names are appended). */ +const BUILTIN_TOOLS = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls']; + +/** Build the playwright-cli Skill object injected for browser-using agents. */ +function buildPlaywrightSkill(): Skill { + const filePath = + process.env.PLAYWRIGHT_CLI_SKILL_PATH ?? path.join(os.homedir(), '.claude/skills/playwright-cli/SKILL.md'); + const baseDir = path.dirname(filePath); + return { + name: 'playwright-cli', + description: + 'Drive a real browser via the playwright-cli binary. Use for any task that navigates, clicks, ' + + 'fills forms, takes screenshots, or reads live pages.', + filePath, + baseDir, + sourceInfo: { path: filePath, source: 'custom', scope: 'user', origin: 'top-level', baseDir }, + disableModelInvocation: false, + }; +} + +async function buildResourceLoader( + cwd: string, + logger: ActivityLogger, + agentName: string | null, +): Promise { + // Always enforce bounded bash timeouts so an unbounded command cannot hang the agent. + const additionalExtensionPaths: string[] = [BASH_TIMEOUT_EXTENSION_DIR]; + if (permissionSystemConfigExists(getAgentDir())) { + try { + additionalExtensionPaths.push(permissionSystemPackageDir()); + } catch { + logger.warn( + 'code_path deny config present but @gotgenes/pi-permission-system not resolvable — skipping enforcement', + ); + } + } + + // Only browser-driving agents get the playwright-cli skill; the rest run with no skills. + const loader = new DefaultResourceLoader({ + cwd, + agentDir: getAgentDir(), + ...(additionalExtensionPaths.length > 0 && { additionalExtensionPaths }), + ...(isBrowserAgent(agentName) + ? { + skillsOverride: (base) => ({ + skills: [buildPlaywrightSkill()], + diagnostics: base.diagnostics, + }), + } + : { noSkills: true }), + }); + await loader.reload(); + return loader; +} + +export interface PiPromptResult { + result?: string | null | undefined; + success: boolean; + duration: number; + turns?: number | undefined; + cost: number; + model?: string | undefined; + partialCost?: number | undefined; + apiErrorDetected?: boolean | undefined; + error?: string | undefined; + errorType?: string | undefined; + prompt?: string | undefined; + retryable?: boolean | undefined; + structuredOutput?: unknown; +} + +function outputLines(lines: string[]): void { + for (const line of lines) { + console.log(line); + } +} + +async function writeErrorLog( + err: Error & { code?: string; status?: number }, + sourceDir: string, + fullPrompt: string, + duration: number, +): Promise { + try { + const errorLog = { + timestamp: formatTimestamp(), + agent: 'pi-executor', + error: { name: err.constructor.name, message: err.message, code: err.code, status: err.status, stack: err.stack }, + context: { sourceDir, prompt: `${fullPrompt.slice(0, 200)}...`, retryable: isRetryableError(err) }, + duration, + }; + const logPath = path.join(deliverablesDir(sourceDir), 'error.log'); + await fs.appendFile(logPath, `${JSON.stringify(errorLog)}\n`); + } catch { + // Best-effort error log writing - don't propagate failures + } +} + +export async function validateAgentOutput( + result: PiPromptResult, + agentName: string | null, + sourceDir: string, + logger: ActivityLogger, +): Promise { + logger.info(`Validating ${agentName} agent output`); + try { + if (!result.success || (!result.result && result.structuredOutput === undefined)) { + logger.error('Validation failed: Agent execution was unsuccessful'); + return false; + } + const validator = agentName ? AGENT_VALIDATORS[agentName as keyof typeof AGENT_VALIDATORS] : undefined; + if (!validator) { + logger.warn(`No validator found for agent "${agentName}" - assuming success`); + return true; + } + logger.info(`Using validator for agent: ${agentName}`, { sourceDir }); + const validationResult = await validator(sourceDir, logger); + if (validationResult) { + logger.info('Validation passed: Required files/structure present'); + } else { + logger.error('Validation failed: Missing required deliverable files'); + } + return validationResult; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`Validation failed with error: ${errMsg}`); + return false; + } +} + +/** Concatenate the text blocks of an assistant message (skips thinking + tool calls). */ +function extractAssistantText(message: AgentMessage): string { + if (message.role !== 'assistant') return ''; + const blocks = message.content as Array<{ type: string; text?: string }>; + return blocks + .filter((c) => c.type === 'text') + .map((c) => c.text ?? '') + .join('\n'); +} + +/** + * Classify error-bearing text into a PentestError, mirroring the prior provider error + * handling. Spending-cap / billing text is retryable (Temporal backs off and + * recovers when the cap resets); session limit is permanent. + */ +function classifyErrorText(content: string): PentestError | null { + if (!content) return null; + if (matchesBillingTextPattern(content)) { + return new PentestError( + `Billing limit reached: ${content.slice(0, 100)}`, + 'billing', + true, + {}, + ErrorCode.SPENDING_CAP_REACHED, + ); + } + if (content.toLowerCase().includes('session limit reached')) { + return new PentestError('Session limit reached', 'billing', false); + } + return null; +} + +// Low-level pi execution. Drives one agent session to completion with progress and +// audit logging. Exported for Temporal activities to call single-attempt execution. +export async function runPiPrompt( + prompt: string, + sourceDir: string, + context: string = '', + description: string = 'Agent analysis', + agentName: string | null = null, + auditSession: AuditSession | null = null, + logger: ActivityLogger, + modelTier: ModelTier = 'medium', + callerTools?: ToolDefinition[], + deliverablesSubdir?: string, + cancellationSignal?: AbortSignal, + submitTool?: CapturedSubmitTool, +): Promise { + // 1. Initialize timing and prompt. A submit tool appends its directive so the + // instruction to call it lives with the tool, not in every prompt file. + const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`); + const basePrompt = context ? `${context}\n\n${prompt}` : prompt; + const fullPrompt = submitTool?.directive ? basePrompt + submitTool.directive : basePrompt; + + // 2. Set up progress and audit infrastructure + const execContext = detectExecutionContext(description); + const progress = createProgressManager( + { description, useCleanOutput: execContext.useCleanOutput }, + global.SHANNON_DISABLE_LOADER ?? false, + ); + const auditLogger = createAuditLogger(auditSession); + + logger.info(`Running pi agent: ${description}...`); + + // 3. Expose bash-invoked CLI tooling (playwright-cli, save-deliverable) to the + // environment pi's bash tool inherits. These are constant per container, so + // setting them on process.env is parallel-safe across this workflow's agents. + process.env.PLAYWRIGHT_MCP_OUTPUT_DIR = deliverablesSubdir + ? path.join(sourceDir, path.dirname(deliverablesSubdir), '.playwright-cli') + : path.join(sourceDir, '.shannon', '.playwright-cli'); + if (deliverablesSubdir) process.env.SHANNON_DELIVERABLES_SUBDIR = deliverablesSubdir; + + // 4. Resolve model + auth, then assemble the tool set (universal task/todo tools + // plus any caller-supplied collector/submit tools). + const selection = resolveModelSelection((auth) => ModelRegistry.create(auth), modelTier); + const resourceLoader = await buildResourceLoader(sourceDir, logger, agentName); + // Accumulates usage from in-process `task` child sessions so the parent's reported + // cost includes sub-agent spend (their getSessionStats is separate from ours). + const childUsage = { cost: 0, inputTokens: 0, outputTokens: 0 }; + const customTools: ToolDefinition[] = [ + createTaskTool({ + model: selection.model, + thinkingLevel: selection.thinkingLevel, + authStorage: selection.authStorage, + cwd: sourceDir, + onUsage: (usage) => { + childUsage.cost += usage.cost; + childUsage.inputTokens += usage.inputTokens; + childUsage.outputTokens += usage.outputTokens; + }, + resourceLoader, + ...(cancellationSignal && { cancellationSignal }), + }), + createTodoWriteTool(auditLogger), + createGlobTool(sourceDir), + ...(callerTools ?? []), + ...(submitTool ? [submitTool.tool] : []), + ]; + // pi's `tools` allowlist gates custom tools too — list every custom name. + const tools = [...BUILTIN_TOOLS, ...customTools.map((t) => t.name)]; + + let turnCount = 0; + let pendingError: PentestError | null = null; + let apiErrorDetected = false; + + progress.start(); + + try { + const { session } = await createAgentSession({ + cwd: sourceDir, + model: selection.model, + thinkingLevel: selection.thinkingLevel, + tools, + customTools, + authStorage: selection.authStorage, + sessionManager: SessionManager.inMemory(), + // Temporal owns retry; pi compaction stays on (no analog previously, guards + // against context overflow on long agent runs). + settingsManager: SettingsManager.inMemory({ retry: { enabled: false }, compaction: { enabled: true } }), + resourceLoader, + }); + + // 5. Map pi events to audit logging + progress + error capture. + session.subscribe((event: AgentSessionEvent) => { + switch (event.type) { + case 'turn_end': { + turnCount += 1; + const msg = event.message; + const text = extractAssistantText(msg); + if (text.trim()) { + void auditLogger.logLlmResponse(turnCount, text); + progress.stop(); + outputLines(formatAssistantOutput(text, execContext, turnCount, description)); + progress.start(); + const billing = classifyErrorText(text); + if (billing) pendingError = billing; + } + if (msg.role === 'assistant' && msg.stopReason === 'error') { + apiErrorDetected = true; + pendingError = + pendingError ?? + classifyErrorText(msg.errorMessage ?? '') ?? + new PentestError(`Agent error: ${(msg.errorMessage ?? 'unknown').slice(0, 200)}`, 'unknown', true); + } + break; + } + case 'tool_execution_start': { + void auditLogger.logToolStart(event.toolName, event.args); + const toolLines = formatToolCall( + event.toolName, + event.args as Record, + execContext, + description, + ); + if (toolLines.length > 0) { + progress.stop(); + outputLines(toolLines); + progress.start(); + } + break; + } + case 'tool_execution_end': + void auditLogger.logToolEnd(event.result); + break; + case 'compaction_end': + if (!event.aborted && !event.willRetry && event.errorMessage) { + pendingError = + pendingError ?? + classifyErrorText(event.errorMessage) ?? + new PentestError(`Context compaction failed: ${event.errorMessage.slice(0, 200)}`, 'unknown', true); + } + break; + default: + break; + } + }); + + // 6. Run the agent to completion (resolves at agent_end). + await session.prompt(fullPrompt); + session.dispose(); + + // 7. Surface any error captured during the run. + if (pendingError) throw pendingError; + + // 8. Read usage/cost and final text. + const stats = session.getSessionStats(); + const totalCost = stats.cost + childUsage.cost; + const result = session.getLastAssistantText() ?? null; + + // 9. Defense-in-depth: detect a spending cap that produced an empty/cheap run. + if (isSpendingCapBehavior(turnCount, totalCost, result || '')) { + throw new PentestError( + `Spending cap likely reached (turns=${turnCount}, cost=$0): ${result?.slice(0, 100)}`, + 'billing', + true, + ); + } + + const duration = timer.stop(); + progress.finish(formatCompletionMessage(execContext, description, turnCount, duration)); + + // Capture the submit tool's structured payload so callers read it off the + // result instead of holding a reference to the tool. + const structuredOutput = submitTool?.getCaptured(); + + return { + result, + success: true, + duration, + turns: turnCount, + cost: totalCost, + model: selection.model.id, + partialCost: totalCost, + apiErrorDetected, + ...(structuredOutput !== undefined && { structuredOutput }), + }; + } catch (error) { + // 10. Handle errors — log, write error file, return failure + const duration = timer.stop(); + const err = error as Error & { code?: string; status?: number }; + await auditLogger.logError(err, duration, turnCount); + progress.stop(); + outputLines(formatErrorOutput(err, execContext, description, duration, sourceDir, isRetryableError(err))); + await writeErrorLog(err, sourceDir, fullPrompt, duration); + + return { + error: err.message, + errorType: err.constructor.name, + prompt: `${fullPrompt.slice(0, 100)}...`, + success: false, + duration, + cost: 0, + retryable: isRetryableError(err), + }; + } +} diff --git a/apps/worker/src/ai/pi/session-tools.ts b/apps/worker/src/ai/pi/session-tools.ts new file mode 100644 index 0000000..9f77c7f --- /dev/null +++ b/apps/worker/src/ai/pi/session-tools.ts @@ -0,0 +1,116 @@ +// 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. + +/** + * Per-session custom tools registered for every agent: `todo_write` and `glob`. + * + * These replace harness built-ins that pi does not ship. `todo_write` is a + * full-state-replace planning scratchpad mirrored to the workflow log; `glob` is + * fast-glob file matching (pi has no `Glob` built-in). + */ + +import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; +import { fs, glob, path } from 'zx'; + +import type { AuditLogger } from '../audit-logger.js'; + +export interface TodoItem { + content: string; + status: 'pending' | 'in_progress' | 'completed'; + activeForm: string; +} + +function renderTodos(todos: readonly TodoItem[]): string { + const mark = (status: TodoItem['status']): string => { + if (status === 'completed') return 'x'; + if (status === 'in_progress') return '~'; + return ' '; + }; + return todos.map((todo) => `[${mark(todo.status)}] ${todo.content}`).join(' '); +} + +export function createTodoWriteTool(auditLogger: AuditLogger): ToolDefinition { + let current: TodoItem[] = []; + + return defineTool({ + name: 'todo_write', + label: 'Todo Write', + description: + 'Use this tool to create and manage a structured task list for your current session. ' + + 'Pass the complete todo list on every call; it replaces the stored list entirely. Each ' + + 'todo has a status of pending, in_progress, or completed.', + promptSnippet: 'todo_write: create and manage a structured task list', + parameters: Type.Object({ + todos: Type.Array( + Type.Object({ + content: Type.String({ description: 'Imperative task description, e.g. "Map SSRF sinks".' }), + status: Type.Union([Type.Literal('pending'), Type.Literal('in_progress'), Type.Literal('completed')]), + activeForm: Type.String({ description: 'Present-continuous form, e.g. "Mapping SSRF sinks".' }), + }), + ), + }), + async execute(_toolCallId, params) { + current = params.todos as TodoItem[]; + const completed = current.filter((todo) => todo.status === 'completed').length; + await auditLogger.logNote('todo', renderTodos(current)); + return { + content: [ + { + type: 'text' as const, + text: `Todos updated (${current.length} items, ${completed} completed).`, + }, + ], + details: undefined, + }; + }, + }); +} + +export function createGlobTool(cwd: string): ToolDefinition { + return defineTool({ + name: 'glob', + label: 'Glob', + description: + 'Fast file pattern matching. Supports glob patterns like "**/*.ts" or "src/**/*.{js,ts}". ' + + 'Returns matching file paths sorted by modification time, most recent first.', + promptSnippet: 'glob: find files by name pattern', + parameters: Type.Object({ + pattern: Type.String({ description: 'The glob pattern to match files against.' }), + path: Type.Optional(Type.String({ description: 'Directory to search in. Omit for the repository root.' })), + }), + async execute(_toolCallId, params) { + const searchRoot = params.path ? path.resolve(cwd, params.path) : cwd; + const matches = await glob.globby(params.pattern, { + cwd: searchRoot, + absolute: true, + dot: true, + onlyFiles: true, + followSymbolicLinks: false, + }); + + if (matches.length === 0) { + return { content: [{ type: 'text' as const, text: 'No files found' }], details: undefined }; + } + + const withMtime = await Promise.all( + matches.map(async (file) => { + try { + return { file, mtime: (await fs.stat(file)).mtimeMs }; + } catch { + return { file, mtime: 0 }; + } + }), + ); + withMtime.sort((a, b) => b.mtime - a.mtime); + + return { + content: [{ type: 'text' as const, text: withMtime.map((match) => match.file).join('\n') }], + details: undefined, + }; + }, + }); +} diff --git a/apps/worker/src/ai/pi/task-tool.ts b/apps/worker/src/ai/pi/task-tool.ts new file mode 100644 index 0000000..c0865be --- /dev/null +++ b/apps/worker/src/ai/pi/task-tool.ts @@ -0,0 +1,156 @@ +// 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. + +/** + * Generic `task` tool — pi.dev ships no built-in Task tool, so this supplies the + * Task-delegation surface Shannon's prompts require. + * + * Shannon's prompts mandate Task delegation (recon source tracer; the vuln + * agents delegate *every* code review; the exploit agents delegate automation), + * so this tool is required for parity, not optional. It spawns a nested pi + * session with the parent's resolved model object (never a tier string — that + * would route sub-agents through hardcoded IDs and leak billing), the parent's + * resource loader, and a fixed child tool surface. + */ + +import type { ThinkingLevel } from '@earendil-works/pi-agent-core'; +import { type AssistantMessage, type Model, Type } from '@earendil-works/pi-ai'; +import { + type AuthStorage, + createAgentSession, + defineTool, + getAgentDir, + type ModelRegistry, + type ResourceLoader, + SessionManager, + SettingsManager, + type ToolDefinition, +} from '@earendil-works/pi-coding-agent'; + +export interface TaskToolContext { + cwd: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + model: Model; + thinkingLevel?: ThinkingLevel; + authStorage: AuthStorage; + /** Explicit model registry for sub-session resolution. Omit to inherit the parent's default. */ + modelRegistry?: ModelRegistry; + resourceLoader: ResourceLoader; + cancellationSignal?: AbortSignal | undefined; + /** + * Reports the cost/tokens of each spawned sub-session back to the caller. + * Sub-agents run in their own pi sessions that the parent has no reference to, + * so without this their spend (the bulk of a whitebox run, since Shannon + * prompts delegate the heavy work) is invisible to billing. + */ + onUsage?: (usage: { cost: number; inputTokens: number; outputTokens: number }) => void; +} + +const CHILD_TOOLS = ['read', 'grep', 'find', 'ls', 'write', 'bash']; + +function textResult(text: string) { + return { content: [{ type: 'text' as const, text }], details: undefined }; +} + +export function createTaskTool(config: TaskToolContext): ToolDefinition { + const taskTool: ToolDefinition = defineTool({ + name: 'task', + label: 'Task', + description: + 'Delegate a focused task to a sub-agent that runs independently with its own tools and returns ' + + 'the result. Use this to break complex work into smaller, parallelizable sub-tasks.', + executionMode: 'parallel', + promptSnippet: 'task - Delegate a focused task to a sub-agent with read, grep, find, ls, write, and bash.', + promptGuidelines: [ + 'Use the task tool to delegate focused work: code review, reconnaissance, automation scripting, validation.', + 'Pass all necessary context in the "prompt" parameter — the sub-agent cannot see your conversation history.', + 'The sub-agent can use read, grep, find, ls, write, and bash, but cannot call task or custom collector tools.', + 'You can launch multiple task tool calls in a single message to run sub-tasks in parallel.', + ], + parameters: Type.Object({ + prompt: Type.String({ + description: 'The task for the sub-agent to perform. Include all necessary context.', + }), + description: Type.Optional(Type.String({ description: 'A short (3-5 word) description of the task.' })), + }), + async execute(_toolCallId, params) { + const agentDir = getAgentDir(); + const { session: subSession } = await createAgentSession({ + cwd: config.cwd, + agentDir, + resourceLoader: config.resourceLoader, + model: config.model, + ...(config.thinkingLevel && { thinkingLevel: config.thinkingLevel }), + tools: CHILD_TOOLS, + authStorage: config.authStorage, + ...(config.modelRegistry && { modelRegistry: config.modelRegistry }), + sessionManager: SessionManager.inMemory(config.cwd), + settingsManager: SettingsManager.inMemory({ + retry: { enabled: false }, + compaction: { enabled: true }, + }), + }); + + const abortChildSession = (): void => { + void subSession.abort().catch(() => { + // Parent logger is not available inside the tool; dispose still tears + // down the session if abort itself rejects. + }); + }; + const onCancellation = (): void => abortChildSession(); + if (config.cancellationSignal?.aborted) { + abortChildSession(); + } else { + config.cancellationSignal?.addEventListener('abort', onCancellation, { once: true }); + } + + let resultText = ''; + let subCost = 0; + let subInputTokens = 0; + let subOutputTokens = 0; + subSession.subscribe((event) => { + if (event.type === 'turn_end') { + const msg = event.message as AssistantMessage | undefined; + for (const block of msg?.content ?? []) { + if (block.type === 'text' && block.text) { + resultText += (resultText ? '\n' : '') + block.text; + } + } + if (msg?.usage?.cost?.total != null) subCost += msg.usage.cost.total; + subInputTokens += msg?.usage?.input ?? 0; + subOutputTokens += msg?.usage?.output ?? 0; + } + }); + + let swallowedError: string | undefined; + try { + try { + await subSession.prompt(params.prompt); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + resultText += `\n[Sub-agent error: ${errorMsg}]`; + } + + swallowedError = subSession.state.errorMessage; + // Read stats before dispose; reconcile cost the same way the parent does. + const subStats = subSession.getSessionStats(); + if (subStats.cost > subCost) subCost = subStats.cost; + config.onUsage?.({ cost: subCost, inputTokens: subInputTokens, outputTokens: subOutputTokens }); + } finally { + config.cancellationSignal?.removeEventListener('abort', onCancellation); + subSession.dispose(); + } + + if (swallowedError && !resultText.includes(swallowedError)) { + resultText += `\n[Sub-agent error: ${swallowedError}]`; + } + + return textResult(resultText || '[Sub-agent produced no output]'); + }, + }); + + return taskTool; +} diff --git a/apps/worker/src/ai/queue-schemas.ts b/apps/worker/src/ai/queue-schemas.ts index f29b8a7..3dc968c 100644 --- a/apps/worker/src/ai/queue-schemas.ts +++ b/apps/worker/src/ai/queue-schemas.ts @@ -5,196 +5,108 @@ // as published by the Free Software Foundation. /** - * Zod schema definitions for vulnerability exploitation queue structured outputs. + * TypeBox schemas + submit-tool factory for vulnerability exploitation queues. * - * Each vuln agent returns a structured JSON response matching its schema. - * The SDK validates the output against the JSON Schema generated from these Zod definitions. + * pi captures each vuln agent's structured queue via a `submit_exploitation_queue` + * custom tool whose parameters mirror the per-class schema below. Entry types are + * derived from the same schemas and consumed by the findings renderer. */ -import type { JsonSchemaOutputFormat } from '@anthropic-ai/claude-agent-sdk'; -import { z } from 'zod'; +import { defineTool } from '@earendil-works/pi-coding-agent'; +import { type Static, type TObject, Type } from 'typebox'; import type { AgentName } from '../types/agents.js'; - -// === Common Fields === +import type { CapturedSubmitTool } from './submit-tool.js'; const ANALYSIS_NOTES_DESCRIPTION = 'Plain context for defenders (caveats, scope, what is at risk). Not attack steps.'; -function notesField(exploit: boolean) { - const f = z.string().optional(); - return exploit ? f : f.describe(ANALYSIS_NOTES_DESCRIPTION); +function optStr(description?: string) { + return Type.Optional(Type.String(description === undefined ? {} : { description })); } -function makeBase(exploit: boolean) { - return z.object({ - ID: z.string(), - vulnerability_type: z.string(), - externally_exploitable: z.boolean(), - confidence: z.string(), - notes: notesField(exploit), - }); -} - -// === Per-Vuln-Type Schemas (used for type inference; notes description is mode-agnostic for types) === - -const baseVulnerability = makeBase(true); - -const InjectionVulnerability = baseVulnerability.extend({ - source: z.string().optional(), - combined_sources: z.string().optional(), - path: z.string().optional(), - sink_call: z.string().optional(), - slot_type: z.string().optional(), - sanitization_observed: z.string().optional(), - concat_occurrences: z.string().optional(), - verdict: z.string().optional(), - mismatch_reason: z.string().optional(), - witness_payload: z.string().optional(), -}); - -const XssVulnerability = baseVulnerability.extend({ - source: z.string().optional(), - source_detail: z.string().optional(), - path: z.string().optional(), - sink_function: z.string().optional(), - render_context: z.string().optional(), - encoding_observed: z.string().optional(), - verdict: z.string().optional(), - mismatch_reason: z.string().optional(), - witness_payload: z.string().optional(), -}); - -const AuthVulnerability = baseVulnerability.extend({ - source_endpoint: z.string().optional(), - vulnerable_code_location: z.string().optional(), - missing_defense: z.string().optional(), - exploitation_hypothesis: z.string().optional(), - suggested_exploit_technique: z.string().optional(), -}); - -const SsrfVulnerability = baseVulnerability.extend({ - source_endpoint: z.string().optional(), - vulnerable_parameter: z.string().optional(), - vulnerable_code_location: z.string().optional(), - missing_defense: z.string().optional(), - exploitation_hypothesis: z.string().optional(), - suggested_exploit_technique: z.string().optional(), -}); - -const AuthzVulnerability = baseVulnerability.extend({ - endpoint: z.string().optional(), - vulnerable_code_location: z.string().optional(), - role_context: z.string().optional(), - guard_evidence: z.string().optional(), - side_effect: z.string().optional(), - reason: z.string().optional(), - minimal_witness: z.string().optional(), -}); - -// === Inferred Entry Types (consumed by renderer) === - -export type InjectionFinding = z.infer; -export type XssFinding = z.infer; -export type AuthFinding = z.infer; -export type SsrfFinding = z.infer; -export type AuthzFinding = z.infer; - -// === Convert to JSON Schema for SDK === - -// NOTE: The SDK's AJV validator expects draft-07. Zod defaults to draft-2020-12 which -// causes the SDK to silently skip structured output. -function toOutputFormat(zodSchema: z.ZodType): JsonSchemaOutputFormat { - return { type: 'json_schema', schema: z.toJSONSchema(zodSchema, { target: 'draft-07' }) as Record }; -} - -// === Per-Mode Output Format Builders === -// Two maps cached at module load; the only per-mode difference is the -// description on the `notes` field, which steers the LLM's writing. - -function buildOutputFormats(exploit: boolean): Partial> { - const base = makeBase(exploit); +/** Base fields shared by every queue entry. `notes` gains guidance in analysis mode. */ +function baseFields(exploit: boolean) { return { - 'injection-vuln': toOutputFormat( - z.object({ - vulnerabilities: z.array( - base.extend({ - source: z.string().optional(), - combined_sources: z.string().optional(), - path: z.string().optional(), - sink_call: z.string().optional(), - slot_type: z.string().optional(), - sanitization_observed: z.string().optional(), - concat_occurrences: z.string().optional(), - verdict: z.string().optional(), - mismatch_reason: z.string().optional(), - witness_payload: z.string().optional(), - }), - ), - }), - ), - 'xss-vuln': toOutputFormat( - z.object({ - vulnerabilities: z.array( - base.extend({ - source: z.string().optional(), - source_detail: z.string().optional(), - path: z.string().optional(), - sink_function: z.string().optional(), - render_context: z.string().optional(), - encoding_observed: z.string().optional(), - verdict: z.string().optional(), - mismatch_reason: z.string().optional(), - witness_payload: z.string().optional(), - }), - ), - }), - ), - 'auth-vuln': toOutputFormat( - z.object({ - vulnerabilities: z.array( - base.extend({ - source_endpoint: z.string().optional(), - vulnerable_code_location: z.string().optional(), - missing_defense: z.string().optional(), - exploitation_hypothesis: z.string().optional(), - suggested_exploit_technique: z.string().optional(), - }), - ), - }), - ), - 'ssrf-vuln': toOutputFormat( - z.object({ - vulnerabilities: z.array( - base.extend({ - source_endpoint: z.string().optional(), - vulnerable_parameter: z.string().optional(), - vulnerable_code_location: z.string().optional(), - missing_defense: z.string().optional(), - exploitation_hypothesis: z.string().optional(), - suggested_exploit_technique: z.string().optional(), - }), - ), - }), - ), - 'authz-vuln': toOutputFormat( - z.object({ - vulnerabilities: z.array( - base.extend({ - endpoint: z.string().optional(), - vulnerable_code_location: z.string().optional(), - role_context: z.string().optional(), - guard_evidence: z.string().optional(), - side_effect: z.string().optional(), - reason: z.string().optional(), - minimal_witness: z.string().optional(), - }), - ), - }), - ), + ID: Type.String(), + vulnerability_type: Type.String(), + externally_exploitable: Type.Boolean(), + confidence: Type.String(), + notes: exploit ? optStr() : optStr(ANALYSIS_NOTES_DESCRIPTION), }; } -const OUTPUT_FORMATS_EXPLOIT = buildOutputFormats(true); -const OUTPUT_FORMATS_ANALYSIS = buildOutputFormats(false); +const injectionFields = { + source: optStr(), + combined_sources: optStr(), + path: optStr(), + sink_call: optStr(), + slot_type: optStr(), + sanitization_observed: optStr(), + concat_occurrences: optStr(), + verdict: optStr(), + mismatch_reason: optStr(), + witness_payload: optStr(), +}; + +const xssFields = { + source: optStr(), + source_detail: optStr(), + path: optStr(), + sink_function: optStr(), + render_context: optStr(), + encoding_observed: optStr(), + verdict: optStr(), + mismatch_reason: optStr(), + witness_payload: optStr(), +}; + +const authFields = { + source_endpoint: optStr(), + vulnerable_code_location: optStr(), + missing_defense: optStr(), + exploitation_hypothesis: optStr(), + suggested_exploit_technique: optStr(), +}; + +const ssrfFields = { + source_endpoint: optStr(), + vulnerable_parameter: optStr(), + vulnerable_code_location: optStr(), + missing_defense: optStr(), + exploitation_hypothesis: optStr(), + suggested_exploit_technique: optStr(), +}; + +const authzFields = { + endpoint: optStr(), + vulnerable_code_location: optStr(), + role_context: optStr(), + guard_evidence: optStr(), + side_effect: optStr(), + reason: optStr(), + minimal_witness: optStr(), +}; + +// === Per-entry schemas (single vulnerability). Entry types derive from these. === + +const injectionEntry = () => Type.Object({ ...baseFields(true), ...injectionFields }); +const xssEntry = () => Type.Object({ ...baseFields(true), ...xssFields }); +const authEntry = () => Type.Object({ ...baseFields(true), ...authFields }); +const ssrfEntry = () => Type.Object({ ...baseFields(true), ...ssrfFields }); +const authzEntry = () => Type.Object({ ...baseFields(true), ...authzFields }); + +export type InjectionFinding = Static>; +export type XssFinding = Static>; +export type AuthFinding = Static>; +export type SsrfFinding = Static>; +export type AuthzFinding = Static>; + +const PER_TYPE_FIELDS: Partial>>> = { + 'injection-vuln': injectionFields, + 'xss-vuln': xssFields, + 'auth-vuln': authFields, + 'ssrf-vuln': ssrfFields, + 'authz-vuln': authzFields, +}; const VULN_AGENT_QUEUE_FILENAMES: Partial> = { 'injection-vuln': 'injection_exploitation_queue.json', @@ -204,12 +116,53 @@ const VULN_AGENT_QUEUE_FILENAMES: Partial> = { 'authz-vuln': 'authz_exploitation_queue.json', }; -/** Returns the structured output format for a vuln agent, or undefined for non-vuln agents. */ -export function getOutputFormat(agentName: AgentName, exploit = true): JsonSchemaOutputFormat | undefined { - return (exploit ? OUTPUT_FORMATS_EXPLOIT : OUTPUT_FORMATS_ANALYSIS)[agentName]; +/** Build the TypeBox submit-tool parameters for a vuln agent, or undefined for non-vuln agents. */ +function queueSchema(agentName: AgentName, exploit: boolean): TObject | undefined { + const extra = PER_TYPE_FIELDS[agentName]; + if (!extra) return undefined; + return Type.Object({ + vulnerabilities: Type.Array(Type.Object({ ...baseFields(exploit), ...extra })), + }); } /** Returns the queue filename for a vuln agent, or undefined for non-vuln agents. */ export function getQueueFilename(agentName: AgentName): string | undefined { return VULN_AGENT_QUEUE_FILENAMES[agentName]; } + +/** Build the pi submit tool that captures the exploitation queue for vuln agents. */ +export function createQueueSubmitTool(agentName: AgentName, exploit = true): CapturedSubmitTool | undefined { + const schema = queueSchema(agentName, exploit); + if (!schema) return undefined; + + let captured: unknown | undefined; + return { + tool: defineTool({ + name: 'submit_exploitation_queue', + label: 'Submit Exploitation Queue', + description: + 'Submit the final structured list of analyzed vulnerabilities for this class. Call exactly once when analysis is complete.', + promptSnippet: 'submit_exploitation_queue: record the final structured findings list (call once)', + promptGuidelines: [ + 'You MUST call submit_exploitation_queue exactly once as your final action.', + 'Include every analyzed finding in the vulnerabilities array.', + ], + parameters: schema, + async execute(_toolCallId, params) { + captured = params; + const count = Array.isArray((params as { vulnerabilities?: unknown }).vulnerabilities) + ? (params as { vulnerabilities: unknown[] }).vulnerabilities.length + : 0; + return { + content: [{ type: 'text' as const, text: `Recorded ${count} findings.` }], + details: params, + terminate: true, + }; + }, + }), + getCaptured: () => captured, + directive: + '\n\nYou MUST call the submit_exploitation_queue tool exactly once as your final action ' + + 'to deliver your structured exploitation queue. Do not output JSON as text. Fill every required parameter.', + }; +} diff --git a/apps/worker/src/ai/settings-writer.ts b/apps/worker/src/ai/settings-writer.ts deleted file mode 100644 index dea5380..0000000 --- a/apps/worker/src/ai/settings-writer.ts +++ /dev/null @@ -1,41 +0,0 @@ -// 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. - -/** - * Writes ~/.claude/settings.json with permissions.deny rules derived from - * `code_path` avoid patterns. The SDK reads this via `settingSources: ['user']`; - * deny rules fire even in `bypassPermissions` mode. - */ - -import os from 'node:os'; -import { fs, path } from 'zx'; -import type { DistributedConfig } from '../types/config.js'; - -const FILE_TOOLS = ['Read', 'Edit'] as const; - -function denyEntriesFor(pattern: string): string[] { - const arg = `./${pattern.replace(/^[./]+/, '')}`; - return FILE_TOOLS.map((tool) => `${tool}(${arg})`); -} - -export async function writeUserSettingsForCodePathAvoids(config: DistributedConfig | null): Promise { - const avoidPatterns = (config?.avoid ?? []).filter((r) => r.type === 'code_path').map((r) => r.value); - const settingsPath = path.join(os.homedir(), '.claude', 'settings.json'); - - if (avoidPatterns.length === 0) { - await fs.remove(settingsPath); - return; - } - - const settings = { - permissions: { - deny: avoidPatterns.flatMap(denyEntriesFor), - }, - }; - - await fs.ensureDir(path.dirname(settingsPath)); - await fs.writeJson(settingsPath, settings, { spaces: 2 }); -} diff --git a/apps/worker/src/ai/submit-tool.ts b/apps/worker/src/ai/submit-tool.ts new file mode 100644 index 0000000..e461302 --- /dev/null +++ b/apps/worker/src/ai/submit-tool.ts @@ -0,0 +1,60 @@ +// 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 { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent'; +import { Type } from 'typebox'; + +/** + * A pi custom submit tool plus the captured payload it records. + * + * pi ships no JSON-schema output format, so an agent that must return structured + * data does so by calling a purpose-built TypeBox tool. This bundles that tool + * with its capture accessor and the directive that instructs the model to call + * it. The executor owns the wiring — it registers the tool, appends the + * directive to the prompt, and reads `getCaptured()` back as `structuredOutput` + * — so callers never assemble it by hand. + */ +export interface CapturedSubmitTool { + readonly tool: ToolDefinition; + readonly getCaptured: () => unknown | undefined; + readonly directive?: string; +} + +/** + * Build a `submit_result` tool from a raw JSON Schema, for agents whose result + * shape is not one of the built-in per-agent schemas (e.g. an out-of-tree agent + * with its own verdict schema). pi validates the tool call against `schema` + * before `execute()` runs, so a captured payload is already schema-valid — no + * separate validation pass is needed. + */ +export function createGenericSubmitTool(schema: Record): CapturedSubmitTool { + let captured: unknown | undefined; + return { + tool: defineTool({ + name: 'submit_result', + label: 'Submit Result', + description: 'Return your final structured answer. Call exactly once as your last action.', + promptSnippet: 'submit_result: deliver your structured answer (call once)', + promptGuidelines: [ + 'You MUST call submit_result exactly once as your final action.', + 'Fill every required parameter. Do not output JSON as text.', + ], + parameters: Type.Unsafe(schema), + async execute(_toolCallId, params) { + captured = params; + return { + content: [{ type: 'text' as const, text: 'Result submitted.' }], + details: params, + terminate: true, + }; + }, + }), + getCaptured: () => captured, + directive: + '\n\nYou MUST call the submit_result tool exactly once as your final action ' + + 'to deliver your structured answer. Do not output JSON as text. Fill every required parameter.', + }; +} diff --git a/apps/worker/src/ai/types.ts b/apps/worker/src/ai/types.ts index b6c762e..99321f7 100644 --- a/apps/worker/src/ai/types.ts +++ b/apps/worker/src/ai/types.ts @@ -4,9 +4,7 @@ // it under the terms of the GNU Affero General Public License version 3 // as published by the Free Software Foundation. -// Type definitions for Claude executor message processing pipeline - -import type { SDKAssistantMessageError } from '@anthropic-ai/claude-agent-sdk'; +// Shared display/formatting types for the agent executor output layer. export interface ExecutionContext { isParallelExecution: boolean; @@ -14,99 +12,3 @@ export interface ExecutionContext { agentType: string; agentKey: string; } - -export interface AssistantResult { - content: string; - cleanedContent: string; - apiErrorDetected: boolean; - shouldThrow?: Error; - logData: { - turn: number; - content: string; - timestamp: string; - }; -} - -export interface ResultData { - result: string | null; - cost: number; - duration_ms: number; - subtype?: string; - stop_reason?: string | null; - permissionDenials: number; - structuredOutput?: unknown; -} - -export interface ToolUseData { - toolName: string; - parameters: Record; - timestamp: string; -} - -export interface ToolResultData { - content: unknown; - displayContent: string; - timestamp: string; -} - -export interface ContentBlock { - type?: string; - text?: string; - thinking?: string; - data?: string; -} - -export interface AssistantMessage { - type: 'assistant'; - error?: SDKAssistantMessageError; - message: { - content: ContentBlock[] | string; - }; -} - -export interface ResultMessage { - type: 'result'; - result?: string; - total_cost_usd?: number; - duration_ms?: number; - subtype?: string; - stop_reason?: string | null; - permission_denials?: unknown[]; - structured_output?: unknown; -} - -export interface ToolUseMessage { - type: 'tool_use'; - name: string; - input?: Record; -} - -export interface ToolResultMessage { - type: 'tool_result'; - content?: unknown; -} - -export interface ApiErrorDetection { - detected: boolean; - shouldThrow?: Error; -} - -export interface SystemInitMessage { - type: 'system'; - subtype: 'init'; - model?: string; - permissionMode?: string; -} - -/** Emitted when a model refuses a request and the SDK falls back to another model (e.g. Fable 5 routing cybersecurity tasks to Opus 4.8). */ -export interface ModelRefusalFallbackMessage { - type: 'system'; - subtype: 'model_refusal_fallback'; - original_model: string; - fallback_model: string; - api_refusal_category?: string | null; -} - -export interface UserMessage { - type: 'user'; -} diff --git a/apps/worker/src/audit/audit-session.ts b/apps/worker/src/audit/audit-session.ts index 9d364fe..39dc203 100644 --- a/apps/worker/src/audit/audit-session.ts +++ b/apps/worker/src/audit/audit-session.ts @@ -210,7 +210,7 @@ export class AuditSession { /** * Update session status */ - async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled'): Promise { + async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled' | 'partial'): Promise { await this.ensureInitialized(); const unlock = await sessionMutex.lock(this.sessionId); diff --git a/apps/worker/src/audit/metrics-tracker.ts b/apps/worker/src/audit/metrics-tracker.ts index 9bad57c..914c8d1 100644 --- a/apps/worker/src/audit/metrics-tracker.ts +++ b/apps/worker/src/audit/metrics-tracker.ts @@ -57,7 +57,7 @@ interface SessionData { id: string; webUrl: string; repoPath?: string; - status: 'in-progress' | 'completed' | 'failed' | 'cancelled'; + status: 'in-progress' | 'completed' | 'failed' | 'cancelled' | 'partial'; createdAt: string; completedAt?: string; originalWorkflowId?: string; // First workflow that created this workspace @@ -214,9 +214,9 @@ export class MetricsTracker { agent.checkpoint = result.checkpoint; } } else { - if (result.isFinalAttempt) { - agent.status = 'failed'; - } + // A non-final failed attempt stays in-progress (Temporal will retry); only the + // terminal attempt (or an unqualified failure) marks the agent failed. + agent.status = result.isFinalAttempt === false ? 'in-progress' : 'failed'; } // 7. Clear active timer @@ -232,12 +232,12 @@ export class MetricsTracker { /** * Update session status */ - async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled'): Promise { + async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled' | 'partial'): Promise { if (!this.data) return; this.data.session.status = status; - if (status === 'completed' || status === 'failed' || status === 'cancelled') { + if (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'partial') { this.data.session.completedAt = formatTimestamp(); } diff --git a/apps/worker/src/audit/workflow-logger.ts b/apps/worker/src/audit/workflow-logger.ts index a228cb6..5bdf7bb 100644 --- a/apps/worker/src/audit/workflow-logger.ts +++ b/apps/worker/src/audit/workflow-logger.ts @@ -12,7 +12,7 @@ */ import fs from 'node:fs/promises'; -import { isFableModel, resolveModel } from '../ai/models.js'; +import { isFableModel, resolveModelId } from '../ai/models.js'; import { formatDuration, formatTimestamp } from '../utils/formatting.js'; import { LogStream } from './log-stream.js'; import { generateWorkflowLogPath, type SessionMetadata } from './utils.js'; @@ -31,7 +31,7 @@ export interface AgentMetricsSummary { } export interface WorkflowSummary { - status: 'completed' | 'failed' | 'cancelled'; + status: 'completed' | 'failed' | 'cancelled' | 'partial'; totalDurationMs: number; totalCostUsd: number; completedAgents: string[]; @@ -90,7 +90,7 @@ export class WorkflowLogger { // Surface Fable usage: its safety classifiers route cybersecurity tasks to // Opus 4.8, so those phases run on Opus 4.8 regardless of the tier setting. const fableTiers = (['small', 'medium', 'large'] as const) - .map((tier) => ({ tier, model: resolveModel(tier) })) + .map((tier) => ({ tier, model: resolveModelId(tier) })) .filter(({ model }) => isFableModel(model)); if (fableTiers.length > 0) { const tierList = fableTiers.map(({ tier, model }) => `${tier} (${model})`).join(', '); @@ -134,7 +134,7 @@ export class WorkflowLogger { } /** - * Format timestamp for log line (local time, human readable) + * Format timestamp for log line (UTC, human readable) */ private formatLogTime(): string { const now = new Date(); diff --git a/apps/worker/src/mcp-server/exploit-collector.ts b/apps/worker/src/collectors/exploit-collector.ts similarity index 53% rename from apps/worker/src/mcp-server/exploit-collector.ts rename to apps/worker/src/collectors/exploit-collector.ts index 3eaa40d..6e95787 100644 --- a/apps/worker/src/mcp-server/exploit-collector.ts +++ b/apps/worker/src/collectors/exploit-collector.ts @@ -5,10 +5,10 @@ // as published by the Free Software Foundation. /** - * Exploit Collector MCP Server (factory parameterized by vulnerability class - * and per-run valid-ID set). + * Exploit Collector tool factory (parameterized by vulnerability class and + * per-run valid-ID set). * - * Exposes a single Zod-validated MCP tool `add_exploit`, called once per + * Exposes a single TypeBox-validated tool `add_exploit`, called once per * processed vulnerability by the 5 exploit-* agents (injection, xss, auth, * ssrf, authz). After the agent terminates, the host harvests * collector.getAll() and runs exploit-renderer to produce @@ -16,29 +16,29 @@ * output. * * Schema shape: - * - The SDK tool() helper consumes a ZodRawShape (flat object), not a - * top-level discriminated union. The visible shape is therefore a single - * z.object with common fields required, status as a string enum, and - * per-status fields marked optional at the SDK layer. Each field's - * `.describe()` text explains when it applies. + * - The visible parameter schema is a single Type.Object with common fields + * required, status as a string union, and per-status fields marked optional + * at the tool layer (TypeBox cannot express a top-level discriminated union + * as the flat tool parameters). Each field's `description` text explains + * when it applies. * - True per-status field enforcement runs inside the tool handler via a - * z.discriminatedUnion('status', ...). Missing-field errors come back to - * the agent as structured Zod issues with retryable=true so it can fix - * and retry the call. + * Type.Union([exploited, blocked]) re-validation using the TypeBox `Value` + * API. Missing-field errors come back to the agent as structured issues + * with retryable=true so it can fix and retry the call. * - * Strict queue-ID validation: vulnerability_id is refined against the per-run - * queue's known IDs at schema-build time. Hallucinated or typo'd IDs are - * rejected with a structured Zod error that includes the valid-ID list, - * letting the agent recover locally. + * Strict queue-ID validation: vulnerability_id is checked against the per-run + * queue's known IDs in the handler. Hallucinated or typo'd IDs are rejected + * with a structured error that includes the valid-ID list, letting the agent + * recover locally. * - * Each Zod schema's field-level descriptions carry the bullet labels and - * reproducibility guidance, so the SDK injects it into the agent's tool - * catalog. + * Each field's description carries the bullet labels and reproducibility + * guidance, so the harness injects it into the agent's tool catalog. */ -import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk'; -import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'; -import { z } from 'zod'; +import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent'; +import { type TSchema, Type } from 'typebox'; +import { Value } from 'typebox/value'; +import { stringEnum } from './schema.js'; // ============================================================================ // CLASS DISCRIMINATOR @@ -85,6 +85,7 @@ export type BlockedExploit = { vulnerability_id: string; title: string; vulnerable_location: string; + overview: string; prerequisites?: string | null; confidence: (typeof CONFIDENCE_VALUES)[number]; current_blocker: string; @@ -102,215 +103,182 @@ export type AddExploitInput = ExploitedExploit | BlockedExploit; // SCHEMA BUILDER // ============================================================================ -function buildSchemas(validIds: ReadonlySet) { - const vulnerabilityIdField = z - .string() - .min(1) - .describe( +export function buildSchemas(validIds: ReadonlySet) { + const vulnerabilityIdField = Type.String({ + minLength: 1, + description: 'Vulnerability identifier (e.g. "INJ-VULN-03"). Must match an ID from this run\'s ' + - '{class}_exploitation_queue.json exactly — the collector rejects IDs not in the queue. ' + - `Valid IDs for this run: ${formatValidIdsPreview(validIds)}.`, - ) - .refine((id: string) => validIds.has(id), { - message: - `Vulnerability ID not in this run's queue. Valid IDs: ` + - `${formatValidIdsPreview(validIds)}. ` + - 'Check the queue.json for the canonical ID — likely a typo or hallucinated ID.', - }); + '{class}_exploitation_queue.json exactly — the collector rejects IDs not in the queue. ' + + `Valid IDs for this run: ${formatValidIdsPreview(validIds)}.`, + }); - const titleField = z - .string() - .min(1) - .describe( + const titleField = Type.String({ + minLength: 1, + description: 'Descriptive vulnerability title (e.g. "SQL Injection — User Search", "IDOR — Unauthorized ' + - 'Access to User Orders"). Concise; encodes the vulnerability category and where it lives.', - ); + 'Access to User Orders"). Concise; encodes the vulnerability category and where it lives.', + }); - const vulnerableLocationField = z - .string() - .min(1) - .describe( + const vulnerableLocationField = Type.String({ + minLength: 1, + description: 'Endpoint or mechanism where the vulnerability exists (e.g. "GET /api/products?id=", ' + - '"POST /login", or a code location like "controllers/userController.js:42").', - ); + '"POST /login", or a code location like "controllers/userController.js:42").', + }); - const overviewField = z - .string() - .min(1) - .describe( + const overviewField = Type.String({ + minLength: 1, + description: 'Brief summary of the exploit itself — what the vulnerability is and how it was demonstrated ' + - '(or how it would be demonstrated, for blocked findings). 1-3 sentences.', - ); + '(or how it would be demonstrated, for blocked findings). 1-3 sentences.', + }); - const prerequisitesField = z - .string() - .nullable() - .optional() - .describe( - 'Required setup, tools, or conditions to reproduce the exploit (e.g. authentication, ' + + const prerequisitesField = Type.Optional( + Type.Union([Type.String(), Type.Null()], { + description: + 'Required setup, tools, or conditions to reproduce the exploit (e.g. authentication, ' + 'specific role, prior application state). Omit or pass null when no prerequisites apply.', - ); + }), + ); - const notesField = z - .string() - .nullable() - .optional() - .describe( - 'Optional supplementary context — caveats, related findings, environmental observations. ' + + const notesField = Type.Optional( + Type.Union([Type.String(), Type.Null()], { + description: + 'Optional supplementary context — caveats, related findings, environmental observations. ' + 'Free-form Markdown. Omit or pass null when N/A.', - ); + }), + ); - const statusField = z - .enum(['exploited', 'blocked']) - .describe( + const statusField = stringEnum(['exploited', 'blocked'], { + description: 'Verdict bucket. Set to "exploited" only after reaching Proof of Exploitation Level 3+ with ' + - 'concrete impact evidence (extracted data, executed JavaScript, account takeover, internal ' + - 'service access). Set to "blocked" only for real vulnerabilities where external factors ' + - '(NOT security defenses) prevented full exploitation. Findings where a security defense ' + - 'successfully prevented exploitation after exhaustive bypass attempts are FALSE POSITIVE — ' + - 'route those to your workspace tracking file, not this tool.', - ); + 'concrete impact evidence (extracted data, executed JavaScript, account takeover, internal ' + + 'service access). Set to "blocked" only for real vulnerabilities where external factors ' + + '(NOT security defenses) prevented full exploitation. Findings where a security defense ' + + 'successfully prevented exploitation after exhaustive bypass attempts are FALSE POSITIVE — ' + + 'route those to your workspace tracking file, not this tool.', + }); - // Per-status fields. All optional at the SDK shape layer because a single - // ZodRawShape cannot express a top-level discriminated union; the handler - // re-validates against the discriminated union below for true enforcement. - const severityField = z - .enum(SEVERITY_VALUES) - .nullable() - .optional() - .describe( - 'REQUIRED when status="exploited". Severity of the demonstrated impact. Critical = Level 4 ' + + // Per-status fields. All optional at the flat shape layer because a single + // Type.Object cannot express a top-level discriminated union; the handler + // re-validates against the strict union below for true enforcement. + const severityField = Type.Optional( + Type.Union([stringEnum(SEVERITY_VALUES), Type.Null()], { + description: + 'REQUIRED when status="exploited". Severity of the demonstrated impact. Critical = Level 4 ' + '(admin credentials extracted, sensitive data dumped, system commands executed, full account ' + 'takeover). High = Level 3 (data extraction proven, authentication bypass confirmed, ' + 'internal service access). Medium/Low based on impact narrowness or read-only access. Must ' + 'reflect demonstrated impact, not theoretical potential.', - ); + }), + ); - const impactField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="exploited". Business/security impact achieved by the exploit ' + + const impactField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="exploited". Business/security impact achieved by the exploit ' + '(e.g. "Extracted full user table including bcrypt password hashes for 1,247 users", ' + '"Achieved RCE as the application user; arbitrary shell commands executed"). Must describe ' + 'what was actually demonstrated, not what could theoretically happen.', - ); + }), + ); - const exploitationStepsField = z - .array(z.string().min(1)) - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="exploited". Ordered, reproducible exploitation steps — one Markdown ' + + const exploitationStepsField = Type.Optional( + Type.Union([Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }), Type.Null()], { + description: + 'REQUIRED when status="exploited". Ordered, reproducible exploitation steps — one Markdown ' + 'blob per numbered step. Each step must include full URLs (protocol + domain + port + path ' + '+ params), complete payloads, and copy-paste-ready commands. Use clear placeholders for ' + 'variable values like [SESSION_TOKEN], [DATABASE_NAME], [TABLE_NAME], [TARGET_USER_ID]. ' + 'Write each step as natural Markdown — interleave prose with fenced code blocks (```bash, ' + '```http, etc.) as you would in a write-up. Steps must be detailed enough that someone ' + 'unfamiliar with the application can follow without additional research.', - ); + }), + ); - const proofOfImpactField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="exploited". Concrete evidence of successful exploitation — extracted ' + + const proofOfImpactField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="exploited". Concrete evidence of successful exploitation — extracted ' + 'data, achieved actions, captured request/response pairs, log excerpts. Markdown blob; ' + 'interleave prose with fenced code blocks. Must show what the exploit demonstrably achieved, ' + 'not theoretical impact.', - ); + }), + ); - const confidenceField = z - .enum(CONFIDENCE_VALUES) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". Confidence that this finding is a real vulnerability that ' + + const confidenceField = Type.Optional( + Type.Union([stringEnum(CONFIDENCE_VALUES), Type.Null()], { + description: + 'REQUIRED when status="blocked". Confidence that this finding is a real vulnerability that ' + 'would be exploited if the external blocker were removed. High = code analysis strongly ' + 'confirms vulnerability and partial exploitation (Level 1-2) succeeded. Medium = code ' + 'analysis confirms but live evidence is partial. Low = signal-only; revisit if blocker is ' + 'removed in a future run.', - ); + }), + ); - const currentBlockerField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". What prevents full exploitation (e.g. "Server crashes after ' + + const currentBlockerField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". What prevents full exploitation (e.g. "Server crashes after ' + '5 requests, blocking enumeration", "OAuth callback requires verified third-party email ' + 'account we could not provision"). Must be an external operational constraint, not a ' + 'security defense.', - ); + }), + ); - const potentialImpactField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". What could be achieved if the blocker were removed (e.g. ' + + const potentialImpactField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". What could be achieved if the blocker were removed (e.g. ' + '"Full database read access", "Account takeover of arbitrary user via reset-token leak"). ' + 'Distinct from impact — this is the hypothetical outcome, not a demonstrated one.', - ); + }), + ); - const evidenceOfVulnerabilityField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". Code snippets, response excerpts, or observed behavior ' + + const evidenceOfVulnerabilityField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". Code snippets, response excerpts, or observed behavior ' + 'proving the vulnerability is real. Markdown blob; interleave prose with fenced code blocks. ' + 'This is what convinces the reader the finding is not a false positive despite incomplete ' + 'exploitation.', - ); + }), + ); - const whatWeTriedField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". Log of attempted exploitation techniques and why each was ' + + const whatWeTriedField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". Log of attempted exploitation techniques and why each was ' + 'blocked. Each attempt should document the payload, the observed result, and the inferred ' + 'blocker. Markdown blob; multiple attempts as a list or distinct paragraphs. Demonstrates ' + 'exhaustive bypass effort per the Bypass Exhaustion Protocol.', - ); + }), + ); - const howThisWouldBeExploitedField = z - .array(z.string().min(1)) - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". Ordered hypothetical exploitation steps assuming the blocker ' + + const howThisWouldBeExploitedField = Type.Optional( + Type.Union([Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". Ordered hypothetical exploitation steps assuming the blocker ' + 'is removed — one Markdown blob per numbered step. Same reproducibility requirements as ' + 'exploitation_steps: full URLs, complete payloads, copy-paste-ready commands. Frame the ' + 'first step as "If [blocker] were removed: …".', - ); + }), + ); - const expectedImpactField = z - .string() - .min(1) - .nullable() - .optional() - .describe( - 'REQUIRED when status="blocked". Specific data or access that would be compromised if ' + + const expectedImpactField = Type.Optional( + Type.Union([Type.String({ minLength: 1 }), Type.Null()], { + description: + 'REQUIRED when status="blocked". Specific data or access that would be compromised if ' + 'exploitation succeeded (e.g. "Read access to all user profile data including PII; write ' + 'access to user-owned resources"). Markdown blob.', - ); + }), + ); - // The flat shape passed to tool(). The SDK uses this to build the agent's + // The flat shape passed to defineTool. pi uses this to build the agent's // tool catalog. Per-status enforcement happens in the handler via the - // discriminated union below. - const flatShape = { + // strict union below. + const flatSchema = Type.Object({ status: statusField, vulnerability_id: vulnerabilityIdField, title: titleField, @@ -329,85 +297,83 @@ function buildSchemas(validIds: ReadonlySet) { what_we_tried: whatWeTriedField, how_this_would_be_exploited: howThisWouldBeExploitedField, expected_impact: expectedImpactField, - }; + }); // Strict per-status validation. Re-runs in the handler so missing fields - // for the chosen status return a retryable Zod error to the agent. - const ExploitedSchema = z.object({ - status: z.literal('exploited'), + // for the chosen status return a retryable error to the agent. + const ExploitedSchema = Type.Object({ + status: Type.Literal('exploited'), vulnerability_id: vulnerabilityIdField, title: titleField, vulnerable_location: vulnerableLocationField, overview: overviewField, prerequisites: prerequisitesField, - severity: z.enum(SEVERITY_VALUES), - impact: z.string().min(1), - exploitation_steps: z.array(z.string().min(1)).min(1), - proof_of_impact: z.string().min(1), + severity: stringEnum(SEVERITY_VALUES), + impact: Type.String({ minLength: 1 }), + exploitation_steps: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }), + proof_of_impact: Type.String({ minLength: 1 }), notes: notesField, }); - const BlockedSchema = z.object({ - status: z.literal('blocked'), + const BlockedSchema = Type.Object({ + status: Type.Literal('blocked'), vulnerability_id: vulnerabilityIdField, title: titleField, vulnerable_location: vulnerableLocationField, + overview: overviewField, prerequisites: prerequisitesField, - confidence: z.enum(CONFIDENCE_VALUES), - current_blocker: z.string().min(1), - potential_impact: z.string().min(1), - evidence_of_vulnerability: z.string().min(1), - what_we_tried: z.string().min(1), - how_this_would_be_exploited: z.array(z.string().min(1)).min(1), - expected_impact: z.string().min(1), + confidence: stringEnum(CONFIDENCE_VALUES), + current_blocker: Type.String({ minLength: 1 }), + potential_impact: Type.String({ minLength: 1 }), + evidence_of_vulnerability: Type.String({ minLength: 1 }), + what_we_tried: Type.String({ minLength: 1 }), + how_this_would_be_exploited: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }), + expected_impact: Type.String({ minLength: 1 }), notes: notesField, }); - const StrictSchema = z.discriminatedUnion('status', [ExploitedSchema, BlockedSchema]); + const StrictSchema = Type.Union([ExploitedSchema, BlockedSchema]); - return { flatShape, StrictSchema }; + return { flatSchema, StrictSchema }; } // ============================================================================ // RESPONSE HELPERS // ============================================================================ -interface ToolResult { - [x: string]: unknown; - content: Array<{ type: 'text'; text: string }>; - isError: boolean; -} - -function createToolResult(response: { status: string; [key: string]: unknown }): ToolResult { +function toolResult(payload: Record) { return { - content: [{ type: 'text', text: JSON.stringify(response, null, 2) }], - isError: response.status === 'error', + content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }], + details: undefined, }; } -function successResult(data: Record): ToolResult { - return createToolResult({ status: 'success', ...data }); +function successResult(data: Record) { + return toolResult({ status: 'success', ...data }); } -function errorResult(message: string, errorType = 'ValidationError', retryable = true): ToolResult { - return createToolResult({ status: 'error', message, errorType, retryable }); +function errorResult(message: string, errorType = 'ValidationError', retryable = true) { + return toolResult({ status: 'error', message, errorType, retryable }); } -function formatZodIssues(error: z.ZodError): string { - return error.issues - .map((issue) => { - const path = issue.path.length > 0 ? issue.path.join('.') : '(root)'; - return `- ${path}: ${issue.message}`; - }) - .join('\n'); +function formatValueErrors(schema: TSchema, value: unknown): string { + const issues: string[] = []; + for (const err of Value.Errors(schema, value)) { + const path = + err.instancePath && err.instancePath.length > 0 + ? err.instancePath.replace(/^\//, '').replace(/\//g, '.') + : '(root)'; + issues.push(`- ${path}: ${err.message}`); + } + return issues.join('\n'); } // ============================================================================ -// SERVER FACTORY +// COLLECTOR FACTORY // ============================================================================ -export interface ExploitCollectorServer { - server: McpSdkServerConfigWithInstance; +export interface ExploitCollector { + tools: ToolDefinition[]; getAll(): AddExploitInput[]; } @@ -416,14 +382,16 @@ export interface CreateExploitCollectorOptions { validIds: ReadonlySet; } -export function createExploitCollector(options: CreateExploitCollectorOptions): ExploitCollectorServer { +export function createExploitCollector(options: CreateExploitCollectorOptions): ExploitCollector { const { vulnClass, validIds } = options; const exploits: AddExploitInput[] = []; - const { flatShape, StrictSchema } = buildSchemas(validIds); + const { flatSchema, StrictSchema } = buildSchemas(validIds); - const addExploitTool = tool( - 'add_exploit', - `Record a single processed ${vulnClass} vulnerability as structured exploitation evidence. ` + + const addExploitTool = defineTool({ + name: 'add_exploit', + label: 'Add Exploit', + description: + `Record a single processed ${vulnClass} vulnerability as structured exploitation evidence. ` + 'Call this once per vulnerability in your queue.json after reaching a definitive verdict ' + '(either successfully exploited or potential-but-blocked). The status field discriminates the ' + "two report buckets; required sub-fields differ per status (see each field's description for " + @@ -432,20 +400,31 @@ export function createExploitCollector(options: CreateExploitCollectorOptions): 'IDs. FALSE POSITIVE findings do NOT use this tool — they go to your workspace tracking file. ' + 'After all queue vulnerabilities have been emitted, the host renderer assembles the ' + 'deliverable Markdown from your recorded calls.', - flatShape, - async (input): Promise => { + parameters: flatSchema, + async execute(_toolCallId, input) { // Re-validate against the strict discriminated union for per-status enforcement. - const parsed = StrictSchema.safeParse(input); - if (!parsed.success) { + if (!Value.Check(StrictSchema, input)) { return errorResult( `Schema validation failed for status="${(input as { status?: string }).status}". ` + 'Required-field issues:\n' + - formatZodIssues(parsed.error), + formatValueErrors(StrictSchema, input), 'ValidationError', true, ); } - const typed = parsed.data as AddExploitInput; + const typed = Value.Clean(StrictSchema, structuredClone(input)) as AddExploitInput; + + // Reject IDs not in this run's queue (typo'd or hallucinated). + if (!validIds.has(typed.vulnerability_id)) { + return errorResult( + `Vulnerability ID "${typed.vulnerability_id}" not in this run's queue. Valid IDs: ` + + `${formatValidIdsPreview(validIds)}. ` + + 'Check the queue.json for the canonical ID — likely a typo or hallucinated ID.', + 'ValidationError', + true, + ); + } + const existing = exploits.find((e) => e.vulnerability_id === typed.vulnerability_id); if (existing) { return errorResult( @@ -458,16 +437,10 @@ export function createExploitCollector(options: CreateExploitCollectorOptions): exploits.push(typed); return successResult({ added: [typed.vulnerability_id], recorded_status: typed.status }); }, - ); - - const server: McpSdkServerConfigWithInstance = createSdkMcpServer({ - name: 'exploit-collector', - version: '1.0.0', - tools: [addExploitTool], }); return { - server, + tools: [addExploitTool], getAll: (): AddExploitInput[] => [...exploits], }; } diff --git a/apps/worker/src/collectors/pre-recon-collector.ts b/apps/worker/src/collectors/pre-recon-collector.ts new file mode 100644 index 0000000..2c0ee91 --- /dev/null +++ b/apps/worker/src/collectors/pre-recon-collector.ts @@ -0,0 +1,602 @@ +// 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. + +/** + * Pre-Recon Collector tools + * + * Exposes seven TypeBox-validated tools, one per section of the + * pre_recon_deliverable.md report. Every tool is one-shot (write-once; + * duplicate calls return DuplicateError). A skipped tool renders a placeholder + * rather than failing the activity. After the agent finishes, the host calls + * getAll() to harvest the typed payload bag, getCallStatus() to log the + * per-run call pattern, and runs the deterministic renderer to produce the + * deliverable Markdown. + * + * Each TypeBox schema's field-level descriptions carry the section guidance, so + * the harness injects it into the agent's tool catalog. + */ + +import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent'; +import { type Static, Type } from 'typebox'; +import { cleanInput } from './schema.js'; + +// ============================================================================ +// SHARED SCHEMA +// ============================================================================ + +export const SinkRefSchema = Type.Object({ + location: Type.String({ + minLength: 1, + description: + 'File path with line number (e.g., "templates/render.js:34") or richer prose ' + + '(e.g., "innerHTML at templates/render.js:34", "lines 45-67"). Must contain enough ' + + 'detail for a downstream agent to find the exact location.', + }), + sink_function: Type.String({ + minLength: 1, + description: 'The sink function or property name (e.g., "innerHTML", "axios.get", "eval", "document.write").', + }), + notes: Type.Optional( + Type.Union([Type.String(), Type.Null()], { + description: + 'Optional context — render-context detail, attribute name, scope hints, or anything ' + + 'a downstream agent needs to act on this sink. Omit when the location and sink_function ' + + 'are sufficient on their own.', + }), + ), +}); + +export type SinkRef = Static; + +// ============================================================================ +// PER-TOOL INPUT SCHEMAS +// ============================================================================ + +export const ExecutiveSummaryInputSchema = Type.Object({ + text: Type.String({ + minLength: 1, + description: + "Provide a 2-3 paragraph overview of the application's security posture, highlighting " + + 'the most critical attack surfaces and architectural security decisions. Becomes ' + + 'Section 1 of the rendered deliverable.', + }), +}); + +export const ApplicationIntelligenceInputSchema = Type.Object({ + architecture: Type.Object( + { + framework_and_language: Type.String({ + minLength: 1, + description: 'Framework and language details with their security implications.', + }), + architectural_pattern: Type.String({ + minLength: 1, + description: 'Architectural pattern (monolith, microservices, hybrid) with trust boundary analysis.', + }), + critical_security_components: Type.String({ + minLength: 1, + description: 'Critical security components with focus on auth, authz, and data protection.', + }), + }, + { + description: + 'Architecture & Technology Stack — driven by the Architecture Scanner sub-agent. ' + + 'Becomes Section 2 of the rendered deliverable.', + }, + ), + data_security: Type.Object( + { + database_security: Type.String({ + minLength: 1, + description: 'Analyze encryption, access controls, and query safety in database interactions.', + }), + data_flow_security: Type.String({ + minLength: 1, + description: 'Identify sensitive data paths and the protection mechanisms applied along them.', + }), + multi_tenant_isolation: Type.String({ + minLength: 1, + description: + 'Assess tenant separation effectiveness. If the application is single-tenant, state that ' + + 'explicitly rather than leaving the field thin.', + }), + }, + { + description: + 'Data Security & Storage — driven by the Data Security Auditor sub-agent. ' + + 'Becomes Section 4 of the rendered deliverable.', + }, + ), + attack_surface: Type.Object( + { + external_entry_points: Type.String({ + minLength: 1, + description: 'Detailed analysis of each public interface that is network-accessible.', + }), + internal_service_communication: Type.String({ + minLength: 1, + description: + 'Trust relationships and security assumptions between network-reachable services. ' + + 'If the application is a single service with no internal RPC fabric, state that.', + }), + input_validation_patterns: Type.String({ + minLength: 1, + description: 'How user input is handled and validated in network-accessible endpoints.', + }), + background_processing: Type.String({ + minLength: 1, + description: + 'Async job security and privilege models for jobs triggered by network requests. ' + + 'If no async/background processing exists, state that.', + }), + }, + { + description: + 'Attack Surface Analysis — driven by Entry Point Mapper + Architecture Scanner sub-agents. ' + + 'Only include entry points confirmed to be in-scope (network-reachable). ' + + 'Becomes Section 5 of the rendered deliverable.', + }, + ), + infrastructure: Type.Object( + { + secrets_management: Type.String({ + minLength: 1, + description: 'How secrets are stored, rotated, and accessed.', + }), + configuration_security: Type.String({ + minLength: 1, + description: + 'Environment separation and secret handling. Specifically search for infrastructure ' + + 'configuration (e.g., Nginx, Kubernetes Ingress, CDN settings) that defines security ' + + 'headers like Strict-Transport-Security (HSTS) and Cache-Control, and report what was found.', + }), + external_dependencies: Type.String({ + minLength: 1, + description: 'Third-party services and their security implications.', + }), + monitoring_and_logging: Type.String({ + minLength: 1, + description: 'Security event visibility — what is logged, where it goes, and who can see it.', + }), + }, + { + description: 'Infrastructure & Operational Security. Becomes Section 6 of the rendered deliverable.', + }, + ), +}); + +export const AuthDeepDiveInputSchema = Type.Object({ + authentication_mechanisms: Type.String({ + minLength: 1, + description: + 'Authentication mechanisms and their security properties. MUST include an exhaustive list of ' + + 'all API endpoints used for authentication (e.g., login, logout, token refresh, password reset).', + }), + session_management: Type.String({ + minLength: 1, + description: + 'Session management and token security. Pinpoint the exact file and line(s) of code where ' + + 'session cookie flags (HttpOnly, Secure, SameSite) are configured.', + }), + authz_model: Type.String({ + minLength: 1, + description: 'Authorization model and potential bypass scenarios.', + }), + multi_tenancy: Type.String({ + minLength: 1, + description: 'Multi-tenancy security implementation. If the application is single-tenant, state that explicitly.', + }), + sso_oauth_oidc: Type.Union([Type.String(), Type.Null()], { + description: + 'SSO/OAuth/OIDC flows: identify the callback endpoints and locate the specific code that ' + + 'validates the state and nonce parameters. Set null only if the application has no SSO/OAuth/OIDC ' + + 'integration at all.', + }), +}); + +export const CodebaseIndexingInputSchema = Type.Object({ + text: Type.String({ + minLength: 1, + description: + "A detailed, multi-sentence paragraph describing the codebase's directory structure, " + + 'organization, and significant tools or conventions used (e.g., build orchestration, code ' + + 'generation, testing frameworks). Focus on how this structure impacts discoverability of ' + + 'security-relevant components.', + }), +}); + +export const CriticalFilePathsInputSchema = Type.Object({ + configuration: Type.Array(Type.String({ minLength: 1 }), { + description: 'Configuration files (e.g., config/server.yaml, Dockerfile, docker-compose.yml).', + }), + authentication_and_authorization: Type.Array(Type.String({ minLength: 1 }), { + description: + 'Auth/authz files (e.g., auth/jwt_middleware.go, internal/user/permissions.go, ' + + 'config/initializers/session_store.rb, src/services/oauth_callback.js).', + }), + api_and_routing: Type.Array(Type.String({ minLength: 1 }), { + description: + 'API and routing files (e.g., cmd/api/main.go, internal/handlers/user_routes.go, ' + + 'ts/graphql/schema.graphql).', + }), + data_models_and_db: Type.Array(Type.String({ minLength: 1 }), { + description: + 'Data model and DB interaction files (e.g., db/migrations/001_initial.sql, ' + + 'internal/models/user.go, internal/repository/sql_queries.go).', + }), + dependency_manifests: Type.Array(Type.String({ minLength: 1 }), { + description: 'Dependency manifests (e.g., go.mod, package.json, requirements.txt).', + }), + sensitive_data_and_secrets: Type.Array(Type.String({ minLength: 1 }), { + description: + 'Sensitive data and secrets handling (e.g., internal/utils/encryption.go, ' + 'internal/secrets/manager.go).', + }), + middleware_and_input_validation: Type.Array(Type.String({ minLength: 1 }), { + description: + 'Middleware and input validation (e.g., internal/middleware/validator.go, ' + + 'internal/handlers/input_parsers.go).', + }), + logging_and_monitoring: Type.Array(Type.String({ minLength: 1 }), { + description: 'Logging and monitoring (e.g., internal/logging/logger.go, config/monitoring.yaml).', + }), + infrastructure_and_deployment: Type.Array(Type.String({ minLength: 1 }), { + description: + 'Infrastructure and deployment (e.g., infra/pulumi/main.go, kubernetes/deploy.yaml, ' + + 'nginx.conf, gateway-ingress.yaml).', + }), +}); + +export const XssSinksInputSchema = Type.Object({ + applicable: Type.Boolean({ + description: + 'False only if the application has no web frontend at all. Otherwise true, even if no ' + + 'sinks were found in a given category — empty arrays mean "scanned this category, no sinks found".', + }), + html_body: Type.Array(SinkRefSchema, { + description: + 'HTML Body Context sinks: element.innerHTML, element.outerHTML, document.write(), ' + + 'document.writeln(), element.insertAdjacentHTML(), Range.createContextualFragment(), ' + + 'and jQuery sinks like add(), after(), append(), before(), html(), prepend(), replaceWith(), wrap().', + }), + html_attribute: Type.Array(SinkRefSchema, { + description: + 'HTML Attribute Context sinks: event handlers (onclick, onerror, onmouseover, onload, onfocus), ' + + 'URL-based attributes (href, src, formaction, action, background, data), the style attribute, ' + + 'iframe srcdoc, and general attributes (value, id, class, name, alt) when quotes are escaped.', + }), + javascript: Type.Array(SinkRefSchema, { + description: + 'JavaScript Context sinks: eval(), Function() constructor, setTimeout() / setInterval() ' + + 'with string arguments, and direct writes of user data into a