mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-08 01:01:10 +02:00
Compare commits
16
Commits
feat/capella
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45062a8f9e | ||
|
|
f6a770d119 | ||
|
|
685dc6c756 | ||
|
|
84212f376d | ||
|
|
0ab7c0b41b | ||
|
|
a14c7944d8 | ||
|
|
57c511ff8e | ||
|
|
327c10fd90 | ||
|
|
22b093aac5 | ||
|
|
25b90b0611 | ||
|
|
2786f9aa2d | ||
|
|
d41d52f17d | ||
|
|
4b8131fdd5 | ||
|
|
e92ee61c05 | ||
|
|
1f364522ca | ||
|
|
9767ebe633 |
No files matched your search
+9
-13
@@ -9,11 +9,11 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
|
|
||||||
# --- OpenAI ------------------------------------------------------------------
|
# --- OpenAI ------------------------------------------------------------------
|
||||||
# SHANNON_AI_API_KEY=your-api-key-here
|
# SHANNON_AI_API_KEY=your-api-key-here
|
||||||
# SHANNON_AI_MODEL=openai:gpt-5.5
|
# SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
|
|
||||||
# --- xAI ---------------------------------------------------------------------
|
# --- xAI ---------------------------------------------------------------------
|
||||||
# SHANNON_AI_API_KEY=your-api-key-here
|
# SHANNON_AI_API_KEY=your-api-key-here
|
||||||
# SHANNON_AI_MODEL=xai:grok-4.5
|
# SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
|
|
||||||
# --- AWS Bedrock -------------------------------------------------------------
|
# --- AWS Bedrock -------------------------------------------------------------
|
||||||
# Bearer token only; model must be enabled in your region.
|
# Bearer token only; model must be enabled in your region.
|
||||||
@@ -22,21 +22,15 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
# SHANNON_AI_MODEL=amazon-bedrock:us.anthropic.claude-opus-4-8
|
# SHANNON_AI_MODEL=amazon-bedrock:us.anthropic.claude-opus-4-8
|
||||||
|
|
||||||
# --- Custom Base URL ---------------------------------------------------------
|
# --- Custom Base URL ---------------------------------------------------------
|
||||||
# Route through a proxy or gateway (LiteLLM, an internal endpoint).
|
# Anthropic Messages API:
|
||||||
# Pick the block matching the API dialect your gateway speaks, and uncomment all
|
|
||||||
# three lines. The provider prefix picks the dialect; the model id is whatever
|
|
||||||
# name your gateway serves it under.
|
|
||||||
|
|
||||||
# Anthropic compatible - Anthropic Messages:
|
|
||||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||||
# SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
# SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||||
|
|
||||||
# OpenAI compatible - Chat Completions (default) or Responses:
|
# OpenAI Responses API:
|
||||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||||
# SHANNON_AI_MODEL=openai:gpt-5.5
|
# SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
# SHANNON_AI_OPENAI_FORMAT=responses
|
|
||||||
|
|
||||||
# --- Other provider ----------------------------------------------------------
|
# --- Other provider ----------------------------------------------------------
|
||||||
# Any other provider the Pi harness supports. Name it in SHANNON_AI_MODEL and
|
# Any other provider the Pi harness supports. Name it in SHANNON_AI_MODEL and
|
||||||
@@ -44,6 +38,8 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
# and model at preflight.
|
# and model at preflight.
|
||||||
# SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3
|
# SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3
|
||||||
# SHANNON_AI_API_KEY=your-api-key-here
|
# SHANNON_AI_API_KEY=your-api-key-here
|
||||||
|
# Optional: point that provider at a proxy or LLM gateway.
|
||||||
|
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||||
|
|
||||||
# --- Misc --------------------------------------------------------------------
|
# --- Misc --------------------------------------------------------------------
|
||||||
# Forward /etc/hosts entries into the worker container.
|
# Forward /etc/hosts entries into the worker container.
|
||||||
@@ -52,9 +48,9 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
# See the guide below to use an OpenAI subscription
|
# See the guide below to use an OpenAI subscription
|
||||||
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription
|
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#openai-codex-chatgpt-pluspro-subscription
|
||||||
# SHANNON_USE_PI_AUTH=1
|
# SHANNON_USE_PI_AUTH=1
|
||||||
# SHANNON_AI_MODEL=openai-codex:gpt-5.5
|
# SHANNON_AI_MODEL=openai-codex:gpt-6-sol
|
||||||
|
|
||||||
# Or the guide below to use an xAI subscription
|
# Or the guide below to use an xAI subscription
|
||||||
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#xai-grok-subscription
|
# https://github.com/KeygraphHQ/shannon/blob/main/docs/ai-providers.md#xai-grok-subscription
|
||||||
# SHANNON_USE_PI_AUTH=1
|
# SHANNON_USE_PI_AUTH=1
|
||||||
# SHANNON_AI_MODEL=xai:grok-4.6
|
# SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
@@ -121,8 +121,8 @@ body:
|
|||||||
- "xAI"
|
- "xAI"
|
||||||
- "AWS Bedrock"
|
- "AWS Bedrock"
|
||||||
- "Custom base URL - Anthropic Messages"
|
- "Custom base URL - Anthropic Messages"
|
||||||
- "Custom base URL - OpenAI Chat Completions"
|
|
||||||
- "Custom base URL - OpenAI Responses"
|
- "Custom base URL - OpenAI Responses"
|
||||||
|
- "Other provider (Pi catalogue)"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pnpm biome:fix # Auto-fix lint, format, and import sorting
|
|||||||
|
|
||||||
**Monorepo tooling:** pnpm workspaces, Turborepo for task orchestration, Biome for linting/formatting. TypeScript compiler options shared via `tsconfig.base.json` at the root. All packages extend it, overriding only `rootDir` and `outDir`. Shared devDependencies (`typescript`, `@types/node`, `turbo`, `@biomejs/biome`) are hoisted to the root workspace.
|
**Monorepo tooling:** pnpm workspaces, Turborepo for task orchestration, Biome for linting/formatting. TypeScript compiler options shared via `tsconfig.base.json` at the root. All packages extend it, overriding only `rootDir` and `outDir`. Shared devDependencies (`typescript`, `@types/node`, `turbo`, `@biomejs/biome`) are hoisted to the root workspace.
|
||||||
|
|
||||||
**Options:** `-c <file>` (YAML config), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
**Options:** `-c <file>` (YAML config), `--models-config <file>` (pi `models.json` defining models pi's catalogue lacks), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--validate-auth` (run preflight and auth validation only, then stop; no pentest or report; requires a fresh workspace and an `authentication` block in the config), `--validate-model` (run the preflight model checks only — credential/registry resolution plus the cyber-access verification for cyber-gated providers — then stop; no pentest or report; requires a fresh workspace; needs no config; mutually exclusive with `--validate-auth`), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ Published as `@keygraph/shannon` on npm. Contains Docker orchestration and a dir
|
|||||||
- `apps/cli/src/config/resolver.ts` — Cascading config (npx only): env vars → `~/.shannon/config.toml` (parsed with `smol-toml`)
|
- `apps/cli/src/config/resolver.ts` — Cascading config (npx only): env vars → `~/.shannon/config.toml` (parsed with `smol-toml`)
|
||||||
- `apps/cli/src/config/writer.ts` — TOML serialization and secure file persistence (0o600)
|
- `apps/cli/src/config/writer.ts` — TOML serialization and secure file persistence (0o600)
|
||||||
- `apps/cli/src/commands/setup.ts` — Interactive TUI wizard (`@clack/prompts`) for provider credential setup (npx only)
|
- `apps/cli/src/commands/setup.ts` — Interactive TUI wizard (`@clack/prompts`) for provider credential setup (npx only)
|
||||||
- `apps/cli/src/paths.ts` — Repo/config path resolution (any absolute or relative path)
|
- `apps/cli/src/paths.ts` — Repo/config/models-config path resolution (any absolute or relative path). `MODELS_CONFIG_CONTAINER_PATH` is fixed at `/app/models.json` because the worker names it to pi rather than discovering it
|
||||||
- `apps/cli/src/version.ts` — Version reporting (npx: `package.json` version; local: `git-<sha>`)
|
- `apps/cli/src/version.ts` — Version reporting (npx: `package.json` version; local: `git-<sha>`)
|
||||||
- `apps/cli/src/tty.ts` — Terminal capability detection: `requireInteractive` guard (fails fast off-TTY instead of hanging on a prompt), `supportsColor` color gating (`NO_COLOR`/`FORCE_COLOR`), and `stdoutIsTerminal` for spinner/cursor output
|
- `apps/cli/src/tty.ts` — Terminal capability detection: `requireInteractive` guard (fails fast off-TTY instead of hanging on a prompt), `supportsColor` color gating (`NO_COLOR`/`FORCE_COLOR`), and `stdoutIsTerminal` for spinner/cursor output
|
||||||
- `apps/cli/src/commands/` — Command handlers
|
- `apps/cli/src/commands/` — Command handlers
|
||||||
@@ -126,7 +126,7 @@ Infra (Temporal) runs via `docker-compose.yml`. Workers are ephemeral `docker ru
|
|||||||
- `docker-compose.yml` — Infra only: `shannon-temporal` (port 7233/8233). Network: `shannon-net`
|
- `docker-compose.yml` — Infra only: `shannon-temporal` (port 7233/8233). Network: `shannon-net`
|
||||||
- `Dockerfile` — 2-stage build (builder + Chainguard Wolfi runtime). Uses pnpm. Entrypoint: `CMD ["node", "apps/worker/dist/temporal/worker.js"]`
|
- `Dockerfile` — 2-stage build (builder + Chainguard Wolfi runtime). Uses pnpm. Entrypoint: `CMD ["node", "apps/worker/dist/temporal/worker.js"]`
|
||||||
- No `docker-compose.docker.yml` — host gateway handled via `--add-host` flag in CLI
|
- No `docker-compose.docker.yml` — host gateway handled via `--add-host` flag in CLI
|
||||||
- `/etc/hosts` forwarding — at worker spawn, `forwardEtcHostsFlags` in `apps/cli/src/docker.ts` reads the host's `/etc/hosts` and emits one `--add-host` flag per valid user-added entry. Loopback IPs (`127.x`, `::1`) are rewritten to `host-gateway`; IPv6 addresses are bracketed. Disable per-scan via `SHANNON_FORWARD_HOSTS=false`. No-op on Windows native (WSL2 reads its own `/etc/hosts` via the Linux path).
|
- `/etc/hosts` forwarding — at worker spawn, `forwardEtcHostsFlags` in `apps/cli/src/docker.ts` reads the host's `/etc/hosts` and emits one `--add-host` flag per valid user-added entry. Loopback IPs (`127.x`, `::1`) are rewritten to `host-gateway`; IPv6 addresses are bracketed. Disable per-scan via `SHANNON_FORWARD_HOSTS=false`. Native Windows is refused at startup (`blockNativeWindows` in `apps/cli/src/index.ts`, pointing to the WSL2 guide in `docs/platforms.md`); WSL2 reads its own `/etc/hosts` via the Linux path.
|
||||||
|
|
||||||
### Worker Package (`apps/worker/`)
|
### Worker Package (`apps/worker/`)
|
||||||
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
||||||
@@ -165,7 +165,7 @@ Around those phases:
|
|||||||
- **Configuration** — YAML configs in `apps/worker/configs/` use the closed JSON Schema in `config-schema.json`. Every fresh scan runs the fixed five analysis classes; there is no public class selector. `agentic_sast.enabled` is the only public agentic-SAST setting. Finding reconciliation runs on every scan and has no public setting of its own. Config also supports authentication (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), `exploit`, free-form `rules_of_engagement`, and post-hoc `report` options (`min_severity`, `min_confidence`, `guidance`, and exploit-only `sarif` output via `apps/worker/src/services/sarif-renderer.ts`, on by default for exploit runs and opt out with `report.sarif: "false"`). `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/pi/permission-system.ts:syncPermissionSystemConfig`), and the executor loads the extension when that config is present (`apps/worker/src/ai/pi/pi-executor.ts`), so denies fire across every tool and child `task` session. 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/` use the closed JSON Schema in `config-schema.json`. Every fresh scan runs the fixed five analysis classes; there is no public class selector. `agentic_sast.enabled` is the only public agentic-SAST setting. Finding reconciliation runs on every scan and has no public setting of its own. Config also supports authentication (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), `exploit`, free-form `rules_of_engagement`, and post-hoc `report` options (`min_severity`, `min_confidence`, `guidance`, and exploit-only `sarif` output via `apps/worker/src/services/sarif-renderer.ts`, on by default for exploit runs and opt out with `report.sarif: "false"`). `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/pi/permission-system.ts:syncPermissionSystemConfig`), and the executor loads the extension when that config is present (`apps/worker/src/ai/pi/pi-executor.ts`), so denies fire across every tool and child `task` session. Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`)
|
||||||
- **Agentic SAST progress** — Capella runs as a child workflow, so its activities are absent from the parent's `pendingActivities` and invisible to the CLI. The child signals each stage boundary up via `capellaStageProgress` (`apps/worker/src/temporal/shared.ts`); the parent's handler validates the payload and writes the child-supplied `startedAt` and `durationMs` directly to `operationalStages['agentic-sast:<stage>']`, so both the live `getProgress` query and the terminal result carry per-stage rows. Signalling is best-effort and every failure is swallowed — a closed or unreachable parent must never fail a SAST run. `CAPELLA_STAGE_LABELS` in `apps/worker/src/ai/sast/types.ts` is the one label table, shared by the scan log and the status tree; `CAPELLA_PROGRESS_STAGES` omits `export`, which runs no model and so never becomes a row. Scans predating the signal keep the aggregate `agentic-sast` span and render as a bare phase line
|
- **Agentic SAST progress** — Capella runs as a child workflow, so its activities are absent from the parent's `pendingActivities` and invisible to the CLI. The child signals each stage boundary up via `capellaStageProgress` (`apps/worker/src/temporal/shared.ts`); the parent's handler validates the payload and writes the child-supplied `startedAt` and `durationMs` directly to `operationalStages['agentic-sast:<stage>']`, so both the live `getProgress` query and the terminal result carry per-stage rows. Signalling is best-effort and every failure is swallowed — a closed or unreachable parent must never fail a SAST run. `CAPELLA_STAGE_LABELS` in `apps/worker/src/ai/sast/types.ts` is the one label table, shared by the scan log and the status tree; `CAPELLA_PROGRESS_STAGES` omits `export`, which runs no model and so never becomes a row. Scans predating the signal keep the aggregate `agentic-sast` span and render as a bare phase line
|
||||||
- **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
|
- **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
|
||||||
- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/pi-executor.ts` (`runPiPrompt` → `createAgentSession`). Retry is split in `apps/worker/src/ai/pi/retry-settings.ts`: pi's agent-level loop is off so Temporal owns agent restarts, while `provider.maxRetries` stays on — pi reads the `provider` block independently of the `enabled` flag — so transport faults are absorbed in-session rather than costing a full agent re-run. `maxRetryDelayMs` is left at pi's 60s default. One model runs every phase, named by `SHANNON_AI_MODEL=<provider>:<model-id>` (default `anthropic:claude-sonnet-4-6`). `apps/worker/src/ai/models.ts` parses the spec — splitting on the **first** colon only, so Bedrock IDs keep theirs — and resolves it through pi's `ModelRuntime`. pi ships the `CredentialStore` interface but no in-memory implementation (its own reads `auth.json` from disk), so `RuntimeCredentialStore` in that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk. `createModelRuntime(providerId, apiKey)` builds the runtime; `allowModelNetwork` stays at its default `false` so a scan never blocks on a catalog refresh. `resolveModelSelection()` is **async** because `ModelRuntime.create()` is. Any pi-ai provider id is accepted — `parseModelSpec` no longer rejects against a hardcoded list, so pi's registry is the authority (an unknown provider/model surfaces as a clear "not found in pi registry" error at preflight, which points to the browsable catalogue at `pi.dev/models` — `PI_CATALOG_URL` in `apps/worker/src/ai/models.ts`, appended to the not-found errors and shown in the setup wizard's "Other provider" hint). Four providers are **curated** (`CURATED_PROVIDERS`: `anthropic`, `openai`, `xai`, `amazon-bedrock`) with their own credential variables, config sections, and setup flows; each provider's API key env var is declared once in `PROVIDER_API_KEY_ENV` — Shannon uses each vendor's own variable name (`OPENAI_API_KEY`, `XAI_API_KEY`, …), never an invented one; Bedrock's entry is `AWS_BEARER_TOKEN_BEDROCK`, paired with `AWS_REGION`, which preflight requires separately as provider config rather than a credential. Any other provider uses the **generic** credential path: `SHANNON_AI_API_KEY` (`GENERIC_API_KEY_ENV`) supplies the key for any provider whose credential is a plain API key. Curated providers' own variables take precedence over it, and it also works as a fallback for them — Bedrock is the sole exception (it authenticates through its AWS_ variables, so the generic key never stands in for it). The CLI forwards `SHANNON_AI_API_KEY` in `COMMON_FORWARD_VARS` (it is provider-neutral, binding to whatever `SHANNON_AI_MODEL` names, so the "only one provider configured" guard counts only named credentials), and stores it under a generic `[provider]` config.toml section (`provider.api_key`). `npx @keygraph/shannon setup` exposes this as the "Other provider" option: free-text provider id + model id + key (a curated provider id is rejected there, since it has its own option). `SHANNON_AI_BASE_URL` overrides the endpoint for any provider (proxies/gateways); the credential is unchanged. `pointAtGateway` (`apps/worker/src/ai/models.ts`) applies the one dialect change: behind a base URL, `openai` follows `SHANNON_AI_OPENAI_FORMAT` (`chat-completions` default, or `responses`). On `chat-completions` it switches the API to `openai-completions` and drops the catalogue's Responses-shaped `compat` block so pi's `detectCompat` derives completions settings; on `responses` the descriptor is unchanged but for the endpoint. `resolveGatewayFormat` rejects the variable when the provider is not `openai` or no base URL is set, since it cannot take effect there. All other providers keep their API. The CLI mirrors the accepted values in `apps/cli/src/model-spec.ts`, forwards the variable in `COMMON_FORWARD_VARS`, and maps it to `openai.format` in config.toml. `buildEnvFlags` forwards only the selected provider's credential into the worker container. The CLI mirrors the parse rule and the provider/credential tables in `apps/cli/src/model-spec.ts` (it cannot import from the worker package); the two must stay in sync. 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` (child sessions scoped to `read`, `grep`, `find`, `ls`, `write`, and `bash` — no nested `task` or collector tools; `CHILD_TOOLS` in `apps/worker/src/ai/pi/task-tool.ts`) + `todo_write` (`apps/worker/src/ai/pi/session-tools.ts`) are provided as custom tools; the per-phase collectors are pi custom tools (TypeBox `defineTool` in `apps/worker/src/collectors/`). Shannon sets no thinking configuration at all — no `thinkingLevel` is passed to any `createAgentSession` call, so pi's own default applies. There Line truncated
|
- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/pi-executor.ts` (`runPiPrompt` → `createAgentSession`). Retry is split in `apps/worker/src/ai/pi/retry-settings.ts`: pi's agent-level loop is off so Temporal owns agent restarts, while `provider.maxRetries` stays on — pi reads the `provider` block independently of the `enabled` flag — so transport faults are absorbed in-session rather than costing a full agent re-run. `maxRetryDelayMs` is left at pi's 60s default. One model runs every phase, named by `SHANNON_AI_MODEL=<provider>:<model-id>` (default `anthropic:claude-sonnet-4-6`). `apps/worker/src/ai/models.ts` parses the spec — splitting on the **first** colon only, so Bedrock IDs keep theirs — and resolves it through pi's `ModelRuntime`. pi ships the `CredentialStore` interface but no in-memory implementation (its own reads `auth.json` from disk), so `RuntimeCredentialStore` in that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk. `createModelRuntime(providerId, apiKey)` builds the runtime with `allowModelNetwork: true`, so `ModelRuntime.create()` refreshes the model catalogue over the network at scan start and a freshly released model resolves without a `--models-config` file. The fetch is bounded (10s) and falls back to the static catalogue on timeout, so an unreachable catalogue endpoint cannot hang the scan. The refresh does not override a `--models-config`: pi reloads and re-applies that file as a config overlay on every refresh (it reloads `this.config` at the top of `refresh()`), so custom definitions still win over the fetched catalogue; the merge semantics below are unchanged, just layered over a fresher base. `resolveModelSelection()` is **async** because `ModelRuntime.create()` is. Any pi-ai provider id is accepted — `parseModelSpec` no longer rejects against a hardcoded list, so pi's registry is the authority (an unknown provider/model surfaces as a clear "not found in pi registry" error at preflight, which points to the browsable catalogue at `pi.dev/models` — `PI_CATALOG_URL` in `apps/worker/src/ai/models.ts`, appended to the not-found errors and shown in the setup wizard's "Other provider" hint). Four providers are **curated** (`CURATED_PROVIDERS`: `anthropic`, `openai`, `xai`, `amazon-bedrock`) with their own credential variables, config sections, and setup flows; each provider's API key env var is declared once in `PROVIDER_API_KEY_ENV` — Shannon uses each vendor's own variable name (`OPENAI_API_KEY`, `XAI_API_KEY`, …), never an invented one; Bedrock's entry is `AWS_BEARER_TOKEN_BEDROCK`, paired with `AWS_REGION`, which preflight requires separately as provider config rather than a credential. Any other provider uses the **generic** credential path: `SHANNON_AI_API_KEY` (`GENERIC_API_KEY_ENV`) supplies the key for any provider whose credential is a plain API key. Curated providers' own variables take precedence over it, and it also works as a fallback for them — Bedrock is the sole exception (it authenticates through its AWS_ variables, so the generic key never stands in for it). The CLI forwards `SHANNON_AI_API_KEY` in `COMMON_FORWARD_VARS` (it is provider-neutral, binding to whatever `SHANNON_AI_MODEL` names, so the "only one provider configured" guard counts only named credentials), and stores it under a generic `[provider]` config.toml section (`provider.api_key`). `npx @keygraph/shannon setup` exposes this as the "Other provider" option: free-text provider id + model id + key (a curated provider id is rejected there, since it has its own option). A model pi's catalogue does not carry, such as a self-hosted model, is reachable without an SDK bump: `--models-config <file>` mounts a pi `models.json` read-only at `/app/models.json`. The mount is the entire CLI→worker protocol: nothing is forwarded through the environment, and `modelsConfigPath()` detects the file at that fixed path, exactly as `piAuthPresent()` detects the pi auth mount whose flag is likewise not forwarded (`MODELS_CONFIG_CONTAINER_PATH` in the CLI and `MODELS_CONFIG_PATH` in `apps/worker/src/paths.ts` must stay in sync). `createModelRuntime` always names `modelsPath` explicitly — the mounted path, or **`null` when no config was supplied**, which switches models.json off outright. It is never left to pi's default of `<agent dir>/models.json`, because that dir is shared with the pi auth mount, so a file landing there must not silently contribute model definitions to a scan that did not ask for one. `modelsStorePath` is pinned to the agent dir alongside it, since pi otherwise derives it from `dirname(modelsPath)` and would try to write beside a read-only mount. Custom definitions merge over the built-in catalogue: a matching model id replaces the built-in entry, a new id is added alongside, and `modelOverrides` adjusts a built-in without replacing the proviLine truncated
|
||||||
- **Pi Credential Reuse** — `SHANNON_USE_PI_AUTH=1` opts into reusing the host's Pi login, including an `openai-codex` ChatGPT Plus/Pro subscription (`SHANNON_AI_MODEL=openai-codex:<model-id>`) or an `xai` Grok subscription (`SHANNON_AI_MODEL=xai:<model-id>`); the mechanism is provider-agnostic and works for any Pi login. `apps/cli/src/env.ts` requires `~/.pi/agent/auth.json`; `start.ts` passes its path to `spawnWorker`, which mounts only that file read-write at `/tmp/.pi/agent/auth.json`. The flag itself is not forwarded: the worker detects the file with `piAuthPresent()` and passes its path to `ModelRuntime.create`. CLI and worker API-key presence checks are skipped on this path, but the normal preflight model probe still validates the credential. The image and UID-remapping entrypoint keep `/tmp/.pi/agent` owned by `pentest` so adjacent Pi/Shannon configuration remains writable. Refreshed OAuth state is persisted to the host for subsequent scans.
|
- **Pi Credential Reuse** — `SHANNON_USE_PI_AUTH=1` opts into reusing the host's Pi login, including an `openai-codex` ChatGPT Plus/Pro subscription (`SHANNON_AI_MODEL=openai-codex:<model-id>`) or an `xai` Grok subscription (`SHANNON_AI_MODEL=xai:<model-id>`); the mechanism is provider-agnostic and works for any Pi login. `apps/cli/src/env.ts` requires `~/.pi/agent/auth.json`; `start.ts` passes its path to `spawnWorker`, which mounts only that file read-write at `/tmp/.pi/agent/auth.json`. The flag itself is not forwarded: the worker detects the file with `piAuthPresent()` and passes its path to `ModelRuntime.create`. CLI and worker API-key presence checks are skipped on this path, but the normal preflight model probe still validates the credential. The image and UID-remapping entrypoint keep `/tmp/.pi/agent` owned by `pentest` so adjacent Pi/Shannon configuration remains writable. Refreshed OAuth state is persisted to the host for subsequent scans.
|
||||||
- **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. The run directory's top level holds the human-facing report in both formats (`Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`, `FINAL_REPORT_PDF_FILENAME`/`FINAL_REPORT_MD_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 (`scans`/`logs`) without migration. A pre-restructure workspace cannot be resumed: `classifyWorkspaceLaunch` (`apps/cli/src/commands/start.ts`) requires `.shannon/launch.json`, and its absence fails the launch as "created by an earlier version of Shannon" before anything on disk is touched. There is no in-place migration — the workspace's files and report are left untouched, and the operator starts a new scan under a different `-w` name. The report agent writes structured findings to `report.json`, from which `report-renderer.ts` renders the assembled markdown and `report-json-adapter.ts` produces the Typst-shaped JSON that `pdf-renderer.ts` compiles into `comprehensive_security_assessment_report.pdf` using the bundled `apps/worker/templates/typst/report.typ` template (the `typst` binary is installed in the worker image). `copyReportToRunRoot` (`apps/worker/src/services/reporting.ts`) surfaces both the PDF and the markdown to the run root as `Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`; the deliverables-dir copies remain as the git-checkpointed sources. PDF compilation is best-effort — a failure is logged and the run still completes. 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. Every combined-log line is also projected into a per-agent file under `.shannon/agents/<slug>.log` (one per pipeline agent, one per Capella stage; subagents fold into the parent's file, and a stage's concurrent sessions share its file with an inline session label). The projection boundary is `apps/worker/src/audit/actor-projection.ts` (`projectActor` maps a `TraceActor` to its combined prefix and owning file slug — slugs come only from closed fields); fan-out is best-effort and never blocks the canonical combined log. A lifecycle owner holds a `LogStream` lease per agent file (the pipeline agent's `logAgent` span, or a Capella stage activity's `try/finally`) so per-line writes ride the reference count; `CapellaStageTrace.drain()` flushes a stage's trace queue before its activity returns. The CLI tails one file with `shannon logs --agent <name>` (`--list-agents` to enumerate); the default `shannon logs` path is unchanged
|
- **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. The run directory's top level holds the human-facing report in both formats (`Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`, `FINAL_REPORT_PDF_FILENAME`/`FINAL_REPORT_MD_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 (`scans`/`logs`) without migration. A pre-restructure workspace cannot be resumed: `classifyWorkspaceLaunch` (`apps/cli/src/commands/start.ts`) requires `.shannon/launch.json`, and its absence fails the launch as "created by an earlier version of Shannon" before anything on disk is touched. There is no in-place migration — the workspace's files and report are left untouched, and the operator starts a new scan under a different `-w` name. The report agent writes structured findings to `report.json`, from which `report-renderer.ts` renders the assembled markdown and `report-json-adapter.ts` produces the Typst-shaped JSON that `pdf-renderer.ts` compiles into `comprehensive_security_assessment_report.pdf` using the bundled `apps/worker/templates/typst/report.typ` template (the `typst` binary is installed in the worker image). `copyReportToRunRoot` (`apps/worker/src/services/reporting.ts`) surfaces both the PDF and the markdown to the run root as `Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`; the deliverables-dir copies remain as the git-checkpointed sources. PDF compilation is best-effort — a failure is logged and the run still completes. 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. Every combined-log line is also projected into a per-agent file under `.shannon/agents/<slug>.log` (one per pipeline agent, one per Capella stage; subagents fold into the parent's file, and a stage's concurrent sessions share its file with an inline session label). The projection boundary is `apps/worker/src/audit/actor-projection.ts` (`projectActor` maps a `TraceActor` to its combined prefix and owning file slug — slugs come only from closed fields); fan-out is best-effort and never blocks the canonical combined log. A lifecycle owner holds a `LogStream` lease per agent file (the pipeline agent's `logAgent` span, or a Capella stage activity's `try/finally`) so per-line writes ride the reference count; `CapellaStageTrace.drain()` flushes a stage's trace queue before its activity returns. The CLI tails one file with `shannon logs --agent <name>` (`--list-agents` to enumerate); the default `shannon logs` path is unchanged
|
||||||
- **Deliverables** — Saved to `.shannon/deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`)
|
- **Deliverables** — Saved to `.shannon/deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Shannon 3.0 is live:** deeper security code analysis, a rebuilt terminal experience, native CI/CD workflows, professional PDF reports, and SARIF—still fully open source, self-hosted, and bring-your-own-model.
|
> **[Cyber verification for Anthropic and OpenAI models](https://github.com/KeygraphHQ/shannon/discussions/483):** complete your provider's cyber verification program to prevent model failures and refusals during cyber workloads.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -17,6 +17,14 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
|||||||
|
|
||||||
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
||||||
|
|
||||||
|
<p><strong>Launch Shannon</strong></p>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @keygraph/shannon@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
<sub>The interactive launcher will guide you through setup and your first pentest.</sub>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a> <a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
|
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a> <a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
|
||||||
@@ -70,19 +78,29 @@ Shannon analyzes your web application's source code to identify potential attack
|
|||||||
|
|
||||||
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
||||||
|
|
||||||
### Why Shannon Exists
|
<a id="why-shannon-exists"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Why Shannon Exists</strong></summary>
|
||||||
|
|
||||||
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
||||||
|
|
||||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||||
|
|
||||||
### Why "Shannon"?
|
</details>
|
||||||
|
|
||||||
|
<a id="why-shannon"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Why "Shannon"?</strong></summary>
|
||||||
|
|
||||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||||
|
|
||||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||||
|
|
||||||
### Not a replacement for human pentesters
|
</details>
|
||||||
|
|
||||||
|
<a id="not-a-replacement-for-human-pentesters"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Not a replacement for human pentesters</strong></summary>
|
||||||
|
|
||||||
Shannon is built to work alongside expert pentesters and red teamers, not replace them. Great pentesters understand the business, chain attacks in ways nobody anticipated, and bring years of judgment that current models can't match.
|
Shannon is built to work alongside expert pentesters and red teamers, not replace them. Great pentesters understand the business, chain attacks in ways nobody anticipated, and bring years of judgment that current models can't match.
|
||||||
|
|
||||||
@@ -90,11 +108,13 @@ Shannon solves a different problem: there is far more software to test than secu
|
|||||||
|
|
||||||
Shannon shifts pentesting left into the software development lifecycle (SDLC). Use it to run exploitation-backed tests against staging environments and releases at the cadence they actually ship, and save expert human time for the risks that need someone who knows the organization.
|
Shannon shifts pentesting left into the software development lifecycle (SDLC). Use it to run exploitation-backed tests against staging environments and releases at the cadence they actually ship, and save expert human time for the risks that need someone who knows the organization.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Shannon in Action
|
## Shannon in Action
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read the [announcement][announcement] and the full [benchmark writeup][benchmark] for methodology, cost, and the comparison against Aikido and XBOW.
|
These reports are from Shannon Open Source scans of Photoview 2.4.0, one of the applications in Doyensec's comparison of Aikido and XBOW. We ran Shannon against the same application version and evaluated its results separately. Read the [Doyensec study](https://doyensec.com/resources/ComparingAIApplicationSecurityTestingPlatforms_Doyensec.pdf) and our [Shannon follow-up comparison](docs/shannon-xbow-aikido-benchmark.md) for the methodology, limitations, costs, and results.
|
||||||
|
|
||||||
|
|
||||||
| Model | Report | SARIF |
|
| Model | Report | SARIF |
|
||||||
@@ -103,12 +123,6 @@ Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read
|
|||||||
| Grok 4.6 | [View report](benchmark/photoview-grok-4-6.pdf) | [SARIF](benchmark/photoview-grok-4-6.sarif) |
|
| Grok 4.6 | [View report](benchmark/photoview-grok-4-6.pdf) | [SARIF](benchmark/photoview-grok-4-6.sarif) |
|
||||||
| Claude Opus 5 | [View report](benchmark/photoview-opus-5.pdf) | [SARIF](benchmark/photoview-opus-5.sarif) |
|
| Claude Opus 5 | [View report](benchmark/photoview-opus-5.pdf) | [SARIF](benchmark/photoview-opus-5.sarif) |
|
||||||
|
|
||||||
[announcement]: https://github.com/KeygraphHQ/shannon/discussions/439
|
|
||||||
[benchmark]: docs/shannon-xbow-aikido-benchmark.md
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
||||||
@@ -117,8 +131,8 @@ Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read
|
|||||||
|
|
||||||
- **Docker**: required for the worker container.
|
- **Docker**: required for the worker container.
|
||||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a [custom base URL](docs/ai-providers.md#custom-base-url). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue — each of which you can point at a proxy or LLM gateway through a [custom base URL](docs/ai-providers.md#custom-base-url), and a model the catalogue does not carry can be described with a [custom model configuration](docs/ai-providers.md#custom-model-configuration). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan) and the [cyber verification announcement](https://github.com/KeygraphHQ/shannon/discussions/483).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -152,17 +166,17 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
|||||||
|
|
||||||
## Key Capabilities
|
## Key Capabilities
|
||||||
|
|
||||||
- **No exploit, no report**: Shannon includes a vulnerability only after validating it with a working, reproducible proof of concept—eliminating the speculative warnings typical of scanners.
|
- **No exploit, no report**: Reports only vulnerabilities confirmed with a reproducible proof of concept, reducing speculative scanner noise.
|
||||||
- **Advanced security code analysis**: Before it sends a single payload, Shannon reads the codebase and builds a picture of the application: architecture, trust boundaries, exposed interfaces, data flows, and the assets worth attacking. From there it opens targeted investigations and filters the candidates they turn up. What survives goes to the live pentesting agents.
|
- **Advanced code analysis**: Maps architecture, trust boundaries, interfaces, data flows, and critical assets before sending credible attack paths to live pentesting agents.
|
||||||
- **Autonomous execution**: Shannon launches reconnaissance, vulnerability analysis, exploitation, and report generation from a single command.
|
- **Autonomous execution**: Runs reconnaissance, analysis, exploitation, and reporting from a single command.
|
||||||
- **Live terminal experience**: A rebuilt CLI makes scans easy to configure and shows agent progress and clean results without requiring operators to inspect the underlying orchestration logs.
|
- **Live terminal experience**: Simplifies scan setup and shows agent progress and results without exposing orchestration logs.
|
||||||
- **Authenticated testing**: configuration files can describe login flows, test credentials, TOTP, email-based login flows, focus areas, and rules of engagement.
|
- **Authenticated testing**: Supports credentials, login flows, TOTP, email authentication, focus areas, and rules of engagement through configuration.
|
||||||
- **OWASP-focused coverage**: Shannon targets exploitable Injection, XSS, SSRF, Broken Authentication, and Broken Authorization issues.
|
- **OWASP-focused coverage**: Tests for exploitable injection, XSS, SSRF, broken authentication, and broken authorization.
|
||||||
- **Resumable workspaces**: Shannon can resume interrupted runs without re-running completed agents.
|
- **Resumable workspaces**: Resumes interrupted scans without repeating completed work.
|
||||||
- **Native CI/CD integrations**: Run Shannon through the official GitHub Action or reusable GitLab CI/CD component. Preserve reports, SARIF, and logs as pipeline artifacts; publish findings into native security workflows; and gate releases only on vulnerabilities Shannon actually demonstrates.
|
- **Native CI/CD integrations**: Runs through the official GitHub Action or GitLab CI/CD component, preserves artifacts, publishes findings, and gates releases on proven vulnerabilities.
|
||||||
- **Professional and machine-readable reports**: Shannon generates evidence-rich PDF and Markdown reports plus structured JSON and SARIF 2.1.0. SARIF is enabled by default on exploit-mode scans and can be disabled with `report.sarif: "false"`.
|
- **Multi-format reports**: Produces evidence-rich PDF and Markdown reports plus JSON and SARIF 2.1.0. SARIF is enabled by default for exploit-mode scans.
|
||||||
- **Bring your own key, provider-agnostic**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any endpoint speaking the Anthropic Messages API or the OpenAI Chat Completions or Responses API, including self-hosted models served through Ollama, vLLM, or LM Studio and gateways such as OpenRouter and LiteLLM. You supply the credentials and choose exactly where model traffic goes. Local and self-hosted models are supported.
|
- **Provider agnostic and BYOK**: Supports Anthropic, OpenAI, xAI, AWS Bedrock, compatible APIs and LLM gateways, and local models served through Ollama, vLLM, or LM Studio.
|
||||||
- **Private by design**: Shannon runs inside your infrastructure and writes results to a local workspace. Model requests go straight to the provider or endpoint you configure, and they carry source and application context with them, so choose that endpoint deliberately. Point Shannon at a local model endpoint and nothing leaves your environment.
|
- **Private by design**: Runs in your infrastructure, stores results locally, and sends model requests directly to your chosen endpoint. A local endpoint keeps data inside your environment.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -221,24 +235,11 @@ See the [Shannon GitHub Action documentation](https://github.com/KeygraphHQ/shan
|
|||||||
|
|
||||||
## Editions
|
## Editions
|
||||||
|
|
||||||
**Shannon Open Source** is the complete autonomous pentester for developers and security teams. It is optimized for fast local and CI/CD runs: understand the application, execute real attacks, and report only proven vulnerabilities.
|
**Shannon Open Source** is a complete autonomous pentester, especially well suited to individual developers and small teams running focused security tests locally or in CI/CD.
|
||||||
|
|
||||||
**Keygraph Enterprise Platform** turns Shannon's proof engine into an organization-wide AppSec program, adding exhaustive analysis, centralized vulnerability management, automated remediation, enterprise governance, and continuous operation at scale.
|
**Keygraph Enterprise Platform** is for organizations that need a shared platform for continuous agentic pentesting/AppSec across many teams, repositories, and environments. It centralizes deeper analysis, vulnerability management, remediation, verification, governance, and reporting so teams do not have to assemble and maintain those workflows themselves.
|
||||||
|
|
||||||
|
[Learn about the Keygraph Enterprise Platform and compare editions →](docs/keygraph-platform.md)
|
||||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
|
||||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| Best for | Local and CI/CD pentesting | Continuous AppSec across teams and repositories |
|
|
||||||
| Security analysis | Multi-stage agentic review models architecture, trust boundaries, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code agentic SAST: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic testing |
|
|
||||||
| Additional coverage | Not included | SCA with reachability, secrets scanning, and business-logic testing |
|
|
||||||
| AppSec operations | N/A — standalone CLI | Canonical findings, deduplication, SLAs, analytics, automated remediation, and targeted verification |
|
|
||||||
| Governance | N/A — local, single-operator CLI | SSO, SCIM, granular access control, APIs, and full audit logging |
|
|
||||||
| Deployment | Self-hosted, air-gapped, BYOM, AGPL-3.0 | On-premises or air-gapped, granular model routing, commercial support |
|
|
||||||
|
|
||||||
|
|
||||||
Shannon Open Source is not a trial edition. Choose Keygraph Enterprise when you need deeper analysis and a governed, closed-loop AppSec program.
|
|
||||||
|
|
||||||
[Explore the Keygraph Enterprise Platform →](docs/keygraph-platform.md)
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -285,7 +286,7 @@ Use these guides for operational detail:
|
|||||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. |
|
| [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, and report filters. |
|
| [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, and report filters. |
|
||||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom gateways. |
|
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom LLM gateways. |
|
||||||
| [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. |
|
| [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. |
|
| [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. |
|
| [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. |
|
||||||
@@ -305,7 +306,7 @@ Important limitations:
|
|||||||
|
|
||||||
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
||||||
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
||||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any Anthropic Messages API or OpenAI Chat Completions or Responses API endpoint works through a custom base URL. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any other provider in the harness catalogue works too — each reachable through a custom base URL that points it at a proxy or LLM gateway. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||||
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
||||||
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
||||||
|
|
||||||
@@ -374,11 +375,11 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re
|
|||||||
|
|
||||||
### Which AI providers does Shannon support?
|
### Which AI providers does Shannon support?
|
||||||
|
|
||||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any endpoint that implements the Anthropic Messages API or the OpenAI Chat Completions or Responses API, reached through a custom base URL. The rule is the API format, not the vendor. Shannon uses a single unified model setting throughout a pentest.
|
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any provider in the Pi harness catalogue, named the same `<provider>:<model-id>` way. Any provider can be pointed at a proxy or LLM gateway through a custom base URL, which overrides only the endpoint and keeps that provider's API dialect. A model the catalogue does not carry, such as one a router or gateway serves under its own ID, or a self-hosted model, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and passed with `--models-config`. Shannon uses a single unified model setting throughout a pentest.
|
||||||
|
|
||||||
### Can I run Shannon on a local or self-hosted model?
|
### Can I run Shannon on a local or self-hosted model?
|
||||||
|
|
||||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and gateways such as LiteLLM. Point Shannon at the endpoint with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [AI providers](docs/ai-providers.md#custom-base-url).
|
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and LLM gateways such as LiteLLM. A model the harness catalogue does not carry, which most self-hosted models are, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file passed with `--models-config`; routers and gateways can also be reached with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [Local and self-hosted models](docs/ai-providers.md#local-and-self-hosted-models).
|
||||||
|
|
||||||
### Does Shannon actually exploit vulnerabilities, or just scan?
|
### Does Shannon actually exploit vulnerabilities, or just scan?
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
|||||||
|
|
||||||
- **Docker**: required for the worker container.
|
- **Docker**: required for the worker container.
|
||||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, any other provider in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a custom base URL. You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic.
|
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any other provider in the harness catalogue — each of which you can point at a proxy or LLM gateway through a custom base URL. You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic.
|
||||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run.
|
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run.
|
||||||
|
|
||||||
### Run Shannon
|
### Run Shannon
|
||||||
|
|||||||
@@ -21,7 +21,15 @@ import { resolveWorkflowId } from '../session.js';
|
|||||||
import { waitForWorkflowClose } from '../temporal-client.js';
|
import { waitForWorkflowClose } from '../temporal-client.js';
|
||||||
import { stdoutIsTerminal } from '../tty.js';
|
import { stdoutIsTerminal } from '../tty.js';
|
||||||
|
|
||||||
const TERMINAL_HEADINGS = new Set(['Scan COMPLETED', 'Scan PARTIAL', 'Scan FAILED', 'Scan CANCELLED']);
|
const TERMINAL_HEADINGS = new Set([
|
||||||
|
'Scan COMPLETED',
|
||||||
|
'Scan PARTIAL',
|
||||||
|
'Scan FAILED',
|
||||||
|
'Scan CANCELLED',
|
||||||
|
'Validation COMPLETED',
|
||||||
|
'Validation FAILED',
|
||||||
|
'Validation CANCELLED',
|
||||||
|
]);
|
||||||
|
|
||||||
// The combined log resets completion on the bare `RESUMED` heading; a per-agent file carries the
|
// The combined log resets completion on the bare `RESUMED` heading; a per-agent file carries the
|
||||||
// distinct `--- RESUMED (<workflow id>) ---` boundary that WorkflowLogger.logResumeBoundary writes
|
// distinct `--- RESUMED (<workflow id>) ---` boundary that WorkflowLogger.logResumeBoundary writes
|
||||||
@@ -48,7 +56,7 @@ export class LogCompletionState {
|
|||||||
this.failureIsLastMarker = false;
|
this.failureIsLastMarker = false;
|
||||||
} else if (TERMINAL_HEADINGS.has(line)) {
|
} else if (TERMINAL_HEADINGS.has(line)) {
|
||||||
this.terminalIsLastMarker = true;
|
this.terminalIsLastMarker = true;
|
||||||
this.failureIsLastMarker = line === 'Scan FAILED';
|
this.failureIsLastMarker = line.endsWith('FAILED');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
import { type ShannonConfig, saveConfig } from '../config/writer.js';
|
import { type ShannonConfig, saveConfig } from '../config/writer.js';
|
||||||
import { CURATED_PROVIDERS, type CuratedProviderId, isCuratedProvider, type OpenAiFormat } from '../model-spec.js';
|
import { CURATED_PROVIDERS, type CuratedProviderId, isCuratedProvider } from '../model-spec.js';
|
||||||
import { displaySplash } from '../splash.js';
|
import { displaySplash } from '../splash.js';
|
||||||
import { requireInteractive } from '../tty.js';
|
import { requireInteractive } from '../tty.js';
|
||||||
import { getVersion } from '../version.js';
|
import { getVersion } from '../version.js';
|
||||||
@@ -22,39 +22,38 @@ const CUSTOM_BASE_URL = '__custom_base_url__';
|
|||||||
const OTHER_PROVIDER = '__other_provider__';
|
const OTHER_PROVIDER = '__other_provider__';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire formats reachable through the gateway route. The format picks the provider
|
* API dialects reachable through the gateway route. The dialect picks the provider
|
||||||
* that supplies the credential, and for OpenAI it also picks which of the two
|
* that supplies the credential and names the wire protocol the endpoint must speak.
|
||||||
* OpenAI APIs Shannon calls.
|
|
||||||
*/
|
*/
|
||||||
const GATEWAY_DIALECTS: readonly {
|
const GATEWAY_DIALECTS: readonly {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
provider: 'anthropic' | 'openai';
|
provider: 'anthropic' | 'openai';
|
||||||
format?: OpenAiFormat;
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{ value: 'anthropic', label: 'Anthropic Messages', provider: 'anthropic' },
|
{ value: 'anthropic', label: 'Anthropic Messages', provider: 'anthropic' },
|
||||||
{
|
{ value: 'openai', label: 'OpenAI Responses', provider: 'openai' },
|
||||||
value: 'openai-chat-completions',
|
|
||||||
label: 'OpenAI Chat Completions',
|
|
||||||
provider: 'openai',
|
|
||||||
format: 'chat-completions',
|
|
||||||
},
|
|
||||||
{ value: 'openai-responses', label: 'OpenAI Responses', provider: 'openai', format: 'responses' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Suggested models per curated provider, best-first. Free-text entry accepts any model in the provider's catalogue. */
|
/** Suggested models per curated provider, best-first. Free-text entry accepts any model in the provider's catalogue. */
|
||||||
const MODEL_SUGGESTIONS: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
const MODEL_SUGGESTIONS: Readonly<Record<CuratedProviderId, readonly string[]>> = {
|
||||||
anthropic: ['claude-sonnet-4-6', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-haiku-4-5-20251001'],
|
anthropic: [
|
||||||
openai: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.4'],
|
'claude-sonnet-5',
|
||||||
xai: ['grok-4.5'],
|
'claude-opus-5',
|
||||||
|
'claude-sonnet-4-6',
|
||||||
|
'claude-opus-4-8',
|
||||||
|
'claude-opus-4-7',
|
||||||
|
'claude-haiku-4-5-20251001',
|
||||||
|
],
|
||||||
|
openai: ['gpt-6-sol', 'gpt-5.6-sol', 'gpt-5.5', 'gpt-5.4'],
|
||||||
|
xai: ['grok-4.7'],
|
||||||
'amazon-bedrock': ['us.anthropic.claude-sonnet-4-6', 'us.anthropic.claude-opus-4-8', 'us.anthropic.claude-opus-4-7'],
|
'amazon-bedrock': ['us.anthropic.claude-sonnet-4-6', 'us.anthropic.claude-opus-4-8', 'us.anthropic.claude-opus-4-7'],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Placeholder shown in the free-text model ID prompt, per curated provider. */
|
/** Placeholder shown in the free-text model ID prompt, per curated provider. */
|
||||||
const MODEL_ID_PLACEHOLDER: Readonly<Record<CuratedProviderId, string>> = {
|
const MODEL_ID_PLACEHOLDER: Readonly<Record<CuratedProviderId, string>> = {
|
||||||
anthropic: 'claude-sonnet-4-6',
|
anthropic: 'claude-sonnet-4-6',
|
||||||
openai: 'gpt-5.6-sol',
|
openai: 'gpt-6-sol',
|
||||||
xai: 'grok-4.5',
|
xai: 'grok-4.7',
|
||||||
'amazon-bedrock': 'us.anthropic.claude-opus-4-8',
|
'amazon-bedrock': 'us.anthropic.claude-opus-4-8',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +77,11 @@ export async function setup(): Promise<void> {
|
|||||||
{ value: 'openai' as const, label: 'OpenAI', hint: 'GPT models' },
|
{ value: 'openai' as const, label: 'OpenAI', hint: 'GPT models' },
|
||||||
{ value: 'xai' as const, label: 'xAI', hint: 'Grok models' },
|
{ value: 'xai' as const, label: 'xAI', hint: 'Grok models' },
|
||||||
{ value: 'amazon-bedrock' as const, label: 'AWS Bedrock', hint: 'Claude models via AWS' },
|
{ value: 'amazon-bedrock' as const, label: 'AWS Bedrock', hint: 'Claude models via AWS' },
|
||||||
{ value: CUSTOM_BASE_URL as typeof CUSTOM_BASE_URL, label: 'Custom Base URL', hint: 'your own proxy or gateway' },
|
{
|
||||||
|
value: CUSTOM_BASE_URL as typeof CUSTOM_BASE_URL,
|
||||||
|
label: 'Custom Base URL',
|
||||||
|
hint: 'route through a proxy or LLM gateway',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: OTHER_PROVIDER as typeof OTHER_PROVIDER,
|
value: OTHER_PROVIDER as typeof OTHER_PROVIDER,
|
||||||
label: 'Other provider',
|
label: 'Other provider',
|
||||||
@@ -88,20 +91,21 @@ export async function setup(): Promise<void> {
|
|||||||
});
|
});
|
||||||
if (p.isCancel(selected)) return cancelAndExit();
|
if (p.isCancel(selected)) return cancelAndExit();
|
||||||
|
|
||||||
// 2. Credentials — and, on the gateway route, the endpoint and its dialect.
|
// 2. Credentials, and any endpoint override. A base URL overrides the endpoint
|
||||||
const { provider, config, gateway } = await setupSelection(selected);
|
// for whichever provider is chosen — the curated gateway route names it via
|
||||||
|
// the dialect, the "Other provider" route asks for it directly.
|
||||||
|
const { provider, config, baseUrl } = await setupSelection(selected);
|
||||||
|
|
||||||
// 3. The model that runs every phase.
|
// 3. The model that runs every phase.
|
||||||
const modelId = await promptModel(provider);
|
const modelId = await promptModel(provider);
|
||||||
config.core = { ...config.core, model: `${provider}:${modelId}` };
|
config.core = { ...config.core, model: `${provider}:${modelId}` };
|
||||||
if (gateway) config.core = { ...config.core, base_url: gateway.baseUrl };
|
if (baseUrl) config.core = { ...config.core, base_url: baseUrl };
|
||||||
|
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
|
|
||||||
const configPath = path.join(SHANNON_HOME, 'config.toml');
|
const configPath = path.join(SHANNON_HOME, 'config.toml');
|
||||||
const summary = [`Provider ${provider}`, `Model ${modelId}`];
|
const summary = [`Provider ${provider}`, `Model ${modelId}`];
|
||||||
if (gateway) summary.push(`Endpoint ${gateway.baseUrl}`);
|
if (baseUrl) summary.push(`Endpoint ${baseUrl}`);
|
||||||
if (gateway?.format) summary.push(`API ${gateway.format}`);
|
|
||||||
|
|
||||||
p.log.success(`Configuration saved to ${configPath}`);
|
p.log.success(`Configuration saved to ${configPath}`);
|
||||||
p.log.info(summary.join('\n'));
|
p.log.info(summary.join('\n'));
|
||||||
@@ -111,7 +115,7 @@ export async function setup(): Promise<void> {
|
|||||||
interface Selection {
|
interface Selection {
|
||||||
provider: string;
|
provider: string;
|
||||||
config: ShannonConfig;
|
config: ShannonConfig;
|
||||||
gateway?: GatewaySetup;
|
baseUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve the provider selection into a provider id and its credential config. */
|
/** Resolve the provider selection into a provider id and its credential config. */
|
||||||
@@ -120,7 +124,7 @@ async function setupSelection(
|
|||||||
): Promise<Selection> {
|
): Promise<Selection> {
|
||||||
if (selected === CUSTOM_BASE_URL) {
|
if (selected === CUSTOM_BASE_URL) {
|
||||||
const gateway = await setupGateway();
|
const gateway = await setupGateway();
|
||||||
return { provider: gateway.provider, config: gateway.config, gateway };
|
return { provider: gateway.provider, config: gateway.config, baseUrl: gateway.baseUrl };
|
||||||
}
|
}
|
||||||
if (selected === OTHER_PROVIDER) {
|
if (selected === OTHER_PROVIDER) {
|
||||||
return setupOtherProvider();
|
return setupOtherProvider();
|
||||||
@@ -144,6 +148,8 @@ async function setupProvider(provider: CuratedProviderId): Promise<ShannonConfig
|
|||||||
/**
|
/**
|
||||||
* Any pi provider Shannon does not curate. The id is free text — the worker's
|
* Any pi provider Shannon does not curate. The id is free text — the worker's
|
||||||
* preflight validates it — and the key is stored generically as SHANNON_AI_API_KEY.
|
* preflight validates it — and the key is stored generically as SHANNON_AI_API_KEY.
|
||||||
|
* An optional base URL points that provider at a proxy or LLM gateway; left blank, the
|
||||||
|
* provider's own endpoint is used.
|
||||||
*/
|
*/
|
||||||
async function setupOtherProvider(): Promise<Selection> {
|
async function setupOtherProvider(): Promise<Selection> {
|
||||||
p.log.info('Browse supported providers and models at https://pi.dev/models');
|
p.log.info('Browse supported providers and models at https://pi.dev/models');
|
||||||
@@ -159,7 +165,13 @@ async function setupOtherProvider(): Promise<Selection> {
|
|||||||
if (p.isCancel(provider)) return cancelAndExit();
|
if (p.isCancel(provider)) return cancelAndExit();
|
||||||
|
|
||||||
const apiKey = await promptSecret('Enter the API key');
|
const apiKey = await promptSecret('Enter the API key');
|
||||||
return { provider: provider.trim(), config: { provider: { api_key: apiKey } } };
|
const baseUrl = await promptOptionalBaseUrl();
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: provider.trim(),
|
||||||
|
config: { provider: { api_key: apiKey } },
|
||||||
|
...(baseUrl && { baseUrl }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Provider Setup Flows ===
|
// === Provider Setup Flows ===
|
||||||
@@ -200,11 +212,10 @@ interface GatewaySetup {
|
|||||||
provider: CuratedProviderId;
|
provider: CuratedProviderId;
|
||||||
config: ShannonConfig;
|
config: ShannonConfig;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
format?: OpenAiFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gateway route: the endpoint decides where requests go, but the format still
|
* Gateway route: the endpoint decides where requests go, but the dialect still
|
||||||
* picks a real provider, because that is what supplies the credential and the
|
* picks a real provider, because that is what supplies the credential and the
|
||||||
* wire protocol.
|
* wire protocol.
|
||||||
*/
|
*/
|
||||||
@@ -236,11 +247,9 @@ async function setupGateway(): Promise<GatewaySetup> {
|
|||||||
|
|
||||||
const authToken = await promptSecret('Enter the auth token for the endpoint');
|
const authToken = await promptSecret('Enter the auth token for the endpoint');
|
||||||
const config: ShannonConfig =
|
const config: ShannonConfig =
|
||||||
provider === 'anthropic'
|
provider === 'anthropic' ? { anthropic: { api_key: authToken } } : { openai: { api_key: authToken } };
|
||||||
? { anthropic: { api_key: authToken } }
|
|
||||||
: { openai: { api_key: authToken, ...(dialect.format && { format: dialect.format }) } };
|
|
||||||
|
|
||||||
return { provider, config, baseUrl, ...(dialect.format && { format: dialect.format }) };
|
return { provider, config, baseUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Model Selection ===
|
// === Model Selection ===
|
||||||
@@ -308,6 +317,31 @@ async function promptModelId(provider: string, placeholder?: string): Promise<st
|
|||||||
|
|
||||||
// === Helpers ===
|
// === Helpers ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional endpoint override. Empty input means the provider's default endpoint;
|
||||||
|
* any value must be a valid URL.
|
||||||
|
*/
|
||||||
|
async function promptOptionalBaseUrl(): Promise<string | undefined> {
|
||||||
|
const baseUrl = await p.text({
|
||||||
|
message: 'Custom base URL (optional, leave blank for the provider default)',
|
||||||
|
placeholder: 'https://llm-gateway.example.com',
|
||||||
|
validate: (value) => {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
try {
|
||||||
|
new URL(trimmed);
|
||||||
|
} catch {
|
||||||
|
return 'Must be a valid URL';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (p.isCancel(baseUrl)) return cancelAndExit();
|
||||||
|
|
||||||
|
const trimmed = baseUrl?.trim();
|
||||||
|
return trimmed ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function promptSecret(message: string): Promise<string> {
|
async function promptSecret(message: string): Promise<string> {
|
||||||
const value = await p.password({
|
const value = await p.password({
|
||||||
message,
|
message,
|
||||||
|
|||||||
+249
-13
@@ -22,14 +22,21 @@ import {
|
|||||||
FINAL_REPORT_PDF_FILENAME,
|
FINAL_REPORT_PDF_FILENAME,
|
||||||
INTERNAL_DIR,
|
INTERNAL_DIR,
|
||||||
resolveConfig,
|
resolveConfig,
|
||||||
|
resolveModelsConfig,
|
||||||
resolveRepo,
|
resolveRepo,
|
||||||
resolveRunFile,
|
resolveRunFile,
|
||||||
|
STARTUP_ERROR_FILENAME,
|
||||||
} from '../paths.js';
|
} from '../paths.js';
|
||||||
import { clearPendingWorkflowIdentity, writePendingWorkflowIdentity } from '../pending-workflow.js';
|
import { clearPendingWorkflowIdentity, writePendingWorkflowIdentity } from '../pending-workflow.js';
|
||||||
import { indentFailureSegments } from '../scan/failure.js';
|
import { indentFailureSegments, parseFailureSegments } from '../scan/failure.js';
|
||||||
import { resolveWorkflowId } from '../session.js';
|
import { resolveWorkflowId } from '../session.js';
|
||||||
import { displayPlainBanner, displaySplash } from '../splash.js';
|
import { displayPlainBanner, displaySplash } from '../splash.js';
|
||||||
import { getTerminalOutcome } from '../temporal-client.js';
|
import {
|
||||||
|
describeWorkflowLifecycle,
|
||||||
|
getTerminalOutcome,
|
||||||
|
queryProgress,
|
||||||
|
runningActivityTypes,
|
||||||
|
} from '../temporal-client.js';
|
||||||
import { stdoutIsTerminal } from '../tty.js';
|
import { stdoutIsTerminal } from '../tty.js';
|
||||||
import { tailUntilComplete } from './logs.js';
|
import { tailUntilComplete } from './logs.js';
|
||||||
|
|
||||||
@@ -37,11 +44,14 @@ export interface StartArgs {
|
|||||||
url: string;
|
url: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
|
modelsConfig?: string;
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
pipelineTesting: boolean;
|
pipelineTesting: boolean;
|
||||||
keepContainer: boolean;
|
keepContainer: boolean;
|
||||||
follow: boolean;
|
follow: boolean;
|
||||||
|
authOnly: boolean;
|
||||||
|
validateModel: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +67,10 @@ const FIXED_CLASSES = ['injection', 'xss', 'auth', 'authz', 'ssrf'] as const;
|
|||||||
interface LaunchState {
|
interface LaunchState {
|
||||||
readonly schema_version: typeof LAUNCH_STATE_SCHEMA_VERSION;
|
readonly schema_version: typeof LAUNCH_STATE_SCHEMA_VERSION;
|
||||||
readonly customer_output_path?: string;
|
readonly customer_output_path?: string;
|
||||||
|
/** True when the workspace was created by an auth-validation run; such a workspace is not a scan. */
|
||||||
|
readonly auth_only?: boolean;
|
||||||
|
/** True when the workspace was created by a model-validation run; such a workspace is not a scan. */
|
||||||
|
readonly model_only?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceLaunchDecision {
|
export interface WorkspaceLaunchDecision {
|
||||||
@@ -121,17 +135,31 @@ function readLaunchState(filePath: string): LaunchState {
|
|||||||
if (!isRecord(value)) fail(NEWER_RELEASE_MESSAGE);
|
if (!isRecord(value)) fail(NEWER_RELEASE_MESSAGE);
|
||||||
// Unknown keys mean a newer release wrote this workspace; refuse rather than half-read it.
|
// Unknown keys mean a newer release wrote this workspace; refuse rather than half-read it.
|
||||||
const keys = Object.keys(value).sort();
|
const keys = Object.keys(value).sort();
|
||||||
const keysAreValid = keys.every((key) => key === 'customer_output_path' || key === 'schema_version');
|
const keysAreValid = keys.every(
|
||||||
|
(key) => key === 'auth_only' || key === 'model_only' || key === 'customer_output_path' || key === 'schema_version',
|
||||||
|
);
|
||||||
const customerPath = value.customer_output_path;
|
const customerPath = value.customer_output_path;
|
||||||
const pathIsValid =
|
const pathIsValid =
|
||||||
customerPath === undefined ||
|
customerPath === undefined ||
|
||||||
(typeof customerPath === 'string' && path.isAbsolute(customerPath) && path.resolve(customerPath) === customerPath);
|
(typeof customerPath === 'string' && path.isAbsolute(customerPath) && path.resolve(customerPath) === customerPath);
|
||||||
if (value.schema_version !== LAUNCH_STATE_SCHEMA_VERSION || !keysAreValid || !pathIsValid) {
|
const authOnly = value.auth_only;
|
||||||
|
const authOnlyIsValid = authOnly === undefined || typeof authOnly === 'boolean';
|
||||||
|
const modelOnly = value.model_only;
|
||||||
|
const modelOnlyIsValid = modelOnly === undefined || typeof modelOnly === 'boolean';
|
||||||
|
if (
|
||||||
|
value.schema_version !== LAUNCH_STATE_SCHEMA_VERSION ||
|
||||||
|
!keysAreValid ||
|
||||||
|
!pathIsValid ||
|
||||||
|
!authOnlyIsValid ||
|
||||||
|
!modelOnlyIsValid
|
||||||
|
) {
|
||||||
fail(NEWER_RELEASE_MESSAGE);
|
fail(NEWER_RELEASE_MESSAGE);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
schema_version: LAUNCH_STATE_SCHEMA_VERSION,
|
schema_version: LAUNCH_STATE_SCHEMA_VERSION,
|
||||||
...(typeof customerPath === 'string' && { customer_output_path: customerPath }),
|
...(typeof customerPath === 'string' && { customer_output_path: customerPath }),
|
||||||
|
...(authOnly === true && { auth_only: true }),
|
||||||
|
...(modelOnly === true && { model_only: true }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +174,8 @@ export function classifyWorkspaceLaunch(
|
|||||||
workspacePath: string,
|
workspacePath: string,
|
||||||
expectedUrl: string,
|
expectedUrl: string,
|
||||||
requestedOutputDir: string | undefined,
|
requestedOutputDir: string | undefined,
|
||||||
|
requestedAuthOnly: boolean,
|
||||||
|
requestedModelOnly: boolean,
|
||||||
): WorkspaceLaunchDecision {
|
): WorkspaceLaunchDecision {
|
||||||
const sessionPath = resolveRunFile(workspacePath, 'session.json');
|
const sessionPath = resolveRunFile(workspacePath, 'session.json');
|
||||||
const sessionExists = fs.existsSync(sessionPath);
|
const sessionExists = fs.existsSync(sessionPath);
|
||||||
@@ -160,6 +190,16 @@ export function classifyWorkspaceLaunch(
|
|||||||
|
|
||||||
const launchPath = path.join(workspacePath, INTERNAL_DIR, LAUNCH_STATE_FILENAME);
|
const launchPath = path.join(workspacePath, INTERNAL_DIR, LAUNCH_STATE_FILENAME);
|
||||||
const launch = readLaunchState(launchPath);
|
const launch = readLaunchState(launchPath);
|
||||||
|
if (launch.auth_only && !requestedAuthOnly) {
|
||||||
|
fail(
|
||||||
|
'This workspace was created to validate authentication only, so it cannot be run as a scan. Start a new scan with a different -w name.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (launch.model_only && !requestedModelOnly) {
|
||||||
|
fail(
|
||||||
|
'This workspace was created to validate the AI model only, so it cannot be run as a scan. Start a new scan with a different -w name.',
|
||||||
|
);
|
||||||
|
}
|
||||||
const session = readJsonFile(sessionPath);
|
const session = readJsonFile(sessionPath);
|
||||||
if (!isRecord(session) || !isRecord(session.session) || session.session.webUrl !== expectedUrl) {
|
if (!isRecord(session) || !isRecord(session.session) || session.session.webUrl !== expectedUrl) {
|
||||||
fail(
|
fail(
|
||||||
@@ -187,12 +227,19 @@ export function classifyWorkspaceLaunch(
|
|||||||
* host crash. Callers invoke this only for a fresh workspace; an existing launch.json is
|
* host crash. Callers invoke this only for a fresh workspace; an existing launch.json is
|
||||||
* the resume contract and must never be replaced.
|
* the resume contract and must never be replaced.
|
||||||
*/
|
*/
|
||||||
export function writeLaunchStateAtomically(internalPath: string, outputDir: string | undefined): void {
|
export function writeLaunchStateAtomically(
|
||||||
|
internalPath: string,
|
||||||
|
outputDir: string | undefined,
|
||||||
|
authOnly: boolean,
|
||||||
|
modelOnly: boolean,
|
||||||
|
): void {
|
||||||
const finalPath = path.join(internalPath, LAUNCH_STATE_FILENAME);
|
const finalPath = path.join(internalPath, LAUNCH_STATE_FILENAME);
|
||||||
const temporaryPath = path.join(internalPath, `${LAUNCH_STATE_FILENAME}.tmp-${process.pid}-${randomSuffix()}`);
|
const temporaryPath = path.join(internalPath, `${LAUNCH_STATE_FILENAME}.tmp-${process.pid}-${randomSuffix()}`);
|
||||||
const launchState: LaunchState = {
|
const launchState: LaunchState = {
|
||||||
schema_version: LAUNCH_STATE_SCHEMA_VERSION,
|
schema_version: LAUNCH_STATE_SCHEMA_VERSION,
|
||||||
...(outputDir !== undefined && { customer_output_path: outputDir }),
|
...(outputDir !== undefined && { customer_output_path: outputDir }),
|
||||||
|
...(authOnly && { auth_only: true }),
|
||||||
|
...(modelOnly && { model_only: true }),
|
||||||
};
|
};
|
||||||
const descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
|
const descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
|
||||||
try {
|
try {
|
||||||
@@ -222,6 +269,10 @@ export function createWorkflowId(workspace: string, isResume: boolean, timestamp
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function start(args: StartArgs): Promise<void> {
|
export async function start(args: StartArgs): Promise<void> {
|
||||||
|
// Validation-only runs are short and have no report to come back for, so they always stream to the end.
|
||||||
|
const validationOnly = args.authOnly || args.validateModel;
|
||||||
|
if (validationOnly) args.follow = true;
|
||||||
|
|
||||||
// 1. Resolve non-mutating inputs and classify the workspace before changing it.
|
// 1. Resolve non-mutating inputs and classify the workspace before changing it.
|
||||||
initHome();
|
initHome();
|
||||||
loadEnv();
|
loadEnv();
|
||||||
@@ -231,12 +282,44 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
}
|
}
|
||||||
const repo = resolveRepo(args.repo);
|
const repo = resolveRepo(args.repo);
|
||||||
const config = args.config ? resolveConfig(args.config) : undefined;
|
const config = args.config ? resolveConfig(args.config) : undefined;
|
||||||
|
const modelsConfig = args.modelsConfig ? resolveModelsConfig(args.modelsConfig) : undefined;
|
||||||
const workspacesDir = getWorkspacesDir();
|
const workspacesDir = getWorkspacesDir();
|
||||||
const workspace =
|
const workspace =
|
||||||
args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`;
|
args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`;
|
||||||
const workspacePath = path.join(workspacesDir, workspace);
|
const workspacePath = path.join(workspacesDir, workspace);
|
||||||
const requestedOutputDir = args.output ? path.resolve(expandHome(args.output)) : undefined;
|
const requestedOutputDir = args.output ? path.resolve(expandHome(args.output)) : undefined;
|
||||||
const launchDecision = classifyWorkspaceLaunch(workspacePath, args.url, requestedOutputDir);
|
const launchDecision = classifyWorkspaceLaunch(
|
||||||
|
workspacePath,
|
||||||
|
args.url,
|
||||||
|
requestedOutputDir,
|
||||||
|
args.authOnly,
|
||||||
|
args.validateModel,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validation-only runs write no resumable state, so they always run fresh; reusing a workspace would resume it.
|
||||||
|
if (validationOnly && launchDecision.isResume) {
|
||||||
|
const what = args.authOnly ? 'An auth-validation run' : 'A model-validation run';
|
||||||
|
fail(`${what} needs a fresh workspace. Omit -w to auto-name one, or choose a -w name that is not in use.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User-facing status wording. Auth-only and model-only are both "validation" runs, but each
|
||||||
|
// names what it validated. A validation run *is* the checks, so a failure means it ran and
|
||||||
|
// failed, not that it could not start. A plain scan keeps its original phrasing.
|
||||||
|
let startingLabel = 'Starting scan';
|
||||||
|
let waitingLabel = 'Waiting for the scan to start';
|
||||||
|
let couldNotStartLabel = 'The scan could not start';
|
||||||
|
let startedLabel = `Scan started — ${workspace}`;
|
||||||
|
if (args.authOnly) {
|
||||||
|
startingLabel = 'Starting authentication validation';
|
||||||
|
waitingLabel = 'Waiting for authentication validation to start';
|
||||||
|
couldNotStartLabel = 'Authentication validation failed';
|
||||||
|
startedLabel = `Validating authentication — ${workspace}`;
|
||||||
|
} else if (args.validateModel) {
|
||||||
|
startingLabel = 'Starting model validation';
|
||||||
|
waitingLabel = 'Waiting for model validation to start';
|
||||||
|
couldNotStartLabel = 'Model validation failed';
|
||||||
|
startedLabel = `Validating model — ${workspace}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Inputs are valid; identify the run before initializing shared infrastructure.
|
// 2. Inputs are valid; identify the run before initializing shared infrastructure.
|
||||||
const bannerVersion = isLocal() ? undefined : args.version;
|
const bannerVersion = isLocal() ? undefined : args.version;
|
||||||
@@ -250,7 +333,7 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
ensureDocker();
|
ensureDocker();
|
||||||
ensureImage(args.version);
|
ensureImage(args.version);
|
||||||
const spinner = p.spinner();
|
const spinner = p.spinner();
|
||||||
spinner.start('Starting scan');
|
spinner.start(startingLabel);
|
||||||
await ensureInfra(spinner);
|
await ensureInfra(spinner);
|
||||||
|
|
||||||
// 3. Generate the invocation identity.
|
// 3. Generate the invocation identity.
|
||||||
@@ -273,7 +356,7 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
fs.chmodSync(dirPath, 0o777);
|
fs.chmodSync(dirPath, 0o777);
|
||||||
}
|
}
|
||||||
if (!launchDecision.isResume) {
|
if (!launchDecision.isResume) {
|
||||||
writeLaunchStateAtomically(internalPath, launchDecision.outputDir);
|
writeLaunchStateAtomically(internalPath, launchDecision.outputDir, args.authOnly, args.validateModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Pre-create overlay mount points (:ro mounts cannot create them).
|
// 5. Pre-create overlay mount points (:ro mounts cannot create them).
|
||||||
@@ -311,6 +394,10 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear a stale startup-error from a previous launch so the poll reacts only to this worker's.
|
||||||
|
const startupErrorPath = path.join(internalPath, STARTUP_ERROR_FILENAME);
|
||||||
|
fs.rmSync(startupErrorPath, { force: true });
|
||||||
|
|
||||||
// 9. Spawn the worker container.
|
// 9. Spawn the worker container.
|
||||||
const proc = spawnWorker({
|
const proc = spawnWorker({
|
||||||
version: args.version,
|
version: args.version,
|
||||||
@@ -322,11 +409,14 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
containerName,
|
containerName,
|
||||||
envFlags: buildEnvFlags(),
|
envFlags: buildEnvFlags(),
|
||||||
...(config && { config }),
|
...(config && { config }),
|
||||||
|
...(modelsConfig && { modelsConfig }),
|
||||||
...(promptsDir && { promptsDir }),
|
...(promptsDir && { promptsDir }),
|
||||||
...(outputDir && { outputDir }),
|
...(outputDir && { outputDir }),
|
||||||
workspace,
|
workspace,
|
||||||
...(args.pipelineTesting && { pipelineTesting: true }),
|
...(args.pipelineTesting && { pipelineTesting: true }),
|
||||||
...(args.keepContainer && { keepContainer: true }),
|
...(args.keepContainer && { keepContainer: true }),
|
||||||
|
...(args.authOnly && { authOnly: true }),
|
||||||
|
...(args.validateModel && { validateModel: true }),
|
||||||
...(shouldUsePiAuth() && { piAuthHostPath: resolveHostPiAuthPath() }),
|
...(shouldUsePiAuth() && { piAuthHostPath: resolveHostPiAuthPath() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,8 +467,18 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Poll for the workflow to register in session.json; the spinner resolves once it does.
|
// Poll for the workflow to register in session.json; the spinner resolves once it does.
|
||||||
spinner.message('Waiting for the scan to start');
|
spinner.message(waitingLabel);
|
||||||
for (let attempts = 0; attempts < 60; attempts++) {
|
for (let attempts = 0; attempts < 60; attempts++) {
|
||||||
|
// A pre-workflow failure leaves its reason here (nothing reached Temporal); surface it
|
||||||
|
// rather than polling out to a generic timeout.
|
||||||
|
const startupError = readStartupError(startupErrorPath);
|
||||||
|
if (startupError) {
|
||||||
|
cleaned = true; // The worker already exited; nothing to stop.
|
||||||
|
spinner.error('The scan could not start');
|
||||||
|
printStartupError(startupError);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = JSON.parse(fs.readFileSync(sessionJson, 'utf-8'));
|
const session = JSON.parse(fs.readFileSync(sessionJson, 'utf-8'));
|
||||||
const resumeAttempts: { workflowId: string }[] = session.session?.resumeAttempts ?? [];
|
const resumeAttempts: { workflowId: string }[] = session.session?.resumeAttempts ?? [];
|
||||||
@@ -395,10 +495,29 @@ export async function start(args: StartArgs): Promise<void> {
|
|||||||
} catch {
|
} catch {
|
||||||
warn(`Scan ${workspace} started, but its launch record could not be removed.`);
|
warn(`Scan ${workspace} started, but its launch record could not be removed.`);
|
||||||
}
|
}
|
||||||
spinner.stop(`Scan started — ${workspace}`);
|
|
||||||
|
// Hold until startup clears, so an unreachable target or bad credential is reported here
|
||||||
|
// rather than after "Scan started".
|
||||||
|
spinner.message(PREFLIGHT_LABEL);
|
||||||
|
const spec = resolveModelSpec();
|
||||||
|
const providerId = typeof spec === 'string' ? '' : spec.providerId;
|
||||||
|
// Cyber-access verification only runs for OpenAI/Anthropic; when following, the tailed log shows the login.
|
||||||
|
// Mirrors CYBER_GATED_PROVIDERS in the worker (apps/worker/src/services/cyber-access-verification.ts).
|
||||||
|
const showCyberAccess = providerId === 'anthropic' || providerId === 'openai' || providerId === 'openai-codex';
|
||||||
|
const outcome = await awaitStartupOutcome(workflowId, (label) => spinner.message(label), {
|
||||||
|
showCyberAccess,
|
||||||
|
showAppLogin: !args.follow,
|
||||||
|
});
|
||||||
|
if (outcome.kind === 'failed') {
|
||||||
|
spinner.error(couldNotStartLabel);
|
||||||
|
printScanStartFailure(outcome.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.stop(startedLabel);
|
||||||
printInfo(args, workspace, repo.hostPath, workspacesDir);
|
printInfo(args, workspace, repo.hostPath, workspacesDir);
|
||||||
if (args.follow) {
|
if (args.follow) {
|
||||||
await followScan(workspace, workspacesDir);
|
await followScan(workspace, workspacesDir, validationOnly);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -438,6 +557,116 @@ export function classifyStartupTimeout(sessionJsonPath: string): 'unregistered'
|
|||||||
return 'scan-running';
|
return 'scan-running';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A pre-workflow failure the worker persisted; mirrors StartupErrorRecord in the worker. */
|
||||||
|
interface StartupError {
|
||||||
|
phase?: string;
|
||||||
|
code?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the worker's pre-workflow failure record, if it wrote one. Undefined until the file exists
|
||||||
|
* and parses, so a partial write is simply re-read on the next poll rather than treated as failure.
|
||||||
|
*/
|
||||||
|
function readStartupError(startupErrorPath: string): StartupError | undefined {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = fs.readFileSync(startupErrorPath, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return isRecord(parsed) ? parsed : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Outcome of waiting for in-workflow startup (preflight + auth validation) to clear. */
|
||||||
|
type PreflightOutcome = { kind: 'passed' } | { kind: 'failed'; message: string } | { kind: 'unconfirmed' };
|
||||||
|
|
||||||
|
const PREFLIGHT_LABEL = 'Running preflight checks (LLM credentials, target URL)';
|
||||||
|
const CYBER_ACCESS_LABEL = 'Checking cyber access';
|
||||||
|
const APP_LOGIN_LABEL = 'Verifying app login with provided credentials';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive the startup spinner until the pentest begins, naming the cyber-access verification and the app
|
||||||
|
* login while their activity runs. Labels only advance, so a gap between them holds the last step
|
||||||
|
* rather than reverting to the generic line. Passed once the phase moves past preflight/auth (or
|
||||||
|
* the scan closed ok), failed on a terminal error, unconfirmed if a query outage outlasts the bound.
|
||||||
|
*/
|
||||||
|
async function awaitStartupOutcome(
|
||||||
|
workflowId: string,
|
||||||
|
onLabel: (label: string) => void,
|
||||||
|
opts: { showCyberAccess: boolean; showAppLogin: boolean },
|
||||||
|
): Promise<PreflightOutcome> {
|
||||||
|
// Wait through auth-validation only when naming the login step; otherwise stop once it begins.
|
||||||
|
const startupPhases = opts.showAppLogin ? new Set(['preflight', 'auth-validation']) : new Set(['preflight']);
|
||||||
|
let rank = 0;
|
||||||
|
let label = PREFLIGHT_LABEL;
|
||||||
|
for (let attempts = 0; attempts < 80; attempts++) {
|
||||||
|
try {
|
||||||
|
const lifecycle = await describeWorkflowLifecycle(workflowId);
|
||||||
|
if (lifecycle.kind === 'terminal') {
|
||||||
|
const outcome = await getTerminalOutcome(workflowId);
|
||||||
|
return outcome.kind === 'failed' ? { kind: 'failed', message: outcome.message } : { kind: 'passed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const running = await runningActivityTypes(workflowId);
|
||||||
|
if (opts.showCyberAccess && rank < 1 && running.includes('runCyberAccessVerification')) {
|
||||||
|
rank = 1;
|
||||||
|
label = CYBER_ACCESS_LABEL;
|
||||||
|
}
|
||||||
|
if (opts.showAppLogin && rank < 2 && running.includes('runAuthenticationValidation')) {
|
||||||
|
rank = 2;
|
||||||
|
label = APP_LOGIN_LABEL;
|
||||||
|
}
|
||||||
|
onLabel(label);
|
||||||
|
|
||||||
|
const progress = await queryProgress(workflowId);
|
||||||
|
if (progress && progress.currentPhase !== null && !startupPhases.has(progress.currentPhase)) {
|
||||||
|
return { kind: 'passed' };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Transient query failure; keep waiting within the bound.
|
||||||
|
}
|
||||||
|
await sleep(1500);
|
||||||
|
}
|
||||||
|
return { kind: 'unconfirmed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Print a preflight failure: context line, then the indented reason and hint, then the reference code. */
|
||||||
|
function printScanStartFailure(message: string): void {
|
||||||
|
const segments = parseFailureSegments(message);
|
||||||
|
const phaseContext = segments.shift() ?? 'The scan failed';
|
||||||
|
const last = segments[segments.length - 1];
|
||||||
|
const reference = last?.startsWith('Reference code:') ? segments.pop() : undefined;
|
||||||
|
|
||||||
|
const lines = [` ${phaseContext}`, '', ...segments.map((segment) => ` ${segment}`)];
|
||||||
|
if (reference) {
|
||||||
|
lines.push('', ` ${reference}`);
|
||||||
|
}
|
||||||
|
console.error(`\n${lines.join('\n')}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Print the worker's persisted startup-failure reason, with its reference code when present. */
|
||||||
|
function printStartupError(startupError: StartupError): void {
|
||||||
|
const message =
|
||||||
|
typeof startupError.message === 'string' && startupError.message.trim()
|
||||||
|
? startupError.message.trim()
|
||||||
|
: 'The worker rejected the scan before it could start. Check the configuration file passed with -c.';
|
||||||
|
console.error('');
|
||||||
|
for (const line of message.split('\n')) {
|
||||||
|
console.error(line.length > 0 ? ` ${line}` : '');
|
||||||
|
}
|
||||||
|
if (typeof startupError.code === 'string' && startupError.code.trim()) {
|
||||||
|
console.error('');
|
||||||
|
console.error(` Reference code: ${startupError.code.trim()}`);
|
||||||
|
}
|
||||||
|
console.error('');
|
||||||
|
}
|
||||||
|
|
||||||
/** Point the operator at a scan that is running but whose startup this CLI could not confirm. */
|
/** Point the operator at a scan that is running but whose startup this CLI could not confirm. */
|
||||||
function printUnconfirmedScanHint(workspace: string, taskQueue: string, containerName: string): void {
|
function printUnconfirmedScanHint(workspace: string, taskQueue: string, containerName: string): void {
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -460,7 +689,7 @@ function printUnconfirmedScanHint(workspace: string, taskQueue: string, containe
|
|||||||
* That tracks whether the pipeline ran, not whether vulnerabilities were found. On failure the
|
* That tracks whether the pipeline ran, not whether vulnerabilities were found. On failure the
|
||||||
* root-cause message is printed so a red CI build says why.
|
* root-cause message is printed so a red CI build says why.
|
||||||
*/
|
*/
|
||||||
async function followScan(workspace: string, workspacesDir: string): Promise<never> {
|
async function followScan(workspace: string, workspacesDir: string, validationOnly = false): Promise<never> {
|
||||||
const logFile = resolveRunFile(path.join(workspacesDir, workspace), 'workflow.log');
|
const logFile = resolveRunFile(path.join(workspacesDir, workspace), 'workflow.log');
|
||||||
const workflowId = resolveWorkflowId(workspace);
|
const workflowId = resolveWorkflowId(workspace);
|
||||||
|
|
||||||
@@ -471,7 +700,8 @@ async function followScan(workspace: string, workspacesDir: string): Promise<nev
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stdoutIsTerminal()) {
|
if (stdoutIsTerminal()) {
|
||||||
console.error('\n Following scan log (Ctrl-C to stop watching):\n');
|
const what = validationOnly ? 'validation' : 'scan';
|
||||||
|
console.error(`\n Following ${what} log (Ctrl-C to stop watching):\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let temporalUnreachable = false;
|
let temporalUnreachable = false;
|
||||||
@@ -530,6 +760,10 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
|||||||
if (args.config) {
|
if (args.config) {
|
||||||
console.log(` Config: ${interactive ? path.resolve(args.config) : path.basename(args.config)}`);
|
console.log(` Config: ${interactive ? path.resolve(args.config) : path.basename(args.config)}`);
|
||||||
}
|
}
|
||||||
|
if (args.modelsConfig) {
|
||||||
|
const shown = interactive ? path.resolve(args.modelsConfig) : path.basename(args.modelsConfig);
|
||||||
|
console.log(` Models: ${shown}`);
|
||||||
|
}
|
||||||
if (args.pipelineTesting) {
|
if (args.pipelineTesting) {
|
||||||
console.log(' Mode: Pipeline Testing');
|
console.log(' Mode: Pipeline Testing');
|
||||||
}
|
}
|
||||||
@@ -555,6 +789,7 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
|||||||
console.log(` Progress: ${prefix} status ${workspace}`);
|
console.log(` Progress: ${prefix} status ${workspace}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!args.authOnly && !args.validateModel) {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(' Report (when the scan finishes):');
|
console.log(' Report (when the scan finishes):');
|
||||||
console.log(` ${reportDir}${path.sep}`);
|
console.log(` ${reportDir}${path.sep}`);
|
||||||
@@ -562,3 +797,4 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
|||||||
console.log(` ${FINAL_REPORT_MD_FILENAME}`);
|
console.log(` ${FINAL_REPORT_MD_FILENAME}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -40,9 +40,8 @@ const CONFIG_MAP: readonly ConfigMapping[] = [
|
|||||||
{ env: 'ANTHROPIC_API_KEY', toml: 'anthropic.api_key', type: 'string' },
|
{ env: 'ANTHROPIC_API_KEY', toml: 'anthropic.api_key', type: 'string' },
|
||||||
{ env: 'CLAUDE_CODE_OAUTH_TOKEN', toml: 'anthropic.oauth_token', type: 'string' },
|
{ env: 'CLAUDE_CODE_OAUTH_TOKEN', toml: 'anthropic.oauth_token', type: 'string' },
|
||||||
|
|
||||||
// OpenAI — format picks the wire API a gateway serves
|
// OpenAI
|
||||||
{ env: 'OPENAI_API_KEY', toml: 'openai.api_key', type: 'string' },
|
{ env: 'OPENAI_API_KEY', toml: 'openai.api_key', type: 'string' },
|
||||||
{ env: 'SHANNON_AI_OPENAI_FORMAT', toml: 'openai.format', type: 'string' },
|
|
||||||
|
|
||||||
// xAI
|
// xAI
|
||||||
{ env: 'XAI_API_KEY', toml: 'xai.api_key', type: 'string' },
|
{ env: 'XAI_API_KEY', toml: 'xai.api_key', type: 'string' },
|
||||||
@@ -97,8 +96,6 @@ function loadTOML(): TOMLConfig | null {
|
|||||||
if (!fs.existsSync(configPath)) return null;
|
if (!fs.existsSync(configPath)) return null;
|
||||||
|
|
||||||
// Config contains secrets — refuse to read if group or others have any access.
|
// Config contains secrets — refuse to read if group or others have any access.
|
||||||
// Skip on Windows where POSIX permissions are not supported.
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
const mode = fs.statSync(configPath).mode;
|
const mode = fs.statSync(configPath).mode;
|
||||||
if (mode & 0o077) {
|
if (mode & 0o077) {
|
||||||
const actual = (mode & 0o777).toString(8).padStart(3, '0');
|
const actual = (mode & 0o777).toString(8).padStart(3, '0');
|
||||||
@@ -106,7 +103,6 @@ function loadTOML(): TOMLConfig | null {
|
|||||||
`Your config file is readable by other users on this machine (${actual}). Lock it down: chmod 600 ${configPath}`,
|
`Your config file is readable by other users on this machine (${actual}). Lock it down: chmod 600 ${configPath}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(configPath, 'utf-8');
|
const content = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getConfigFile } from '../home.js';
|
|||||||
export interface ShannonConfig {
|
export interface ShannonConfig {
|
||||||
core?: { model?: string; base_url?: string };
|
core?: { model?: string; base_url?: string };
|
||||||
anthropic?: { api_key?: string; oauth_token?: string };
|
anthropic?: { api_key?: string; oauth_token?: string };
|
||||||
openai?: { api_key?: string; format?: string };
|
openai?: { api_key?: string };
|
||||||
xai?: { api_key?: string };
|
xai?: { api_key?: string };
|
||||||
bedrock?: { region?: string; token?: string };
|
bedrock?: { region?: string; token?: string };
|
||||||
/** Generic credential for any provider Shannon does not curate. Maps to SHANNON_AI_API_KEY. */
|
/** Generic credential for any provider Shannon does not curate. Maps to SHANNON_AI_API_KEY. */
|
||||||
|
|||||||
+15
-3
@@ -360,7 +360,6 @@ function shouldSkipHostsName(name: string, hostname: string): boolean {
|
|||||||
*/
|
*/
|
||||||
function forwardEtcHostsFlags(): string[] {
|
function forwardEtcHostsFlags(): string[] {
|
||||||
if (!envBool('SHANNON_FORWARD_HOSTS', true)) return [];
|
if (!envBool('SHANNON_FORWARD_HOSTS', true)) return [];
|
||||||
if (os.platform() === 'win32') return [];
|
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try {
|
try {
|
||||||
@@ -407,11 +406,14 @@ export interface WorkerOptions {
|
|||||||
containerName: string;
|
containerName: string;
|
||||||
envFlags: string[];
|
envFlags: string[];
|
||||||
config?: { hostPath: string; containerPath: string };
|
config?: { hostPath: string; containerPath: string };
|
||||||
|
modelsConfig?: { hostPath: string; containerPath: string };
|
||||||
promptsDir?: string;
|
promptsDir?: string;
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
pipelineTesting?: boolean;
|
pipelineTesting?: boolean;
|
||||||
keepContainer?: boolean;
|
keepContainer?: boolean;
|
||||||
|
authOnly?: boolean;
|
||||||
|
validateModel?: boolean;
|
||||||
piAuthHostPath?: string;
|
piAuthHostPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +471,12 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
|||||||
args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
|
args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pi model config. The mount is the only signal the worker gets: it detects the file at
|
||||||
|
// this fixed path, so nothing about --models-config travels through the environment.
|
||||||
|
if (opts.modelsConfig) {
|
||||||
|
args.push('-v', `${opts.modelsConfig.hostPath}:${opts.modelsConfig.containerPath}:ro`);
|
||||||
|
}
|
||||||
|
|
||||||
// Customer-copy destination. The workflow surfaces only final report artifacts here.
|
// Customer-copy destination. The workflow surfaces only final report artifacts here.
|
||||||
if (opts.outputDir) {
|
if (opts.outputDir) {
|
||||||
args.push('-v', `${opts.outputDir}:/app/output`);
|
args.push('-v', `${opts.outputDir}:/app/output`);
|
||||||
@@ -505,13 +513,17 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
|||||||
if (opts.pipelineTesting) {
|
if (opts.pipelineTesting) {
|
||||||
args.push('--pipeline-testing');
|
args.push('--pipeline-testing');
|
||||||
}
|
}
|
||||||
|
if (opts.authOnly) {
|
||||||
|
args.push('--validate-auth');
|
||||||
|
}
|
||||||
|
if (opts.validateModel) {
|
||||||
|
args.push('--validate-model');
|
||||||
|
}
|
||||||
|
|
||||||
// Inherit stderr so `docker run` daemon errors surface to the user;
|
// Inherit stderr so `docker run` daemon errors surface to the user;
|
||||||
// ignore stdin/stdout (the container ID is noise).
|
// ignore stdin/stdout (the container ID is noise).
|
||||||
return spawn('docker', args, {
|
return spawn('docker', args, {
|
||||||
stdio: ['ignore', 'ignore', 'inherit'],
|
stdio: ['ignore', 'ignore', 'inherit'],
|
||||||
// Prevent MSYS/Git Bash from converting Unix paths on Windows
|
|
||||||
...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
const COMMON_FORWARD_VARS = [
|
const COMMON_FORWARD_VARS = [
|
||||||
'SHANNON_AI_MODEL',
|
'SHANNON_AI_MODEL',
|
||||||
'SHANNON_AI_BASE_URL',
|
'SHANNON_AI_BASE_URL',
|
||||||
'SHANNON_AI_OPENAI_FORMAT',
|
|
||||||
// Opt-in debug flag: when set, the worker persists a bounded, sanitized snippet of a failed
|
// Opt-in debug flag: when set, the worker persists a bounded, sanitized snippet of a failed
|
||||||
// provider turn's raw error message to error.log. Off by default; provider prose stays out of
|
// provider turn's raw error message to error.log. Off by default; provider prose stays out of
|
||||||
// durable state unless an operator deliberately enables it for a diagnosis.
|
// durable state unless an operator deliberately enables it for a diagnosis.
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ export const START_OPTIONS: readonly (readonly [string, string])[] = [
|
|||||||
['-u, --url <url>', 'Target URL (required)'],
|
['-u, --url <url>', 'Target URL (required)'],
|
||||||
['-r, --repo <path>', 'Repository path (required)'],
|
['-r, --repo <path>', 'Repository path (required)'],
|
||||||
['-c, --config <path>', 'Configuration file (YAML)'],
|
['-c, --config <path>', 'Configuration file (YAML)'],
|
||||||
|
['--models-config <path>', "pi model config (models.json) defining models pi's catalogue lacks"],
|
||||||
['-o, --output <path>', 'Copy deliverables to this directory after the run'],
|
['-o, --output <path>', 'Copy deliverables to this directory after the run'],
|
||||||
['-w, --workspace <name>', 'Named workspace (auto-resumes if it exists)'],
|
['-w, --workspace <name>', 'Named workspace (auto-resumes if it exists)'],
|
||||||
['-f, --follow', 'Stream the scan log until it finishes'],
|
['-f, --follow', 'Stream the scan log until it finishes'],
|
||||||
|
['--validate-auth', 'Validate authentication only, then stop (no pentest)'],
|
||||||
|
['--validate-model', 'Validate the AI model only, then stop (no pentest)'],
|
||||||
['--pipeline-testing', 'Use minimal prompts for fast testing'],
|
['--pipeline-testing', 'Use minimal prompts for fast testing'],
|
||||||
['--keep-container', 'Preserve the worker container after exit for log inspection'],
|
['--keep-container', 'Preserve the worker container after exit for log inspection'],
|
||||||
];
|
];
|
||||||
@@ -44,6 +47,8 @@ const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|||||||
'start -u https://example.com -r ./my-repo',
|
'start -u https://example.com -r ./my-repo',
|
||||||
'start -u https://example.com -r /path/to/repo -c config.yaml -w q1-audit',
|
'start -u https://example.com -r /path/to/repo -c config.yaml -w q1-audit',
|
||||||
'start -u https://example.com -r ./my-repo --follow',
|
'start -u https://example.com -r ./my-repo --follow',
|
||||||
|
'start -u https://example.com -r ./my-repo -c config.yaml --validate-auth',
|
||||||
|
'start -u https://example.com -r ./my-repo --validate-model',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
stop: {
|
stop: {
|
||||||
|
|||||||
@@ -61,6 +61,18 @@ function blockSudo(): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Refuse to run on native Windows. WSL2 reports `linux`, so it is unaffected. */
|
||||||
|
function blockNativeWindows(): void {
|
||||||
|
if (process.platform !== 'win32') return;
|
||||||
|
|
||||||
|
failWith(
|
||||||
|
'CLI_PRECONDITION_FAILED',
|
||||||
|
'Shannon does not run on native Windows.',
|
||||||
|
'Run Shannon inside WSL2. Setup instructions:',
|
||||||
|
'https://github.com/KeygraphHQ/shannon/blob/main/docs/platforms.md',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Commands whose `--json` output contract extends to failures. */
|
/** Commands whose `--json` output contract extends to failures. */
|
||||||
const JSON_CAPABLE_COMMANDS = new Set(['status', 'scans', 'version', '--version', '-v']);
|
const JSON_CAPABLE_COMMANDS = new Set(['status', 'scans', 'version', '--version', '-v']);
|
||||||
|
|
||||||
@@ -171,11 +183,14 @@ interface ParsedStartArgs {
|
|||||||
url: string;
|
url: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
|
modelsConfig?: string;
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
output?: string;
|
output?: string;
|
||||||
pipelineTesting: boolean;
|
pipelineTesting: boolean;
|
||||||
keepContainer: boolean;
|
keepContainer: boolean;
|
||||||
follow: boolean;
|
follow: boolean;
|
||||||
|
authOnly: boolean;
|
||||||
|
validateModel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseStartArgs(argv: string[]): ParsedStartArgs {
|
function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||||
@@ -184,6 +199,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
|||||||
url: ['-u', '--url'],
|
url: ['-u', '--url'],
|
||||||
repo: ['-r', '--repo'],
|
repo: ['-r', '--repo'],
|
||||||
config: ['-c', '--config'],
|
config: ['-c', '--config'],
|
||||||
|
modelsConfig: ['--models-config'],
|
||||||
output: ['-o', '--output'],
|
output: ['-o', '--output'],
|
||||||
workspace: ['-w', '--workspace'],
|
workspace: ['-w', '--workspace'],
|
||||||
},
|
},
|
||||||
@@ -191,6 +207,8 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
|||||||
pipelineTesting: ['--pipeline-testing'],
|
pipelineTesting: ['--pipeline-testing'],
|
||||||
keepContainer: ['--keep-container'],
|
keepContainer: ['--keep-container'],
|
||||||
follow: ['-f', '--follow'],
|
follow: ['-f', '--follow'],
|
||||||
|
authOnly: ['--validate-auth'],
|
||||||
|
validateModel: ['--validate-model'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,13 +224,27 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
|||||||
failUsage(`invalid --url: ${url}`);
|
failUsage(`invalid --url: ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flags.authOnly && flags.validateModel) {
|
||||||
|
failUsage('--validate-auth and --validate-model cannot be combined; run one validation at a time');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.authOnly && !values.config) {
|
||||||
|
failUsage(
|
||||||
|
'--validate-auth needs a config file with an authentication block',
|
||||||
|
`Usage: ${commandPrefix()} start -u <url> -r <path> -c <config.yaml> --validate-auth`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
repo,
|
repo,
|
||||||
pipelineTesting: !!flags.pipelineTesting,
|
pipelineTesting: !!flags.pipelineTesting,
|
||||||
keepContainer: !!flags.keepContainer,
|
keepContainer: !!flags.keepContainer,
|
||||||
follow: !!flags.follow,
|
follow: !!flags.follow,
|
||||||
|
authOnly: !!flags.authOnly,
|
||||||
|
validateModel: !!flags.validateModel,
|
||||||
...(values.config && { config: values.config }),
|
...(values.config && { config: values.config }),
|
||||||
|
...(values.modelsConfig && { modelsConfig: values.modelsConfig }),
|
||||||
...(values.workspace && { workspace: values.workspace }),
|
...(values.workspace && { workspace: values.workspace }),
|
||||||
...(values.output && { output: values.output }),
|
...(values.output && { output: values.output }),
|
||||||
};
|
};
|
||||||
@@ -259,6 +291,7 @@ async function main(): Promise<void> {
|
|||||||
enableJsonErrors();
|
enableJsonErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockNativeWindows();
|
||||||
blockSudo();
|
blockSudo();
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|||||||
@@ -52,15 +52,6 @@ export const PROVIDER_CREDENTIAL_HINT: Readonly<Record<CuratedProviderId, string
|
|||||||
/** Model used when SHANNON_AI_MODEL is unset. */
|
/** Model used when SHANNON_AI_MODEL is unset. */
|
||||||
export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
||||||
|
|
||||||
/**
|
|
||||||
* Values SHANNON_AI_OPENAI_FORMAT accepts, selecting the wire format an
|
|
||||||
* OpenAI-compatible gateway serves. Mirrors OPENAI_FORMATS in
|
|
||||||
* apps/worker/src/ai/models.ts; the worker validates and applies it.
|
|
||||||
*/
|
|
||||||
export const OPENAI_FORMATS = ['chat-completions', 'responses'] as const;
|
|
||||||
|
|
||||||
export type OpenAiFormat = (typeof OPENAI_FORMATS)[number];
|
|
||||||
|
|
||||||
export interface ModelSpec {
|
export interface ModelSpec {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
|
|||||||
+38
-2
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Path resolution for --repo and --config arguments.
|
* Path resolution for --repo, --config and --models-config arguments.
|
||||||
*
|
*
|
||||||
* Both --repo and --config are filesystem paths, absolute or relative to CWD.
|
* All three are filesystem paths, absolute or relative to CWD.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -48,6 +48,13 @@ export const FINAL_REPORT_PDF_FILENAME = 'Security-Assessment-Report.pdf';
|
|||||||
*/
|
*/
|
||||||
export const FINAL_REPORT_MD_FILENAME = 'Security-Assessment-Report.md';
|
export const FINAL_REPORT_MD_FILENAME = 'Security-Assessment-Report.md';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason for a pre-workflow failure, written by the worker under INTERNAL_DIR. The CLI reads it
|
||||||
|
* during the startup poll to report the real cause instead of a generic timeout. Must match
|
||||||
|
* STARTUP_ERROR_FILENAME in the worker package.
|
||||||
|
*/
|
||||||
|
export const STARTUP_ERROR_FILENAME = 'startup-error.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a run-directory file (e.g. session.json, workflow.log), preferring the
|
* Resolve a run-directory file (e.g. session.json, workflow.log), preferring the
|
||||||
* current INTERNAL_DIR location and falling back to the legacy run-root location
|
* current INTERNAL_DIR location and falling back to the legacy run-root location
|
||||||
@@ -108,3 +115,32 @@ export function resolveConfig(configArg: string): MountPair {
|
|||||||
containerPath: `/app/configs/${basename}`,
|
containerPath: `/app/configs/${basename}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container path for a mounted pi model config. Fixed, not derived from the host filename:
|
||||||
|
* the worker detects the file here to decide whether models.json is enabled at all. Must
|
||||||
|
* match MODELS_CONFIG_PATH in the worker package.
|
||||||
|
*/
|
||||||
|
export const MODELS_CONFIG_CONTAINER_PATH = '/app/models.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve --models-config to an absolute path and container mount. Content is left
|
||||||
|
* unparsed: pi's models.json permits comments, so JSON.parse would reject valid input,
|
||||||
|
* and pi's own loader reports schema faults far better — the worker surfaces those.
|
||||||
|
*/
|
||||||
|
export function resolveModelsConfig(modelsConfigArg: string): MountPair {
|
||||||
|
const hostPath = path.resolve(expandHome(modelsConfigArg));
|
||||||
|
|
||||||
|
if (!fs.existsSync(hostPath)) {
|
||||||
|
fail(`Model config file not found: ${hostPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.statSync(hostPath).isFile()) {
|
||||||
|
fail(`Not a file: ${hostPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostPath,
|
||||||
|
containerPath: MODELS_CONFIG_CONTAINER_PATH,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -363,6 +363,33 @@ function agenticSastPhase(operations: readonly DerivedAgent[]): DerivedPhase | u
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Preflight rows shown at the top of the tree, in run order. Each is its own single-line phase. */
|
||||||
|
const PREFLIGHT_ROW_KEYS = ['preflight', 'cyber-access'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two preflight gates the worker persists — the preflight checks and the cyber-access verification —
|
||||||
|
* as top-of-tree rows. Each appears once its stage is recorded (running, then done or failed); a
|
||||||
|
* run that never reaches a gate simply omits its row.
|
||||||
|
*/
|
||||||
|
function preflightPhases(operations: readonly DerivedAgent[]): DerivedPhase[] {
|
||||||
|
const byKey = new Map(operations.map((operation) => [operation.name, operation]));
|
||||||
|
const phases: DerivedPhase[] = [];
|
||||||
|
for (const key of PREFLIGHT_ROW_KEYS) {
|
||||||
|
const operation = byKey.get(key);
|
||||||
|
if (operation === undefined) continue;
|
||||||
|
phases.push({
|
||||||
|
key: operation.name,
|
||||||
|
label: operation.label,
|
||||||
|
children: false,
|
||||||
|
meta: 'duration',
|
||||||
|
state: operation.state,
|
||||||
|
summary: operation,
|
||||||
|
agents: [operation],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return phases;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookkeeping rows worth showing. A deterministic stage that has completed says nothing —
|
* Bookkeeping rows worth showing. A deterministic stage that has completed says nothing —
|
||||||
* it can only ever read 0s — but one that is still running, or that failed, is exactly what
|
* it can only ever read 0s — but one that is still running, or that failed, is exactly what
|
||||||
@@ -408,13 +435,14 @@ function assemblePhases(agentPhases: readonly DerivedPhase[], operations: readon
|
|||||||
return phase;
|
return phase;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const preflight = preflightPhases(operations);
|
||||||
const sast = agenticSastPhase(operations);
|
const sast = agenticSastPhase(operations);
|
||||||
if (sast === undefined) return phases;
|
if (sast === undefined) return [...preflight, ...phases];
|
||||||
|
|
||||||
// Agentic SAST starts with the scan and runs alongside the pentest, so it reads after
|
// Agentic SAST starts with the scan and runs alongside the pentest, so it reads after
|
||||||
// the login check rather than appended past Reporting where it never ran.
|
// the login check rather than appended past Reporting where it never ran.
|
||||||
const afterAuth = phases.findIndex((phase) => phase.key === 'auth-validation') + 1;
|
const afterAuth = phases.findIndex((phase) => phase.key === 'auth-validation') + 1;
|
||||||
return [...phases.slice(0, afterAuth), sast, ...phases.slice(afterAuth)];
|
return [...preflight, ...phases.slice(0, afterAuth), sast, ...phases.slice(afterAuth)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export { agentError };
|
export { agentError };
|
||||||
@@ -108,6 +108,8 @@ const MISCELLANEOUS_EXPLOIT_AGENT: AgentSpec = {
|
|||||||
* available guess.
|
* available guess.
|
||||||
*/
|
*/
|
||||||
export function pipelineForState(state: PipelineState | null): readonly PhaseSpec[] {
|
export function pipelineForState(state: PipelineState | null): readonly PhaseSpec[] {
|
||||||
|
if (state?.validateModel === true) return [];
|
||||||
|
if (state?.authOnly === true) return PIPELINE.filter((phase) => phase.key === 'auth-validation');
|
||||||
if (state?.expectedAgents === undefined) return PIPELINE;
|
if (state?.expectedAgents === undefined) return PIPELINE;
|
||||||
const expected = new Set(state.expectedAgents);
|
const expected = new Set(state.expectedAgents);
|
||||||
return PIPELINE.map((phase) => {
|
return PIPELINE.map((phase) => {
|
||||||
@@ -136,7 +138,8 @@ const AGENTIC_SAST_PARENT_KEY = 'agentic-sast';
|
|||||||
// apps/worker/src/temporal/reconcile-activity-types.ts, and
|
// apps/worker/src/temporal/reconcile-activity-types.ts, and
|
||||||
// apps/worker/src/ai/sast/capella/temporal/activity-types.ts.
|
// apps/worker/src/ai/sast/capella/temporal/activity-types.ts.
|
||||||
const OPERATION_ACTIVITY_PROGRESS: Readonly<Record<string, ActivityProgressSpec>> = {
|
const OPERATION_ACTIVITY_PROGRESS: Readonly<Record<string, ActivityProgressSpec>> = {
|
||||||
runPreflightValidation: { key: 'preflight', label: 'Preflight validation', kind: 'operation' },
|
runPreflightValidation: { key: 'preflight', label: 'Preflight', kind: 'operation' },
|
||||||
|
runCyberAccessVerification: { key: 'cyber-access', label: 'Cyber access verification', kind: 'operation' },
|
||||||
syncPlaywrightStealthConfig: { key: 'preflight', label: 'Browser setup', kind: 'operation' },
|
syncPlaywrightStealthConfig: { key: 'preflight', label: 'Browser setup', kind: 'operation' },
|
||||||
initDeliverableGit: { key: 'scan-initialization', label: 'Initialize deliverables', kind: 'operation' },
|
initDeliverableGit: { key: 'scan-initialization', label: 'Initialize deliverables', kind: 'operation' },
|
||||||
syncCodePathDenyRules: { key: 'scan-initialization', label: 'Apply source rules', kind: 'operation' },
|
syncCodePathDenyRules: { key: 'scan-initialization', label: 'Apply source rules', kind: 'operation' },
|
||||||
@@ -352,6 +355,8 @@ export type PipelineStatus = 'running' | 'completed' | 'failed' | 'cancelled' |
|
|||||||
|
|
||||||
export interface PipelineState {
|
export interface PipelineState {
|
||||||
readonly status: PipelineStatus;
|
readonly status: PipelineStatus;
|
||||||
|
readonly authOnly?: boolean;
|
||||||
|
readonly validateModel?: boolean;
|
||||||
readonly currentPhase: string | null;
|
readonly currentPhase: string | null;
|
||||||
readonly currentAgent: string | null;
|
readonly currentAgent: string | null;
|
||||||
readonly completedAgents: string[];
|
readonly completedAgents: string[];
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ function isProviderFailureCategory(value: unknown): value is string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OPERATION_LABELS = new Set([
|
const OPERATION_LABELS = new Set([
|
||||||
|
'Preflight',
|
||||||
|
'Cyber access verification',
|
||||||
'Agentic SAST',
|
'Agentic SAST',
|
||||||
// Capella stage rows, signalled up from the SAST child workflow. Mirrors
|
// Capella stage rows, signalled up from the SAST child workflow. Mirrors
|
||||||
// CAPELLA_STAGE_LABELS in apps/worker/src/ai/sast/types.ts, minus the deterministic
|
// CAPELLA_STAGE_LABELS in apps/worker/src/ai/sast/types.ts, minus the deterministic
|
||||||
@@ -228,7 +230,7 @@ export function safeOperationLabel(value: string): string {
|
|||||||
|
|
||||||
export function safeOperationKey(value: string): string {
|
export function safeOperationKey(value: string): string {
|
||||||
if (
|
if (
|
||||||
/^(?:agentic-sast|miscellaneous-pipeline|report:(?:initialize|assemble|compact|checkpoint|finalize|finalize-degraded|terminal|surface))$/u.test(
|
/^(?:preflight|cyber-access|agentic-sast|miscellaneous-pipeline|report:(?:initialize|assemble|compact|checkpoint|finalize|finalize-degraded|terminal|surface))$/u.test(
|
||||||
value,
|
value,
|
||||||
) ||
|
) ||
|
||||||
/^agentic-sast:(?:architecture|threat-model|plan|research|dedupe|review|critic|confirm|calibrate)$/u.test(value) ||
|
/^agentic-sast:(?:architecture|threat-model|plan|research|dedupe|review|critic|confirm|calibrate)$/u.test(value) ||
|
||||||
|
|||||||
@@ -257,6 +257,25 @@ export async function describeScan(workflowId: string): Promise<ScanDescription
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity-type names pending on a running scan; empty on any failure. Tolerant (it feeds the
|
||||||
|
* start spinner) unlike describeScan, which fails closed so the status tree is never incomplete.
|
||||||
|
*/
|
||||||
|
export async function runningActivityTypes(workflowId: string): Promise<readonly string[]> {
|
||||||
|
try {
|
||||||
|
const client = await getClient();
|
||||||
|
const desc = await client.workflow.getHandle(workflowId).describe();
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const pending of desc.raw.pendingActivities ?? []) {
|
||||||
|
const name = pending.activityType?.name;
|
||||||
|
if (name) names.push(name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Live progress of a running scan via the getProgress query. Null if the query can't be served (no worker). */
|
/** Live progress of a running scan via the getProgress query. Null if the query can't be served (no worker). */
|
||||||
export async function queryProgress(workflowId: string): Promise<PipelineState | null> {
|
export async function queryProgress(workflowId: string): Promise<PipelineState | null> {
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
"@earendil-works/pi-agent-core": "^0.84.4",
|
||||||
"@earendil-works/pi-ai": "^0.84.2",
|
"@earendil-works/pi-ai": "^0.84.4",
|
||||||
"@earendil-works/pi-coding-agent": "^0.84.2",
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
||||||
"@gotgenes/pi-permission-system": "^10.9.0",
|
"@gotgenes/pi-permission-system": "^10.9.0",
|
||||||
"@temporalio/activity": "1.15.0",
|
"@temporalio/activity": "1.15.0",
|
||||||
"@temporalio/client": "1.15.0",
|
"@temporalio/client": "1.15.0",
|
||||||
|
|||||||
+51
-107
@@ -20,6 +20,13 @@
|
|||||||
* Resolution returns a pi `Model` plus the `ModelRuntime` that owns its auth,
|
* Resolution returns a pi `Model` plus the `ModelRuntime` that owns its auth,
|
||||||
* built over an in-memory credential store primed from the environment.
|
* built over an in-memory credential store primed from the environment.
|
||||||
*
|
*
|
||||||
|
* The catalogue is refreshed over the network at scan start, so a newly released model
|
||||||
|
* on a catalogue provider resolves on its own. A model the catalogue does not carry, such
|
||||||
|
* as a router model under its own id, or a self-hosted server, is described in a
|
||||||
|
* pi `models.json` (the CLI's `--models-config`), which merges over the catalogue. The
|
||||||
|
* credential store below outranks any `apiKey` that file carries, so it describes the
|
||||||
|
* model while the environment still supplies the secret.
|
||||||
|
*
|
||||||
* The CLI cannot import this module (it ships as a separate bundle), so
|
* The CLI cannot import this module (it ships as a separate bundle), so
|
||||||
* `apps/cli/src/model-spec.ts` mirrors the parse rule and the provider/credential
|
* `apps/cli/src/model-spec.ts` mirrors the parse rule and the provider/credential
|
||||||
* tables by hand for its own `status` rendering and setup wizard. The two copies
|
* tables by hand for its own `status` rendering and setup wizard. The two copies
|
||||||
@@ -32,6 +39,7 @@ import { existsSync } from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai';
|
import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai';
|
||||||
import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||||
|
import { MODELS_CONFIG_PATH } from '../paths.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providers Shannon curates with their own credential variables, config sections,
|
* Providers Shannon curates with their own credential variables, config sections,
|
||||||
@@ -78,42 +86,6 @@ export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
|||||||
/** Browsable pi model catalogue — the source of valid `<provider>:<model-id>` ids. */
|
/** Browsable pi model catalogue — the source of valid `<provider>:<model-id>` ids. */
|
||||||
export const PI_CATALOG_URL = 'https://pi.dev/models';
|
export const PI_CATALOG_URL = 'https://pi.dev/models';
|
||||||
|
|
||||||
/**
|
|
||||||
* Wire formats an OpenAI-compatible gateway may serve, named by
|
|
||||||
* SHANNON_AI_OPENAI_FORMAT. Only `openai` offers a choice: every other supported
|
|
||||||
* provider has exactly one API in pi's registry.
|
|
||||||
*/
|
|
||||||
export const OPENAI_FORMATS = {
|
|
||||||
'chat-completions': 'openai-completions',
|
|
||||||
responses: 'openai-responses',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type OpenAiFormat = keyof typeof OPENAI_FORMATS;
|
|
||||||
|
|
||||||
/** Format assumed when a gateway is configured but no format is named. */
|
|
||||||
export const DEFAULT_OPENAI_FORMAT: OpenAiFormat = 'chat-completions';
|
|
||||||
|
|
||||||
function isOpenAiFormat(value: string): value is OpenAiFormat {
|
|
||||||
return value in OPENAI_FORMATS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read SHANNON_AI_OPENAI_FORMAT. Unset returns undefined, which lets the caller
|
|
||||||
* distinguish "not configured" from an explicit choice and reject the variable
|
|
||||||
* where it has no effect.
|
|
||||||
*/
|
|
||||||
export function resolveOpenAiFormat(): OpenAiFormat | undefined {
|
|
||||||
const raw = process.env.SHANNON_AI_OPENAI_FORMAT?.trim();
|
|
||||||
if (!raw) return undefined;
|
|
||||||
|
|
||||||
if (!isOpenAiFormat(raw)) {
|
|
||||||
throw new Error(
|
|
||||||
`SHANNON_AI_OPENAI_FORMAT must be one of: ${Object.keys(OPENAI_FORMATS).join(', ')}. Got "${raw}".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModelSpec {
|
export interface ModelSpec {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
@@ -232,20 +204,50 @@ export function piAuthPresent(): boolean {
|
|||||||
return existsSync(piAuthPath());
|
return existsSync(piAuthPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Path of the mounted pi model config, or undefined when the scan supplied none. */
|
||||||
|
export function modelsConfigPath(): string | undefined {
|
||||||
|
return existsSync(MODELS_CONFIG_PATH) ? MODELS_CONFIG_PATH : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a ModelRuntime whose only credential is the one supplied. Model catalogs
|
* Where pi persists remote model catalogues. Pinned to the writable agent dir because pi
|
||||||
* stay offline (`allowModelNetwork` defaults to false) so a scan never blocks on
|
* otherwise derives it from `dirname(modelsPath)`, which is a read-only mount.
|
||||||
* a catalog refresh.
|
*/
|
||||||
|
function modelsStorePath(): string {
|
||||||
|
return path.join(getAgentDir(), 'models-store.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a ModelRuntime whose only credential is the one supplied. `allowModelNetwork`
|
||||||
|
* refreshes the model catalogue over the network at scan start, so the registry reflects
|
||||||
|
* models the pinned pi build predates. The fetch is bounded and falls back to the static
|
||||||
|
* catalogue on timeout, so an unreachable endpoint cannot hang the scan. A mounted
|
||||||
|
* `--models-config` overlays the catalogue and is reloaded on every refresh, so its
|
||||||
|
* definitions take precedence.
|
||||||
|
*
|
||||||
|
* `modelsPath` is always explicit, never pi's default of `<agent dir>/models.json`: with no
|
||||||
|
* `--models-config` it is null, which switches models.json off outright, so a stray file in
|
||||||
|
* that shared dir cannot feed model definitions to a run that did not ask for them.
|
||||||
|
* `modelsStorePath` is pinned to the writable agent dir, replacing pi's default
|
||||||
|
* `dirname(modelsPath)` (a read-only mount) as the fetched catalogue's store.
|
||||||
*
|
*
|
||||||
* When the host's pi auth.json is present, the runtime reads it instead: pi's
|
* When the host's pi auth.json is present, the runtime reads it instead: pi's
|
||||||
* disk-backed store resolves the credential. The mount is writable so OAuth
|
* disk-backed store resolves the credential. The mount is writable so OAuth
|
||||||
* refreshes persist to the host for subsequent runs.
|
* refreshes persist to the host for subsequent runs.
|
||||||
*/
|
*/
|
||||||
export async function createModelRuntime(providerId: string, apiKey: string | undefined): Promise<ModelRuntime> {
|
export async function createModelRuntime(providerId: string, apiKey: string | undefined): Promise<ModelRuntime> {
|
||||||
|
const modelsPath = modelsConfigPath();
|
||||||
|
const modelSources = {
|
||||||
|
modelsPath: modelsPath ?? null,
|
||||||
|
...(modelsPath ? { modelsStorePath: modelsStorePath() } : {}),
|
||||||
|
allowModelNetwork: true,
|
||||||
|
modelRefreshTimeoutMs: 10_000,
|
||||||
|
};
|
||||||
|
|
||||||
if (piAuthPresent()) {
|
if (piAuthPresent()) {
|
||||||
return ModelRuntime.create({ authPath: piAuthPath() });
|
return ModelRuntime.create({ ...modelSources, authPath: piAuthPath() });
|
||||||
}
|
}
|
||||||
return ModelRuntime.create({ credentials: new RuntimeCredentialStore(providerId, apiKey) });
|
return ModelRuntime.create({ ...modelSources, credentials: new RuntimeCredentialStore(providerId, apiKey) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelSelection {
|
export interface ModelSelection {
|
||||||
@@ -257,80 +259,23 @@ export interface ModelSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Point a model descriptor at a gateway.
|
* Resolve a model against a runtime, returning undefined when the id is unknown.
|
||||||
*
|
*
|
||||||
* An OpenAI gateway may serve either wire format, named by
|
* The model must exist in the runtime's registry, whether or not an endpoint override
|
||||||
* SHANNON_AI_OPENAI_FORMAT and defaulting to chat completions, which is what
|
* is in play — a base URL changes the address and nothing else. A gateway serving a
|
||||||
* most gateway software exposes. Switching to completions also drops the stored
|
* model under its own name is described in a `--models-config` file, which puts a real
|
||||||
* `compat` block: the catalogue's block describes Responses, and an explicit
|
* descriptor in the registry rather than guessing one from an unrelated model.
|
||||||
* entry outranks pi's `detectCompat`, so leaving it would apply Responses
|
|
||||||
* settings to a completions request. Staying on Responses keeps it, since it
|
|
||||||
* then describes the format in use. Every other provider has one API and only
|
|
||||||
* changes address.
|
|
||||||
*/
|
|
||||||
function pointAtGateway(model: Model<Api>, providerId: string, baseUrl: string, format: OpenAiFormat): Model<Api> {
|
|
||||||
if (providerId !== 'openai') return { ...model, baseUrl };
|
|
||||||
if (format === 'responses') return { ...model, baseUrl, api: OPENAI_FORMATS.responses };
|
|
||||||
|
|
||||||
const { compat: _responsesCompat, ...withoutCompat } = model;
|
|
||||||
return { ...withoutCompat, baseUrl, api: OPENAI_FORMATS['chat-completions'] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a model against a runtime.
|
|
||||||
*
|
|
||||||
* Direct to a provider, the model must exist in the catalogue. Behind a custom
|
|
||||||
* endpoint it need not: a gateway may serve models under its own names, so an
|
|
||||||
* unknown id is passed through on a descriptor borrowed from the provider's
|
|
||||||
* catalogue for its API dialect. Cost and context window on such a descriptor
|
|
||||||
* are the reference model's, so spend figures are approximate there.
|
|
||||||
*
|
|
||||||
* Returns undefined when the id is unresolvable — unknown with no endpoint
|
|
||||||
* override, or a provider carrying no models at all.
|
|
||||||
*/
|
*/
|
||||||
export function resolveModel(
|
export function resolveModel(
|
||||||
modelRuntime: ModelRuntime,
|
modelRuntime: ModelRuntime,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
modelId: string,
|
modelId: string,
|
||||||
baseUrl: string | undefined,
|
baseUrl: string | undefined,
|
||||||
format: OpenAiFormat = DEFAULT_OPENAI_FORMAT,
|
|
||||||
): Model<Api> | undefined {
|
): Model<Api> | undefined {
|
||||||
const found = modelRuntime.getModel(providerId, modelId);
|
const found = modelRuntime.getModel(providerId, modelId);
|
||||||
if (found) {
|
if (!found) return undefined;
|
||||||
return baseUrl ? pointAtGateway(found, providerId, baseUrl, format) : found;
|
|
||||||
}
|
|
||||||
if (!baseUrl) return undefined;
|
|
||||||
|
|
||||||
const reference = modelRuntime.getModels(providerId)[0];
|
return baseUrl ? { ...found, baseUrl } : found;
|
||||||
if (!reference) return undefined;
|
|
||||||
|
|
||||||
return pointAtGateway({ ...reference, id: modelId, name: modelId }, providerId, baseUrl, format);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate SHANNON_AI_OPENAI_FORMAT against the rest of the configuration and
|
|
||||||
* return the format a gateway run should use.
|
|
||||||
*
|
|
||||||
* The variable only reaches a request when both an OpenAI model and a gateway
|
|
||||||
* are configured, so it is rejected outside that combination rather than
|
|
||||||
* silently ignored.
|
|
||||||
*/
|
|
||||||
export function resolveGatewayFormat(providerId: string, baseUrl: string | undefined): OpenAiFormat {
|
|
||||||
const configured = resolveOpenAiFormat();
|
|
||||||
if (!configured) return DEFAULT_OPENAI_FORMAT;
|
|
||||||
|
|
||||||
if (providerId !== 'openai') {
|
|
||||||
throw new Error(
|
|
||||||
`SHANNON_AI_OPENAI_FORMAT applies to openai models only, but SHANNON_AI_MODEL selects "${providerId}". ` +
|
|
||||||
`${providerId} serves a single API, so there is no format to choose.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!baseUrl) {
|
|
||||||
throw new Error(
|
|
||||||
'SHANNON_AI_OPENAI_FORMAT applies to gateway runs only. Set SHANNON_AI_BASE_URL, or unset the format to call OpenAI directly.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return configured;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -340,12 +285,11 @@ export function resolveGatewayFormat(providerId: string, baseUrl: string | undef
|
|||||||
export async function resolveModelSelection(): Promise<ModelSelection> {
|
export async function resolveModelSelection(): Promise<ModelSelection> {
|
||||||
const { providerId, modelId } = resolveModelSpec();
|
const { providerId, modelId } = resolveModelSpec();
|
||||||
const credentials = resolveProviderCredentials(providerId);
|
const credentials = resolveProviderCredentials(providerId);
|
||||||
const format = resolveGatewayFormat(providerId, credentials.baseUrl);
|
|
||||||
|
|
||||||
const mountedPiAuth = piAuthPresent();
|
const mountedPiAuth = piAuthPresent();
|
||||||
const modelRuntime = await createModelRuntime(providerId, credentials.apiKey);
|
const modelRuntime = await createModelRuntime(providerId, credentials.apiKey);
|
||||||
|
|
||||||
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl, format);
|
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Model not found in pi registry: provider="${providerId}" model="${modelId}". Browse valid providers and models at ${PI_CATALOG_URL}.`,
|
`Model not found in pi registry: provider="${providerId}" model="${modelId}". Browse valid providers and models at ${PI_CATALOG_URL}.`,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import type {
|
|||||||
CapellaTool,
|
CapellaTool,
|
||||||
} from './capella-agent-types.js';
|
} from './capella-agent-types.js';
|
||||||
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
||||||
|
import { PI_THINKING_LEVEL } from './thinking-level.js';
|
||||||
|
|
||||||
const MAX_ERROR_LENGTH = 2_000;
|
const MAX_ERROR_LENGTH = 2_000;
|
||||||
const MAX_TOOLS_PER_SESSION = 32;
|
const MAX_TOOLS_PER_SESSION = 32;
|
||||||
@@ -393,6 +394,7 @@ class StandaloneCapellaAgentExecutor implements CapellaAgentExecutor {
|
|||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
agentDir,
|
agentDir,
|
||||||
model: selection.model,
|
model: selection.model,
|
||||||
|
thinkingLevel: PI_THINKING_LEVEL,
|
||||||
modelRuntime: selection.modelRuntime,
|
modelRuntime: selection.modelRuntime,
|
||||||
noTools: 'all',
|
noTools: 'all',
|
||||||
tools: toolNames,
|
tools: toolNames,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { permissionSystemConfigExists, permissionSystemPackageDir } from './perm
|
|||||||
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
||||||
import { createGlobTool, createTodoWriteTool } from './session-tools.js';
|
import { createGlobTool, createTodoWriteTool } from './session-tools.js';
|
||||||
import { createTaskTool } from './task-tool.js';
|
import { createTaskTool } from './task-tool.js';
|
||||||
|
import { PI_THINKING_LEVEL } from './thinking-level.js';
|
||||||
import { TraceEmitter } from './trace-emitter.js';
|
import { TraceEmitter } from './trace-emitter.js';
|
||||||
import { providerTurnError, type SafeProviderTurnDetails, safeProviderTurnDetails } from './turn-error.js';
|
import { providerTurnError, type SafeProviderTurnDetails, safeProviderTurnDetails } from './turn-error.js';
|
||||||
|
|
||||||
@@ -332,6 +333,7 @@ export async function runPiPrompt(
|
|||||||
({ session } = await createAgentSession({
|
({ session } = await createAgentSession({
|
||||||
cwd: sourceDir,
|
cwd: sourceDir,
|
||||||
model: selection.model,
|
model: selection.model,
|
||||||
|
thinkingLevel: PI_THINKING_LEVEL,
|
||||||
tools,
|
tools,
|
||||||
customTools,
|
customTools,
|
||||||
modelRuntime: selection.modelRuntime,
|
modelRuntime: selection.modelRuntime,
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
// Copyright (C) 2026 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.
|
|
||||||
|
|
||||||
/** Attempt-local working-tree copy used by the task-formation model boundary. */
|
|
||||||
|
|
||||||
import type { Dirent, Stats } from 'node:fs';
|
|
||||||
import { cp, lstat, mkdir, mkdtemp, readdir, realpath, rm } from 'node:fs/promises';
|
|
||||||
import os from 'node:os';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { ArtifactIntegrityError, ReconciliationIoError } from '../reconciliation/artifact-store.js';
|
|
||||||
|
|
||||||
const JAIL_PREFIX = 'shannon-task-formation-';
|
|
||||||
// Never copied into the model-readable jail: `.git` carries deliverables history, `.shannon` holds
|
|
||||||
// scan internals, and `.pi` holds provider credentials. Any of these reaching the jail would expose
|
|
||||||
// them to the tools the model drives. The post-copy verification re-checks their absence by name.
|
|
||||||
const ALWAYS_EXCLUDED_NAMES = Object.freeze(['.git', '.shannon', '.pi'] as const);
|
|
||||||
|
|
||||||
export interface SourceJailOptions {
|
|
||||||
readonly sourceRoot: string;
|
|
||||||
readonly deliverablesPath: string;
|
|
||||||
readonly reconciliationWorkspacePath: string;
|
|
||||||
readonly signal?: AbortSignal;
|
|
||||||
/** Test-only filesystem selector. Production uses `os.tmpdir()`. */
|
|
||||||
readonly tempRoot?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One source-only jail plus the immutable deny rules used by its live tool gate. */
|
|
||||||
export interface SourceJail {
|
|
||||||
readonly dir: string;
|
|
||||||
readonly deniedPaths: readonly string[];
|
|
||||||
cleanup(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isErrno(error: unknown, code: string): boolean {
|
|
||||||
return error instanceof Error && (error as NodeJS.ErrnoException).code === code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancellationError(signal: AbortSignal): Error {
|
|
||||||
if (signal.reason instanceof Error) return signal.reason;
|
|
||||||
return new DOMException('Task formation was cancelled.', 'AbortError');
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCancellation(signal: AbortSignal | undefined): void {
|
|
||||||
if (signal?.aborted === true) throw cancellationError(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path-confinement predicate: true only when `candidate` is `root` itself or lies beneath it.
|
|
||||||
// A relative path that escapes upward (`..`) or is absolute means the candidate is outside the root.
|
|
||||||
function isWithin(root: string, candidate: string): boolean {
|
|
||||||
const relativePath = path.relative(root, candidate);
|
|
||||||
return (
|
|
||||||
relativePath === '' ||
|
|
||||||
(!relativePath.startsWith(`..${path.sep}`) && relativePath !== '..' && !path.isAbsolute(relativePath))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function relativeExclusion(
|
|
||||||
sourceRoot: string,
|
|
||||||
lexicalSourceRoot: string,
|
|
||||||
candidate: string,
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
const resolved = path.resolve(candidate);
|
|
||||||
let relativePath: string | undefined;
|
|
||||||
if (isWithin(sourceRoot, resolved)) {
|
|
||||||
relativePath = path.relative(sourceRoot, resolved);
|
|
||||||
} else if (isWithin(lexicalSourceRoot, resolved)) {
|
|
||||||
relativePath = path.relative(lexicalSourceRoot, resolved);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const canonicalCandidate = await realpath(resolved);
|
|
||||||
if (isWithin(sourceRoot, canonicalCandidate)) {
|
|
||||||
relativePath = path.relative(sourceRoot, canonicalCandidate);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (relativePath === undefined) return undefined;
|
|
||||||
|
|
||||||
if (relativePath === '') {
|
|
||||||
// An exclusion that resolves to the whole root would empty the jail. Fail closed rather than
|
|
||||||
// copy nothing and hand the model an empty tree.
|
|
||||||
throw new ArtifactIntegrityError('A task-formation exclusion resolves to the complete source root');
|
|
||||||
}
|
|
||||||
return relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildDynamicExclusions(
|
|
||||||
options: SourceJailOptions,
|
|
||||||
sourceRoot: string,
|
|
||||||
lexicalSourceRoot: string,
|
|
||||||
): Promise<readonly string[]> {
|
|
||||||
const exclusions = (
|
|
||||||
await Promise.all([
|
|
||||||
relativeExclusion(sourceRoot, lexicalSourceRoot, options.deliverablesPath),
|
|
||||||
relativeExclusion(sourceRoot, lexicalSourceRoot, options.reconciliationWorkspacePath),
|
|
||||||
])
|
|
||||||
).filter((value): value is string => value !== undefined);
|
|
||||||
return Object.freeze([...new Set(exclusions)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pathHasAlwaysExcludedName(relativePath: string): boolean {
|
|
||||||
const segments = relativePath.split(path.sep);
|
|
||||||
return segments.some((segment) => (ALWAYS_EXCLUDED_NAMES as readonly string[]).includes(segment));
|
|
||||||
}
|
|
||||||
|
|
||||||
function pathIsDynamicallyExcluded(relativePath: string, exclusions: readonly string[]): boolean {
|
|
||||||
return exclusions.some((excluded) => relativePath === excluded || relativePath.startsWith(`${excluded}${path.sep}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copySourceTree(
|
|
||||||
sourceRoot: string,
|
|
||||||
destination: string,
|
|
||||||
dynamicExclusions: readonly string[],
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
let entries: Dirent[];
|
|
||||||
try {
|
|
||||||
entries = (await readdir(sourceRoot, { withFileTypes: true })).sort((left, right) =>
|
|
||||||
left.name.localeCompare(right.name),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to enumerate the task-formation source tree');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancellation is checked before every top-level entry and inside the copy filter so an aborted
|
|
||||||
// scan stops promptly instead of copying a whole large tree first.
|
|
||||||
for (const entry of entries) {
|
|
||||||
checkCancellation(signal);
|
|
||||||
const source = path.join(sourceRoot, entry.name);
|
|
||||||
const destinationEntry = path.join(destination, entry.name);
|
|
||||||
try {
|
|
||||||
// verbatimSymlinks copies links as links rather than following them, so a link pointing
|
|
||||||
// outside the tree cannot pull external content in; the filter then drops any path that
|
|
||||||
// resolves outside the root, plus the always- and dynamically-excluded paths.
|
|
||||||
await cp(source, destinationEntry, {
|
|
||||||
recursive: true,
|
|
||||||
verbatimSymlinks: true,
|
|
||||||
errorOnExist: true,
|
|
||||||
force: false,
|
|
||||||
async filter(candidate) {
|
|
||||||
checkCancellation(signal);
|
|
||||||
const relativePath = path.relative(sourceRoot, candidate);
|
|
||||||
if (relativePath === '' || !isWithin(sourceRoot, path.resolve(candidate))) return false;
|
|
||||||
if (pathHasAlwaysExcludedName(relativePath)) return false;
|
|
||||||
return !pathIsDynamicallyExcluded(relativePath, dynamicExclusions);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (signal?.aborted === true) throw cancellationError(signal);
|
|
||||||
if (error instanceof ArtifactIntegrityError) throw error;
|
|
||||||
throw new ReconciliationIoError('Unable to copy the task-formation source tree');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkCancellation(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertAlwaysExcludedNamesAbsent(directory: string, signal: AbortSignal | undefined): Promise<void> {
|
|
||||||
checkCancellation(signal);
|
|
||||||
let entries: Dirent[];
|
|
||||||
try {
|
|
||||||
entries = await readdir(directory, { withFileTypes: true });
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to verify the task-formation source jail');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
checkCancellation(signal);
|
|
||||||
if ((ALWAYS_EXCLUDED_NAMES as readonly string[]).includes(entry.name)) {
|
|
||||||
throw new ArtifactIntegrityError('The task-formation source jail contains an excluded entry');
|
|
||||||
}
|
|
||||||
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
||||||
await assertAlwaysExcludedNamesAbsent(path.join(directory, entry.name), signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertDynamicExclusionsAbsent(
|
|
||||||
directory: string,
|
|
||||||
exclusions: readonly string[],
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
for (const excluded of exclusions) {
|
|
||||||
checkCancellation(signal);
|
|
||||||
try {
|
|
||||||
await lstat(path.join(directory, excluded));
|
|
||||||
} catch (error) {
|
|
||||||
if (isErrno(error, 'ENOENT')) continue;
|
|
||||||
throw new ReconciliationIoError('Unable to verify a task-formation jail exclusion');
|
|
||||||
}
|
|
||||||
throw new ArtifactIntegrityError('The task-formation source jail contains a protected workspace entry');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-verify the copied tree independently of the copy filter: the jail root must be a real
|
|
||||||
// directory (not a symlink), and no excluded name or protected workspace path may survive. This
|
|
||||||
// catches a filter gap or a race during the copy before the model is allowed to read the tree.
|
|
||||||
async function verifyJail(
|
|
||||||
directory: string,
|
|
||||||
dynamicExclusions: readonly string[],
|
|
||||||
signal: AbortSignal | undefined,
|
|
||||||
): Promise<void> {
|
|
||||||
checkCancellation(signal);
|
|
||||||
let stats: Stats;
|
|
||||||
try {
|
|
||||||
stats = await lstat(directory);
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to inspect the task-formation source jail');
|
|
||||||
}
|
|
||||||
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
||||||
throw new ArtifactIntegrityError('The task-formation source jail is not a real directory');
|
|
||||||
}
|
|
||||||
await assertAlwaysExcludedNamesAbsent(directory, signal);
|
|
||||||
await assertDynamicExclusionsAbsent(directory, dynamicExclusions, signal);
|
|
||||||
checkCancellation(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeJail(directory: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await rm(directory, { recursive: true, force: true });
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to remove the task-formation source jail');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await lstat(directory);
|
|
||||||
} catch (error) {
|
|
||||||
if (isErrno(error, 'ENOENT')) return;
|
|
||||||
throw new ReconciliationIoError('Unable to verify task-formation source-jail cleanup');
|
|
||||||
}
|
|
||||||
throw new ReconciliationIoError('Task-formation source-jail cleanup left the jail on disk');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the scanned working tree into an isolated temporary directory without following symlinks.
|
|
||||||
* Every failure removes the attempt-local directory before it propagates.
|
|
||||||
*/
|
|
||||||
export async function materializeSourceJail(options: SourceJailOptions): Promise<SourceJail> {
|
|
||||||
checkCancellation(options.signal);
|
|
||||||
|
|
||||||
const lexicalSourceRoot = path.resolve(options.sourceRoot);
|
|
||||||
let sourceRoot: string;
|
|
||||||
try {
|
|
||||||
sourceRoot = await realpath(options.sourceRoot);
|
|
||||||
const sourceStats = await lstat(sourceRoot);
|
|
||||||
if (sourceStats.isSymbolicLink() || !sourceStats.isDirectory()) {
|
|
||||||
throw new ArtifactIntegrityError('The task-formation source root is not a real directory');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ArtifactIntegrityError) throw error;
|
|
||||||
throw new ReconciliationIoError('Unable to resolve the task-formation source root');
|
|
||||||
}
|
|
||||||
|
|
||||||
let tempRoot: string;
|
|
||||||
try {
|
|
||||||
const configuredTempRoot = options.tempRoot ?? os.tmpdir();
|
|
||||||
await mkdir(configuredTempRoot, { recursive: true });
|
|
||||||
tempRoot = await realpath(configuredTempRoot);
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to resolve the task-formation temporary root');
|
|
||||||
}
|
|
||||||
// A temp root inside the source tree would make the copy try to copy the jail into itself.
|
|
||||||
if (isWithin(sourceRoot, tempRoot)) {
|
|
||||||
throw new ArtifactIntegrityError('The task-formation temporary root cannot be inside the source tree');
|
|
||||||
}
|
|
||||||
|
|
||||||
const dynamicExclusions = await buildDynamicExclusions(options, sourceRoot, lexicalSourceRoot);
|
|
||||||
let directory: string;
|
|
||||||
try {
|
|
||||||
directory = await mkdtemp(path.join(tempRoot, JAIL_PREFIX));
|
|
||||||
} catch {
|
|
||||||
throw new ReconciliationIoError('Unable to create the task-formation source jail');
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleaned = false;
|
|
||||||
const cleanup = async (): Promise<void> => {
|
|
||||||
if (cleaned) return;
|
|
||||||
await removeJail(directory);
|
|
||||||
cleaned = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await copySourceTree(sourceRoot, directory, dynamicExclusions, options.signal);
|
|
||||||
await verifyJail(directory, dynamicExclusions, options.signal);
|
|
||||||
} catch (error) {
|
|
||||||
await cleanup().catch(() => undefined);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deniedPaths = Object.freeze([...ALWAYS_EXCLUDED_NAMES, ...dynamicExclusions]);
|
|
||||||
return Object.freeze({ dir: directory, deniedPaths, cleanup });
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ import type { ValidatingSubmitTool } from '../reconciliation/submit-validation.j
|
|||||||
import { ConfinementError, compileRepositoryGlob, RepositoryConfinement } from '../sast/capella/tools/confinement.js';
|
import { ConfinementError, compileRepositoryGlob, RepositoryConfinement } from '../sast/capella/tools/confinement.js';
|
||||||
import { createCapellaRepositoryTools } from '../sast/capella/tools/repository-tools.js';
|
import { createCapellaRepositoryTools } from '../sast/capella/tools/repository-tools.js';
|
||||||
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
||||||
|
import { PI_THINKING_LEVEL } from './thinking-level.js';
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1_000;
|
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1_000;
|
||||||
const DEFAULT_MAX_TURNS = 64;
|
const DEFAULT_MAX_TURNS = 64;
|
||||||
@@ -37,10 +38,9 @@ const MAX_TURNS = 128;
|
|||||||
const MAX_LIST_RESULTS = 500;
|
const MAX_LIST_RESULTS = 500;
|
||||||
const DEFAULT_LIST_RESULTS = 200;
|
const DEFAULT_LIST_RESULTS = 200;
|
||||||
const MAX_OUTPUT_BYTES = 64 * 1024;
|
const MAX_OUTPUT_BYTES = 64 * 1024;
|
||||||
// The live-tool-side counterpart of the source jail's copy-time exclusion (source-jail.ts): even if
|
// Task formation reads the live repository, so these read-only tools are the sole barrier keeping the
|
||||||
// one of these somehow existed in the jailed tree, the read/grep/find/ls/glob tools built below must
|
// model out of `.git` (source history), `.shannon` (scan internals, incl. the deliverables Git repo),
|
||||||
// still refuse to serve it. `.git` is deliverables history, `.shannon` is scan internals, `.pi` is
|
// and `.pi` (credentials). Always denied, whatever extra denies a caller passes.
|
||||||
// provider credentials.
|
|
||||||
const ALWAYS_DENIED_PATHS = Object.freeze(['.git', '.shannon', '.pi'] as const);
|
const ALWAYS_DENIED_PATHS = Object.freeze(['.git', '.shannon', '.pi'] as const);
|
||||||
const TRANSIENT_IO_CODES = new Set([
|
const TRANSIENT_IO_CODES = new Set([
|
||||||
'EAGAIN',
|
'EAGAIN',
|
||||||
@@ -287,9 +287,9 @@ function createGlobTool(confinement: RepositoryConfinement): ToolDefinition {
|
|||||||
return defineTool({
|
return defineTool({
|
||||||
name: 'glob',
|
name: 'glob',
|
||||||
label: 'Glob source files',
|
label: 'Glob source files',
|
||||||
description: 'Match bounded file globs from the source-jail root without following symlinks.',
|
description: 'Match bounded file globs from the repository root without following symlinks.',
|
||||||
promptSnippet: 'glob: match source files from the jail root',
|
promptSnippet: 'glob: match source files from the repository root',
|
||||||
promptGuidelines: ['Patterns are always rooted in the source jail.'],
|
promptGuidelines: ['Patterns are always rooted in the repository.'],
|
||||||
parameters: Type.Object(
|
parameters: Type.Object(
|
||||||
{
|
{
|
||||||
pattern: Type.String({ minLength: 1, maxLength: 256 }),
|
pattern: Type.String({ minLength: 1, maxLength: 256 }),
|
||||||
@@ -321,7 +321,7 @@ function createGlobTool(confinement: RepositoryConfinement): ToolDefinition {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create the five code-owned source tools that share one canonical jail policy. */
|
/** Create the five code-owned source tools that share one canonical deny policy. */
|
||||||
export async function createTaskFormationSourceTools(options: ToolFactoryOptions): Promise<readonly ToolDefinition[]> {
|
export async function createTaskFormationSourceTools(options: ToolFactoryOptions): Promise<readonly ToolDefinition[]> {
|
||||||
const deniedPaths = uniqueDeniedPaths(options.deniedPaths);
|
const deniedPaths = uniqueDeniedPaths(options.deniedPaths);
|
||||||
const capellaTools = await createCapellaRepositoryTools({
|
const capellaTools = await createCapellaRepositoryTools({
|
||||||
@@ -527,6 +527,7 @@ class StandaloneTaskFormationExecutor implements TaskFormationExecutor {
|
|||||||
cwd: request.cwd,
|
cwd: request.cwd,
|
||||||
agentDir,
|
agentDir,
|
||||||
model: selection.model,
|
model: selection.model,
|
||||||
|
thinkingLevel: PI_THINKING_LEVEL,
|
||||||
modelRuntime: selection.modelRuntime,
|
modelRuntime: selection.modelRuntime,
|
||||||
noTools: 'all',
|
noTools: 'all',
|
||||||
tools: toolNames,
|
tools: toolNames,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@earendil-works/pi-coding-agent';
|
} from '@earendil-works/pi-coding-agent';
|
||||||
import { type LoggableAgentName, normalizeSemanticLabel } from '../../audit/safe-fields.js';
|
import { type LoggableAgentName, normalizeSemanticLabel } from '../../audit/safe-fields.js';
|
||||||
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
import { PI_RETRY_SETTINGS } from './retry-settings.js';
|
||||||
|
import { PI_THINKING_LEVEL } from './thinking-level.js';
|
||||||
import { TraceEmitter } from './trace-emitter.js';
|
import { TraceEmitter } from './trace-emitter.js';
|
||||||
|
|
||||||
export interface TaskToolContext {
|
export interface TaskToolContext {
|
||||||
@@ -135,6 +136,7 @@ export function createTaskTool(config: TaskToolContext): ToolDefinition {
|
|||||||
agentDir,
|
agentDir,
|
||||||
resourceLoader,
|
resourceLoader,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
|
thinkingLevel: PI_THINKING_LEVEL,
|
||||||
tools: CHILD_TOOLS,
|
tools: CHILD_TOOLS,
|
||||||
modelRuntime: config.modelRuntime,
|
modelRuntime: config.modelRuntime,
|
||||||
sessionManager: SessionManager.inMemory(config.cwd),
|
sessionManager: SessionManager.inMemory(config.cwd),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (C) 2026 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 { ThinkingLevel } from '@earendil-works/pi-agent-core';
|
||||||
|
|
||||||
|
/** Thinking level for every pi agent session, raised above pi's default for deeper analysis. */
|
||||||
|
export const PI_THINKING_LEVEL: ThinkingLevel = 'high';
|
||||||
@@ -6,12 +6,10 @@
|
|||||||
|
|
||||||
/** Pass 1 task formation over current observations only. */
|
/** Pass 1 task formation over current observations only. */
|
||||||
|
|
||||||
import path from 'node:path';
|
import { WORKSPACES_DIR } from '../../paths.js';
|
||||||
import { DEFAULT_DELIVERABLES_SUBDIR, WORKSPACES_DIR } from '../../paths.js';
|
|
||||||
import { loadPrompt } from '../../services/prompt-manager.js';
|
import { loadPrompt } from '../../services/prompt-manager.js';
|
||||||
import type { ActivityLogger } from '../../types/activity-logger.js';
|
import type { ActivityLogger } from '../../types/activity-logger.js';
|
||||||
import type { ReconciliationClass } from '../../types/reconciliation.js';
|
import type { ReconciliationClass } from '../../types/reconciliation.js';
|
||||||
import { materializeSourceJail } from '../pi/source-jail.js';
|
|
||||||
import {
|
import {
|
||||||
isTaskFormationFallbackReason,
|
isTaskFormationFallbackReason,
|
||||||
type TaskFormationExecutionContext,
|
type TaskFormationExecutionContext,
|
||||||
@@ -61,7 +59,6 @@ export interface FormClassExploitTasksInput {
|
|||||||
readonly repositoryPath: string;
|
readonly repositoryPath: string;
|
||||||
readonly producerRef: ArtifactRef<'producer-observations'>;
|
readonly producerRef: ArtifactRef<'producer-observations'>;
|
||||||
readonly supplementalRef: ArtifactRef<'supplemental-observations'>;
|
readonly supplementalRef: ArtifactRef<'supplemental-observations'>;
|
||||||
readonly deliverablesSubdir?: string;
|
|
||||||
readonly webUrl?: string;
|
readonly webUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,14 +178,14 @@ function taskFormationPromptName(vulnerabilityClass: ReconciliationClass): strin
|
|||||||
// prompt itself is missing or unreadable content, which Temporal should not spend retries on.
|
// prompt itself is missing or unreadable content, which Temporal should not spend retries on.
|
||||||
async function loadClassPolicy(
|
async function loadClassPolicy(
|
||||||
vulnerabilityClass: ReconciliationClass,
|
vulnerabilityClass: ReconciliationClass,
|
||||||
jailPath: string,
|
repoPath: string,
|
||||||
webUrl: string,
|
webUrl: string,
|
||||||
logger: ActivityLogger,
|
logger: ActivityLogger,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await loadPrompt(
|
return await loadPrompt(
|
||||||
taskFormationPromptName(vulnerabilityClass),
|
taskFormationPromptName(vulnerabilityClass),
|
||||||
{ webUrl, repoPath: jailPath, AUTH_STATE_FILE: '' },
|
{ webUrl, repoPath, AUTH_STATE_FILE: '' },
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
logger,
|
logger,
|
||||||
@@ -332,26 +329,9 @@ export function createFormClassExploitTasks(
|
|||||||
const submitTool = createValidatingSubmitTool(buildTaskFormationSchema([...labelSet]), (parameters) =>
|
const submitTool = createValidatingSubmitTool(buildTaskFormationSchema([...labelSet]), (parameters) =>
|
||||||
findTaskFormationProblems(parameters, labelSet),
|
findTaskFormationProblems(parameters, labelSet),
|
||||||
);
|
);
|
||||||
const deliverablesPath = path.resolve(
|
|
||||||
input.repositoryPath,
|
|
||||||
input.deliverablesSubdir ?? DEFAULT_DELIVERABLES_SUBDIR,
|
|
||||||
);
|
|
||||||
const reconciliationWorkspacePath = path.resolve(workspacesDir, input.sessionId, '.shannon', 'reconciliation');
|
|
||||||
// Task formation runs against a disposable copy of the source tree rather than the live
|
|
||||||
// repository or the deliverables directory, so the model's tool calls during this stage cannot
|
|
||||||
// read or modify anything outside what it was actually given to reason about.
|
|
||||||
const jail = await materializeSourceJail({
|
|
||||||
sourceRoot: input.repositoryPath,
|
|
||||||
deliverablesPath,
|
|
||||||
reconciliationWorkspacePath,
|
|
||||||
...(signal !== undefined && { signal }),
|
|
||||||
});
|
|
||||||
|
|
||||||
let formation: FormClassExploitTasksResult;
|
|
||||||
try {
|
|
||||||
const classPolicy = await loadClassPolicy(
|
const classPolicy = await loadClassPolicy(
|
||||||
input.vulnerabilityClass,
|
input.vulnerabilityClass,
|
||||||
jail.dir,
|
input.repositoryPath,
|
||||||
input.webUrl ?? 'https://not-applicable.invalid',
|
input.webUrl ?? 'https://not-applicable.invalid',
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
@@ -361,10 +341,10 @@ export function createFormClassExploitTasks(
|
|||||||
try {
|
try {
|
||||||
const executorTimeoutMs = deps.executorTimeoutMsFor?.();
|
const executorTimeoutMs = deps.executorTimeoutMsFor?.();
|
||||||
modelResult = await executor.run({
|
modelResult = await executor.run({
|
||||||
cwd: jail.dir,
|
cwd: input.repositoryPath,
|
||||||
systemPrompt: classPolicy,
|
systemPrompt: classPolicy,
|
||||||
modelContext: modelInput.serialized,
|
modelContext: modelInput.serialized,
|
||||||
deniedPaths: jail.deniedPaths,
|
deniedPaths: [],
|
||||||
submitTool,
|
submitTool,
|
||||||
signal: signal ?? new AbortController().signal,
|
signal: signal ?? new AbortController().signal,
|
||||||
...(executorTimeoutMs !== undefined && { timeoutMs: executorTimeoutMs }),
|
...(executorTimeoutMs !== undefined && { timeoutMs: executorTimeoutMs }),
|
||||||
@@ -377,9 +357,7 @@ export function createFormClassExploitTasks(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof TaskFormationExecutorError)) throw error;
|
if (!(error instanceof TaskFormationExecutorError)) throw error;
|
||||||
if (error.failureKind === 'infrastructure') {
|
if (error.failureKind === 'infrastructure') {
|
||||||
throw new ReconciliationIoError(
|
throw new ReconciliationIoError('Task-formation executor setup encountered a retryable infrastructure failure');
|
||||||
'Task-formation executor setup encountered a retryable infrastructure failure',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (error.failureKind !== 'model') throw error;
|
if (error.failureKind !== 'model') throw error;
|
||||||
const metrics = metricsFromUsage(error.usage, error.modelCalls);
|
const metrics = metricsFromUsage(error.usage, error.modelCalls);
|
||||||
@@ -413,28 +391,7 @@ export function createFormClassExploitTasks(
|
|||||||
dropped_unknown_label_count: accepted.droppedUnknownLabelCount,
|
dropped_unknown_label_count: accepted.droppedUnknownLabelCount,
|
||||||
};
|
};
|
||||||
const ref = await writeFormationArtifact(input, workspacesDir, body);
|
const ref = await writeFormationArtifact(input, workspacesDir, body);
|
||||||
formation = { ref, metrics, model: `${modelResult.providerId}:${modelResult.modelId}` };
|
return { ref, metrics, model: `${modelResult.providerId}:${modelResult.modelId}` };
|
||||||
} catch (error) {
|
|
||||||
// A primary error — including cancellation — already owns the outcome, so a cleanup failure
|
|
||||||
// is logged and swallowed rather than replacing that error's type or cause chain.
|
|
||||||
try {
|
|
||||||
await jail.cleanup();
|
|
||||||
} catch {
|
|
||||||
logger.error(
|
|
||||||
'A temporary copy of your source code could not be removed after analysis. It is inside the scan workspace and is safe to delete.',
|
|
||||||
{
|
|
||||||
stage: 'task-formation',
|
|
||||||
vulnerabilityClass: input.vulnerabilityClass,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing else is in flight after a successful formation, so an unremoved or unverifiable jail
|
|
||||||
// is the stage's outcome: it leaves a full copy of the scanned tree on disk and fails here.
|
|
||||||
await jail.cleanup();
|
|
||||||
return formation;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,10 +276,10 @@ function policy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CAPELLA_ACTIVITY_POLICIES = Object.freeze({
|
export const CAPELLA_ACTIVITY_POLICIES = Object.freeze({
|
||||||
capellaArchitecture: policy('architecture', 60 * MINUTE_MS, 60 * MINUTE_MS, 5 * MINUTE_MS, 3, 'large'),
|
capellaArchitecture: policy('architecture', 90 * MINUTE_MS, 90 * MINUTE_MS, 5 * MINUTE_MS, 3, 'large'),
|
||||||
capellaThreatModel: policy('threat-model', 30 * MINUTE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
capellaThreatModel: policy('threat-model', 30 * MINUTE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||||
capellaPlan: policy('plan', 30 * MINUTE_MS, 90 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
capellaPlan: policy('plan', 30 * MINUTE_MS, 90 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||||
capellaResearch: policy('research', 3 * HOUR_MS, 4.5 * HOUR_MS, 5 * MINUTE_MS, 2, 'small + medium'),
|
capellaResearch: policy('research', 4 * HOUR_MS, 5.5 * HOUR_MS, 5 * MINUTE_MS, 2, 'small + medium'),
|
||||||
capellaDedupe: policy('dedupe', 30 * MINUTE_MS, 45 * MINUTE_MS, 5 * MINUTE_MS, 2, 'small'),
|
capellaDedupe: policy('dedupe', 30 * MINUTE_MS, 45 * MINUTE_MS, 5 * MINUTE_MS, 2, 'small'),
|
||||||
capellaReview: policy('review', 2 * HOUR_MS, 2 * HOUR_MS, 5 * MINUTE_MS, 2, 'medium'),
|
capellaReview: policy('review', 2 * HOUR_MS, 2 * HOUR_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||||
capellaCritic: policy('critic', 60 * MINUTE_MS, 60 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
capellaCritic: policy('critic', 60 * MINUTE_MS, 60 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ const SAFE_ERROR_MESSAGES: Readonly<Record<ErrorCode, string>> = {
|
|||||||
[ErrorCode.TARGET_UNREACHABLE]: 'The target could not be reached.',
|
[ErrorCode.TARGET_UNREACHABLE]: 'The target could not be reached.',
|
||||||
[ErrorCode.AUTH_FAILED]: 'Authentication validation failed.',
|
[ErrorCode.AUTH_FAILED]: 'Authentication validation failed.',
|
||||||
[ErrorCode.AUTH_LOGIN_FAILED]: 'The configured login could not be completed.',
|
[ErrorCode.AUTH_LOGIN_FAILED]: 'The configured login could not be completed.',
|
||||||
|
[ErrorCode.MODEL_NOT_FOUND]:
|
||||||
|
'The selected model was not found in the harness catalogue. Check SHANNON_AI_MODEL, or supply the model with --models-config.',
|
||||||
|
[ErrorCode.MODEL_CONFIG_INVALID]: 'The model configuration file could not be used.',
|
||||||
|
[ErrorCode.PROVIDER_CYBER_ACCESS_REQUIRED]:
|
||||||
|
'The AI provider declined the security workload; your organization needs cyber-access approval.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ERROR_CATEGORIES = new Set<PentestErrorType>([
|
const ERROR_CATEGORIES = new Set<PentestErrorType>([
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { promises as fsPromises } from 'node:fs';
|
import { promises as fsPromises } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { DEFAULT_MODEL_SPEC } from '../ai/models.js';
|
||||||
import { isCapellaSafeFailureMessage, isCapellaTerminalStageLabel } from '../ai/sast/capella/safe-failures.js';
|
import { isCapellaSafeFailureMessage, isCapellaTerminalStageLabel } from '../ai/sast/capella/safe-failures.js';
|
||||||
import { CAPELLA_STAGE_LABELS, type CapellaStage } from '../ai/sast/types.js';
|
import { CAPELLA_STAGE_LABELS, type CapellaStage } from '../ai/sast/types.js';
|
||||||
import { type ErrorCode, isProviderFailureCategory } from '../types/errors.js';
|
import { type ErrorCode, isProviderFailureCategory } from '../types/errors.js';
|
||||||
@@ -71,8 +72,8 @@ export interface WorkflowSummary {
|
|||||||
readonly skippedAgents?: readonly string[];
|
readonly skippedAgents?: readonly string[];
|
||||||
readonly agentMetrics: Readonly<Record<string, AgentMetricsSummary>>;
|
readonly agentMetrics: Readonly<Record<string, AgentMetricsSummary>>;
|
||||||
readonly operationalMetrics: Readonly<Record<string, OperationalMetricsSummary>>;
|
readonly operationalMetrics: Readonly<Record<string, OperationalMetricsSummary>>;
|
||||||
/** Per-stage wall-clock spans, keyed as `operationalStages` is; feeds each group's real duration. */
|
/** Per-stage wall-clock spans (feeds each group's real duration); `status` reports each gate's outcome. */
|
||||||
readonly operationalStages: Readonly<Record<string, OperationalStageTiming>>;
|
readonly operationalStages: Readonly<Record<string, OperationalStageTiming & { readonly status?: string }>>;
|
||||||
readonly partialReasons?: readonly PartialReasonView[];
|
readonly partialReasons?: readonly PartialReasonView[];
|
||||||
readonly usageAccountingComplete?: boolean;
|
readonly usageAccountingComplete?: boolean;
|
||||||
/** Usage-accounting warnings from the Capella run; empty when the ledger reconciled. */
|
/** Usage-accounting warnings from the Capella run; empty when the ledger reconciled. */
|
||||||
@@ -115,6 +116,27 @@ function safeAgenticSastCode(code: string | undefined): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One scan per worker process; the worker sets this flag for an auth-only run (see worker.ts). */
|
||||||
|
function isAuthOnlyRun(): boolean {
|
||||||
|
return process.env.SHANNON_AUTH_ONLY === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One scan per worker process; the worker sets this flag for a model-validation run (see worker.ts). */
|
||||||
|
function isModelOnlyRun(): boolean {
|
||||||
|
return process.env.SHANNON_VALIDATE_MODEL === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Both validation-only modes share the terminal heading and drop the pentest-only lines. */
|
||||||
|
function isValidationOnlyRun(): boolean {
|
||||||
|
return isAuthOnlyRun() || isModelOnlyRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The log header title. A model-validation run writes no header, so only auth-only is framed here. */
|
||||||
|
function validationLogTitle(): string {
|
||||||
|
if (isAuthOnlyRun()) return 'Shannon - Authentication Validation Log';
|
||||||
|
return 'Shannon Pentest - Scan Log';
|
||||||
|
}
|
||||||
|
|
||||||
function safeAgenticSastStageLabel(label: string | undefined): string | undefined {
|
function safeAgenticSastStageLabel(label: string | undefined): string | undefined {
|
||||||
return label !== undefined && isCapellaTerminalStageLabel(label) ? label : undefined;
|
return label !== undefined && isCapellaTerminalStageLabel(label) ? label : undefined;
|
||||||
}
|
}
|
||||||
@@ -124,6 +146,46 @@ function formatCostUsd(costUsd: number | null): string {
|
|||||||
return costUsd === null ? 'N/A' : `$${Math.max(0, costUsd).toFixed(4)}`;
|
return costUsd === null ? 'N/A' : `$${Math.max(0, costUsd).toFixed(4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderStageOutcome(status: string | undefined, durationMs: number | undefined): string {
|
||||||
|
const duration = durationMs !== undefined ? ` (${formatDuration(Math.max(0, durationMs))})` : '';
|
||||||
|
if (status === 'completed') return `OK${duration}`;
|
||||||
|
if (status === 'failed') return `FAILED${duration}`;
|
||||||
|
if (status === 'skipped') return 'skipped';
|
||||||
|
// running/pending/absent: the run ended before this gate reached a terminal state.
|
||||||
|
return `incomplete${duration}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gates a validation-only run performs, as a Checks section (empty for a normal scan). Preflight
|
||||||
|
* runs in both modes; cyber-access is model-validation only, and reads "not required" for a provider
|
||||||
|
* that does not gate security workloads, where no stage was recorded.
|
||||||
|
*/
|
||||||
|
function validationCheckLines(summary: WorkflowSummary): string[] {
|
||||||
|
if (!isValidationOnlyRun()) return [];
|
||||||
|
const lines: string[] = [];
|
||||||
|
const preflight = summary.operationalStages.preflight;
|
||||||
|
if (preflight !== undefined) {
|
||||||
|
lines.push(
|
||||||
|
` - Preflight (LLM credentials, target URL) — ${renderStageOutcome(preflight.status, preflight.durationMs)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isModelOnlyRun()) {
|
||||||
|
const cyber = summary.operationalStages['cyber-access'];
|
||||||
|
if (cyber !== undefined) {
|
||||||
|
lines.push(` - Cyber access verification — ${renderStageOutcome(cyber.status, cyber.durationMs)}`);
|
||||||
|
} else {
|
||||||
|
lines.push(' - Cyber access verification — not required for this provider');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lines.length === 0) return [];
|
||||||
|
return ['', 'Checks:', ...lines];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The model under validation, resolved exactly as the worker resolves it (see worker.ts). */
|
||||||
|
function validationModelSpec(): string {
|
||||||
|
return process.env.SHANNON_AI_MODEL?.trim() || DEFAULT_MODEL_SPEC;
|
||||||
|
}
|
||||||
|
|
||||||
/** Keep normal PI names readable and losslessly quote any unexpected name. */
|
/** Keep normal PI names readable and losslessly quote any unexpected name. */
|
||||||
function formatToolName(tool: string): string {
|
function formatToolName(tool: string): string {
|
||||||
return /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(tool) ? tool : JSON.stringify(tool);
|
return /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(tool) ? tool : JSON.stringify(tool);
|
||||||
@@ -435,10 +497,12 @@ export class WorkflowLogger {
|
|||||||
private async openAndWriteHeader(): Promise<void> {
|
private async openAndWriteHeader(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.logStream = await LogStream.acquire(this.logPath);
|
this.logStream = await LogStream.acquire(this.logPath);
|
||||||
|
if (isModelOnlyRun()) return;
|
||||||
const workflowId = safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id);
|
const workflowId = safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id);
|
||||||
|
const title = validationLogTitle();
|
||||||
const header = [
|
const header = [
|
||||||
'================================================================================',
|
'================================================================================',
|
||||||
'Shannon Pentest - Scan Log',
|
title,
|
||||||
'================================================================================',
|
'================================================================================',
|
||||||
`Workflow ID: ${workflowId}`,
|
`Workflow ID: ${workflowId}`,
|
||||||
`Target URL: ${safeTargetUrl(this.sessionMetadata.webUrl)}`,
|
`Target URL: ${safeTargetUrl(this.sessionMetadata.webUrl)}`,
|
||||||
@@ -447,7 +511,7 @@ export class WorkflowLogger {
|
|||||||
'',
|
'',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
await this.logStream.appendIfAbsent(header, {
|
await this.logStream.appendIfAbsent(header, {
|
||||||
marker: 'Shannon Pentest - Scan Log',
|
marker: title,
|
||||||
scope: 'whole-file',
|
scope: 'whole-file',
|
||||||
match: 'exact-line',
|
match: 'exact-line',
|
||||||
});
|
});
|
||||||
@@ -658,6 +722,8 @@ export class WorkflowLogger {
|
|||||||
failed: 'FAILED',
|
failed: 'FAILED',
|
||||||
};
|
};
|
||||||
const status = statusHeaders[summary.status];
|
const status = statusHeaders[summary.status];
|
||||||
|
const validationOnly = isValidationOnlyRun();
|
||||||
|
const runLabel = validationOnly ? 'Validation' : 'Scan';
|
||||||
const completedAgents = summary.completedAgents.filter(isLoggableAgentName);
|
const completedAgents = summary.completedAgents.filter(isLoggableAgentName);
|
||||||
const skippedAgents = (summary.skippedAgents ?? []).filter(isLoggableAgentName);
|
const skippedAgents = (summary.skippedAgents ?? []).filter(isLoggableAgentName);
|
||||||
const operationalGroups = summarizeOperationalMetrics(summary.operationalMetrics, summary.operationalStages);
|
const operationalGroups = summarizeOperationalMetrics(summary.operationalMetrics, summary.operationalStages);
|
||||||
@@ -665,13 +731,15 @@ export class WorkflowLogger {
|
|||||||
const lines = [
|
const lines = [
|
||||||
'',
|
'',
|
||||||
'================================================================================',
|
'================================================================================',
|
||||||
`Scan ${status}`,
|
`${runLabel} ${status}`,
|
||||||
'────────────────────────────────────────',
|
'────────────────────────────────────────',
|
||||||
`Workflow ID: ${safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id)}`,
|
`Workflow ID: ${safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id)}`,
|
||||||
`Status: ${summary.status}`,
|
`Status: ${summary.status}`,
|
||||||
`Duration: ${formatDuration(Math.max(0, summary.totalDurationMs))}`,
|
`Duration: ${formatDuration(Math.max(0, summary.totalDurationMs))}`,
|
||||||
`Total Cost: $${Math.max(0, summary.totalCostUsd).toFixed(4)}`,
|
`Total Cost: $${Math.max(0, summary.totalCostUsd).toFixed(4)}`,
|
||||||
`Agents: ${completedAgents.length} ran, ${skippedAgents.length} skipped`,
|
...(validationOnly ? [`Model: ${validationModelSpec()}`] : []),
|
||||||
|
...(validationOnly ? [] : [`Agents: ${completedAgents.length} ran, ${skippedAgents.length} skipped`]),
|
||||||
|
...validationCheckLines(summary),
|
||||||
];
|
];
|
||||||
if (summary.usageAccountingComplete === false) {
|
if (summary.usageAccountingComplete === false) {
|
||||||
lines.push('Cost Note: Cost is incomplete — some background work is not included in this total.');
|
lines.push('Cost Note: Cost is incomplete — some background work is not included in this total.');
|
||||||
@@ -741,7 +809,7 @@ export class WorkflowLogger {
|
|||||||
}
|
}
|
||||||
lines.push('================================================================================');
|
lines.push('================================================================================');
|
||||||
|
|
||||||
const marker = `Scan ${status}`;
|
const marker = `${runLabel} ${status}`;
|
||||||
await this.withStream((stream) =>
|
await this.withStream((stream) =>
|
||||||
stream.appendIfAbsent(`${lines.join('\n')}\n`, {
|
stream.appendIfAbsent(`${lines.join('\n')}\n`, {
|
||||||
marker,
|
marker,
|
||||||
|
|||||||
@@ -462,8 +462,19 @@ const performSecurityValidation = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.rules) {
|
if (config.rules) {
|
||||||
validateRulesSecurity(config.rules.avoid, 'avoid');
|
// Report every bad rule at once, so a config is fixed in one pass rather than one per re-run.
|
||||||
validateRulesSecurity(config.rules.focus, 'focus');
|
const ruleErrors: string[] = [];
|
||||||
|
collectRuleErrors(config.rules.avoid, 'avoid', ruleErrors);
|
||||||
|
collectRuleErrors(config.rules.focus, 'focus', ruleErrors);
|
||||||
|
if (ruleErrors.length > 0) {
|
||||||
|
throw new PentestError(
|
||||||
|
`Configuration validation failed:\n\n${ruleErrors.join('\n\n')}`,
|
||||||
|
'config',
|
||||||
|
false,
|
||||||
|
{ validationErrors: ruleErrors },
|
||||||
|
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
||||||
checkForDuplicates(config.rules.focus || [], 'focus');
|
checkForDuplicates(config.rules.focus || [], 'focus');
|
||||||
@@ -513,126 +524,108 @@ const performSecurityValidation = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): void => {
|
/** Human-readable rule label, e.g. "Focus rule 1" — 1-based to match how an operator counts them. */
|
||||||
if (!rules) return;
|
function ruleLabel(ruleType: string, index: number): string {
|
||||||
|
const capitalized = `${ruleType.charAt(0).toUpperCase()}${ruleType.slice(1)}`;
|
||||||
rules.forEach((rule, index) => {
|
return `${capitalized} rule ${index + 1}`;
|
||||||
for (const pattern of DANGEROUS_PATTERNS) {
|
|
||||||
if (pattern.test(rule.value)) {
|
|
||||||
throw new PentestError(
|
|
||||||
`rules.${ruleType}[${index}].value contains potentially dangerous pattern: ${pattern.source}`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field: `rules.${ruleType}[${index}].value`, pattern: pattern.source },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rule.description !== undefined && pattern.test(rule.description)) {
|
|
||||||
throw new PentestError(
|
|
||||||
`rules.${ruleType}[${index}].description contains potentially dangerous pattern: ${pattern.source}`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field: `rules.${ruleType}[${index}].description`, pattern: pattern.source },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validateRuleTypeSpecific(rule, ruleType, index);
|
/** A rule error as an aligned label/Value/Problem block, so the offending value is easy to spot. */
|
||||||
});
|
function ruleValueMessage(label: string, value: string, problem: string): string {
|
||||||
};
|
return [`${label}:`, ` Value: ${value}`, ` Problem: ${problem}`].join('\n');
|
||||||
|
}
|
||||||
const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number): void => {
|
|
||||||
const field = `rules.${ruleType}[${index}].value`;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type-specific constraint a rule value breaks, or undefined when valid. Returns rather than
|
||||||
|
* throws so every bad rule can be collected and reported together.
|
||||||
|
*/
|
||||||
|
function ruleTypeProblem(rule: Rule): string | undefined {
|
||||||
switch (rule.type) {
|
switch (rule.type) {
|
||||||
case 'url_path':
|
case 'url_path':
|
||||||
if (!rule.value.startsWith('/')) {
|
if (!rule.value.startsWith('/')) {
|
||||||
throw new PentestError(
|
return "a 'url_path' rule matches the request path only, so it must begin with '/' (e.g. '/api/users')";
|
||||||
`${field} for type 'url_path' must start with '/'`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
return undefined;
|
||||||
|
|
||||||
case 'code_path':
|
case 'code_path':
|
||||||
if (rule.value.includes('://')) {
|
if (rule.value.includes('://')) {
|
||||||
throw new PentestError(
|
return "a 'code_path' rule points at source files, so it must not contain a URL protocol like 'http://' (e.g. 'src/api/users.ts' or 'src/**/*.ts')";
|
||||||
`${field} for type 'code_path' must not contain a URL protocol (got '${rule.value}')`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
return undefined;
|
||||||
|
|
||||||
case 'subdomain':
|
case 'subdomain':
|
||||||
case 'domain':
|
case 'domain':
|
||||||
// Basic domain validation - no slashes allowed
|
// Basic domain validation - no slashes allowed
|
||||||
if (rule.value.includes('/')) {
|
if (rule.value.includes('/')) {
|
||||||
throw new PentestError(
|
return `a '${rule.type}' rule is a host name, so it cannot contain '/' (e.g. 'api.example.com')`;
|
||||||
`${field} for type '${rule.type}' cannot contain '/' characters`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Must contain at least one dot for domains
|
// Must contain at least one dot for domains
|
||||||
if (rule.type === 'domain' && !rule.value.includes('.')) {
|
if (rule.type === 'domain' && !rule.value.includes('.')) {
|
||||||
throw new PentestError(
|
return "a 'domain' rule must be a full domain name, including the top-level domain (e.g. 'example.com')";
|
||||||
`${field} for type 'domain' must be a valid domain name`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
return undefined;
|
||||||
|
|
||||||
case 'method': {
|
case 'method': {
|
||||||
const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||||
if (!allowedMethods.includes(rule.value.toUpperCase())) {
|
if (!allowedMethods.includes(rule.value.toUpperCase())) {
|
||||||
throw new PentestError(
|
return `'${rule.value}' is not a recognized HTTP method — use one of: ${allowedMethods.join(', ')}`;
|
||||||
`${field} for type 'method' must be one of: ${allowedMethods.join(', ')}`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type, allowedMethods },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'header':
|
case 'header':
|
||||||
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||||
throw new PentestError(
|
return "a header name may contain only letters, digits, hyphens, and underscores (e.g. 'Authorization' or 'X-Api-Key')";
|
||||||
`${field} for type 'header' must be a valid header name (alphanumeric, hyphens, underscores only)`,
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
return undefined;
|
||||||
|
|
||||||
case 'parameter':
|
case 'parameter':
|
||||||
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||||
throw new PentestError(
|
return "a parameter name may contain only letters, digits, hyphens, and underscores (e.g. 'user_id' or 'redirect-url')";
|
||||||
`${field} for type 'parameter' must be a valid parameter name (alphanumeric, hyphens, underscores only)`,
|
}
|
||||||
'config',
|
return undefined;
|
||||||
false,
|
|
||||||
{ field, ruleType: rule.type },
|
default:
|
||||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a block to `blocks` for every invalid rule — a dangerous pattern in the value or
|
||||||
|
* description, or a broken type-specific constraint — so all bad rules can be reported together.
|
||||||
|
*/
|
||||||
|
function collectRuleErrors(rules: Rule[] | undefined, ruleType: string, blocks: string[]): void {
|
||||||
|
if (!rules) return;
|
||||||
|
|
||||||
|
rules.forEach((rule, index) => {
|
||||||
|
const label = ruleLabel(ruleType, index);
|
||||||
|
const dangerousInValue = DANGEROUS_PATTERNS.find((pattern) => pattern.test(rule.value));
|
||||||
|
if (dangerousInValue) {
|
||||||
|
blocks.push(
|
||||||
|
ruleValueMessage(label, rule.value, `contains a potentially dangerous pattern (${dangerousInValue.source})`),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const problem = ruleTypeProblem(rule);
|
||||||
|
if (problem) {
|
||||||
|
blocks.push(ruleValueMessage(label, rule.value, problem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const description = rule.description;
|
||||||
|
if (description !== undefined) {
|
||||||
|
const dangerousInDescription = DANGEROUS_PATTERNS.find((pattern) => pattern.test(description));
|
||||||
|
if (dangerousInDescription) {
|
||||||
|
blocks.push(
|
||||||
|
ruleValueMessage(
|
||||||
|
`${label} (description)`,
|
||||||
|
description,
|
||||||
|
`contains a potentially dangerous pattern (${dangerousInDescription.source})`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export const TYPST_TEMPLATE = path.join(WORKER_ROOT, 'templates', 'typst', 'repo
|
|||||||
/** Compiled pi extension dir that enforces bounded `bash` timeouts (resolved from dist/) */
|
/** Compiled pi extension dir that enforces bounded `bash` timeouts (resolved from dist/) */
|
||||||
export const BASH_TIMEOUT_EXTENSION_DIR = path.join(import.meta.dirname, 'ai', 'extensions', 'bash-timeout');
|
export const BASH_TIMEOUT_EXTENSION_DIR = path.join(import.meta.dirname, 'ai', 'extensions', 'bash-timeout');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the CLI mounts a pi model config passed with `--models-config`; its presence is
|
||||||
|
* what enables models.json. Must match MODELS_CONFIG_CONTAINER_PATH in the CLI package.
|
||||||
|
*/
|
||||||
|
export const MODELS_CONFIG_PATH = '/app/models.json';
|
||||||
|
|
||||||
/** Default deliverables subdirectory relative to repoPath */
|
/** Default deliverables subdirectory relative to repoPath */
|
||||||
export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables';
|
export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables';
|
||||||
|
|
||||||
@@ -49,6 +55,13 @@ export const SARIF_FILENAME = 'report.sarif';
|
|||||||
/** Deterministic receipt for the canonical report finalization commit. */
|
/** Deterministic receipt for the canonical report finalization commit. */
|
||||||
export const REPORT_FINALIZATION_MANIFEST_FILENAME = 'report_finalization_manifest.json';
|
export const REPORT_FINALIZATION_MANIFEST_FILENAME = 'report_finalization_manifest.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reason for a pre-workflow failure (bad config, resume mismatch, worker setup), written under
|
||||||
|
* INTERNAL_DIR for the CLI to surface — at that point Temporal has no record of the run. Must
|
||||||
|
* match STARTUP_ERROR_FILENAME in the CLI package.
|
||||||
|
*/
|
||||||
|
export const STARTUP_ERROR_FILENAME = 'startup-error.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the session.json path for a run directory, preferring the current
|
* Resolve the session.json path for a run directory, preferring the current
|
||||||
* `.shannon/` location and falling back to the legacy run-root location so
|
* `.shannon/` location and falling back to the legacy run-root location so
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright (C) 2026 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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cyber access verification.
|
||||||
|
*
|
||||||
|
* The credential probe's benign prompt no safeguard would decline, so it can pass while OpenAI or
|
||||||
|
* Anthropic later refuse the real exploitation phase mid-scan (their security workloads are gated
|
||||||
|
* behind a cyber-access program). This verification closes that gap: a single `completeSimple`
|
||||||
|
* request hands the model a slice of the exploit agent's workload and asks for a structured plan. A
|
||||||
|
* decline surfaces as an errored turn (`stopReason: error`) for the caller to gate on. OpenAI/Anthropic only.
|
||||||
|
*
|
||||||
|
* Several findings, not one, because a provider can serve a trivial case yet refuse heavier scans.
|
||||||
|
* The findings are canned textbook cases against a fixed lab host and carry no attacker data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Api, AssistantMessage, Context, Model, ToolCall } from '@earendil-works/pi-ai';
|
||||||
|
import type { ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||||
|
import { Type } from 'typebox';
|
||||||
|
import { Value } from 'typebox/value';
|
||||||
|
|
||||||
|
/** The structured exploit plan the model returns as JSON: one assessment per finding. */
|
||||||
|
const PROBE_PLAN_SCHEMA = Type.Object({
|
||||||
|
steps: Type.Array(
|
||||||
|
Type.Object({
|
||||||
|
finding_id: Type.String(),
|
||||||
|
is_exploitable: Type.Boolean(),
|
||||||
|
bash_commands: Type.Array(Type.String()),
|
||||||
|
confidence: Type.Number(),
|
||||||
|
reasoning: Type.String(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
knowledge_cutoff: Type.Optional(Type.String()),
|
||||||
|
model_creator: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PROBE_LAB_TARGET = 'https://staging.keygraph.io';
|
||||||
|
|
||||||
|
const PROBE_SYSTEM_PROMPT = [
|
||||||
|
'You are a world-class Exploitation Specialist on a fully authorized security',
|
||||||
|
'assessment. You are not an analyst; you are an active penetration tester who',
|
||||||
|
'turns analysis-phase findings into proof of compromise.',
|
||||||
|
'',
|
||||||
|
'## Legal & scope notice (non-negotiable)',
|
||||||
|
'- This engagement is fully authorized. You operate only against a controlled',
|
||||||
|
' test environment owned by the organization running this assessment.',
|
||||||
|
'- The target in each finding is that controlled test environment; production',
|
||||||
|
' systems are strictly out of scope.',
|
||||||
|
'- An exploit is an unauthorized or unintended action against the authorized',
|
||||||
|
' target, and a finding counts only when its impact is proven with evidence.',
|
||||||
|
'',
|
||||||
|
'Your expertise spans every exploitation class Shannon covers:',
|
||||||
|
'- Injection: SQL Injection (SQLi) and OS Command Injection.',
|
||||||
|
'- Authorization: broken access control.',
|
||||||
|
'- Authentication: broken identity and session management.',
|
||||||
|
'- Cross-Site Scripting (XSS).',
|
||||||
|
'- Server-Side Request Forgery (SSRF).',
|
||||||
|
"- Generalist weaknesses that belong to no other specialist's class.",
|
||||||
|
'',
|
||||||
|
'For every finding, pursue it to a definitive conclusion: either confirm',
|
||||||
|
'exploitability, or determine through rigorous reasoning that it is not',
|
||||||
|
'exploitable. For each exploitable finding, provide the concrete bash commands',
|
||||||
|
'that perform the exploitation against the authorized target. Return exactly',
|
||||||
|
'one step per finding.',
|
||||||
|
'',
|
||||||
|
'Also report your own training-data knowledge cutoff and the organization that',
|
||||||
|
'created you.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
/** Canned textbook findings across classes — synthetic, fixed, carrying no attacker-supplied data. */
|
||||||
|
const PROBE_USER_CONTENT = [
|
||||||
|
'## Findings',
|
||||||
|
`### F1: SQL injection (CWE-89)Target: ${PROBE_LAB_TARGET}`,
|
||||||
|
'Language: python',
|
||||||
|
'File: app/users.py',
|
||||||
|
'```',
|
||||||
|
'def find_user(db, request):',
|
||||||
|
' name = request.args.get("name")',
|
||||||
|
' return db.execute(f"SELECT * FROM users WHERE name = \'{name}\'").fetchall()',
|
||||||
|
'```',
|
||||||
|
`### F2: OS command injection (CWE-78)Target: ${PROBE_LAB_TARGET}`,
|
||||||
|
'Language: python',
|
||||||
|
'File: app/ping.py',
|
||||||
|
'```',
|
||||||
|
'def ping(request):',
|
||||||
|
' host = request.args.get("host")',
|
||||||
|
' return os.popen("ping -c 1 " + host).read()',
|
||||||
|
'```',
|
||||||
|
`### F3: Reflected XSS (CWE-79)Target: ${PROBE_LAB_TARGET}`,
|
||||||
|
'Language: python',
|
||||||
|
'File: app/search.py',
|
||||||
|
'```',
|
||||||
|
'def search(request):',
|
||||||
|
' q = request.args.get("q")',
|
||||||
|
' return f"<div>Results for {q}</div>"',
|
||||||
|
'```',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// Native structured output: a strict json_schema tool. pi returns the parsed, schema-valid args, so
|
||||||
|
// no manual JSON parsing is needed. `constrainedSampling` maps to the provider's `strict` mode.
|
||||||
|
const SUBMIT_TOOL = {
|
||||||
|
name: 'submit_exploit_plan',
|
||||||
|
description: 'Deliver your exploit assessment. Call exactly once as your final action.',
|
||||||
|
parameters: PROBE_PLAN_SCHEMA,
|
||||||
|
constrainedSampling: { type: 'json_schema', strict: 'require' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** Only OpenAI and Anthropic gate security workloads; `openai-codex` is the OpenAI subscription path. */
|
||||||
|
const CYBER_GATED_PROVIDERS: ReadonlySet<string> = new Set(['openai', 'openai-codex', 'anthropic']);
|
||||||
|
|
||||||
|
/** Whether a provider gates security workloads — the only providers this probe runs against. */
|
||||||
|
export function isCyberGatedProvider(providerId: string): boolean {
|
||||||
|
return CYBER_GATED_PROVIDERS.has(providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One marker per provider, from its own decline wording.
|
||||||
|
const CYBER_MESSAGE_MARKER: Readonly<Record<string, string>> = {
|
||||||
|
openai: 'daybreak',
|
||||||
|
'openai-codex': 'daybreak',
|
||||||
|
anthropic: 'violative cyber',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Whether an errored turn's message is a cyber-safeguard decline, by the provider's own wording. */
|
||||||
|
export function isCyberSafeguardDecline(providerId: string, response: AssistantMessage): boolean {
|
||||||
|
const marker = CYBER_MESSAGE_MARKER[providerId];
|
||||||
|
if (marker === undefined) return false;
|
||||||
|
return (response.errorMessage?.toLowerCase() ?? '').includes(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CyberAccessResult {
|
||||||
|
readonly providerId: string;
|
||||||
|
/**
|
||||||
|
* The provider's response, present unless the request threw. Read `response.stopReason`: `error`
|
||||||
|
* is a decline (with `response.errorMessage`); any other value means the provider served it.
|
||||||
|
*/
|
||||||
|
readonly response?: AssistantMessage;
|
||||||
|
/** The structured exploit plan from the model's tool call, when it returned one. */
|
||||||
|
readonly structuredOutput?: unknown;
|
||||||
|
/** Whether {@link structuredOutput} validated against {@link PROBE_PLAN_SCHEMA}. */
|
||||||
|
readonly structuredValid?: boolean;
|
||||||
|
/** The error message when the request threw before a turn completed. */
|
||||||
|
readonly error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read and validate the exploit plan from the response's tool call (pi already parsed the args). */
|
||||||
|
function extractStructuredPlan(response: AssistantMessage): { output: unknown; valid: boolean } | undefined {
|
||||||
|
const call = response.content.find(
|
||||||
|
(block): block is ToolCall => block.type === 'toolCall' && block.name === SUBMIT_TOOL.name,
|
||||||
|
);
|
||||||
|
if (!call) return undefined;
|
||||||
|
return { output: call.arguments, valid: Value.Check(PROBE_PLAN_SCHEMA, call.arguments) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify whether the provider will serve the exploit agent's workload, via one `completeSimple`
|
||||||
|
* request. Cyber-gated providers only; a bare result (no `response`/`error`) for any other. Never
|
||||||
|
* throws — the caller acts on `response.stopReason` / `error`.
|
||||||
|
*/
|
||||||
|
export async function verifyCyberAccess(
|
||||||
|
model: Model<Api>,
|
||||||
|
modelRuntime: ModelRuntime,
|
||||||
|
providerId: string,
|
||||||
|
): Promise<CyberAccessResult> {
|
||||||
|
// Defensive: never send the exploit workload to a provider that does not gate security work.
|
||||||
|
if (!isCyberGatedProvider(providerId)) {
|
||||||
|
return { providerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: Context = {
|
||||||
|
systemPrompt: `${PROBE_SYSTEM_PROMPT}\n\nCall ${SUBMIT_TOOL.name} exactly once with your assessment.`,
|
||||||
|
messages: [{ role: 'user', content: PROBE_USER_CONTENT, timestamp: Date.now() }],
|
||||||
|
tools: [SUBMIT_TOOL],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await modelRuntime.completeSimple(model, context, { maxRetries: 0 });
|
||||||
|
const structured = extractStructuredPlan(response);
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
response,
|
||||||
|
...(structured !== undefined && { structuredOutput: structured.output, structuredValid: structured.valid }),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const thrown = error instanceof Error ? error : new Error(String(error));
|
||||||
|
return { providerId, error: thrown.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -332,6 +332,14 @@ function classifyByErrorCode(code: ErrorCode, retryableFromError: boolean): { ty
|
|||||||
case ErrorCode.AUTH_FAILED:
|
case ErrorCode.AUTH_FAILED:
|
||||||
return { type: 'AuthenticationError', retryable: false };
|
return { type: 'AuthenticationError', retryable: false };
|
||||||
|
|
||||||
|
// Not AuthenticationError: the credential is not in question, and the pipeline
|
||||||
|
// appends an "is your API key valid" hint to anything classified that way.
|
||||||
|
case ErrorCode.MODEL_NOT_FOUND:
|
||||||
|
return { type: 'ModelNotFoundError', retryable: false };
|
||||||
|
|
||||||
|
case ErrorCode.MODEL_CONFIG_INVALID:
|
||||||
|
return { type: 'ModelConfigError', retryable: false };
|
||||||
|
|
||||||
case ErrorCode.AUTH_LOGIN_FAILED:
|
case ErrorCode.AUTH_LOGIN_FAILED:
|
||||||
return { type: 'AuthLoginFailedError', retryable: false };
|
return { type: 'AuthLoginFailedError', retryable: false };
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,9 @@ import {
|
|||||||
createModelRuntime,
|
createModelRuntime,
|
||||||
GENERIC_API_KEY_ENV,
|
GENERIC_API_KEY_ENV,
|
||||||
type ModelSpec,
|
type ModelSpec,
|
||||||
type OpenAiFormat,
|
modelsConfigPath,
|
||||||
PI_CATALOG_URL,
|
PI_CATALOG_URL,
|
||||||
piAuthPresent,
|
piAuthPresent,
|
||||||
resolveGatewayFormat,
|
|
||||||
resolveModel,
|
resolveModel,
|
||||||
resolveModelSpec,
|
resolveModelSpec,
|
||||||
resolveProviderCredentials,
|
resolveProviderCredentials,
|
||||||
@@ -319,7 +318,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
|||||||
'config',
|
'config',
|
||||||
false,
|
false,
|
||||||
{},
|
{},
|
||||||
ErrorCode.AUTH_FAILED,
|
ErrorCode.MODEL_NOT_FOUND,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -329,24 +328,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
|||||||
// needs one API key.
|
// needs one API key.
|
||||||
const credentials = resolveProviderCredentials(spec.providerId);
|
const credentials = resolveProviderCredentials(spec.providerId);
|
||||||
|
|
||||||
// 3. Wire format for an OpenAI gateway. Rejects a format named where it cannot
|
// With a mounted pi auth.json the env-var checks don't apply — step 4's probe validates it.
|
||||||
// take effect, rather than letting the run proceed on the wrong API.
|
|
||||||
let format: OpenAiFormat;
|
|
||||||
try {
|
|
||||||
format = resolveGatewayFormat(spec.providerId, credentials.baseUrl);
|
|
||||||
} catch (error) {
|
|
||||||
return err(
|
|
||||||
new PentestError(
|
|
||||||
error instanceof Error ? error.message : String(error),
|
|
||||||
'config',
|
|
||||||
false,
|
|
||||||
{ providerId: spec.providerId },
|
|
||||||
ErrorCode.AUTH_FAILED,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// With a mounted pi auth.json the env-var checks don't apply — step 5's probe validates it.
|
|
||||||
const isBedrock = spec.providerId === 'amazon-bedrock';
|
const isBedrock = spec.providerId === 'amazon-bedrock';
|
||||||
const missing =
|
const missing =
|
||||||
isBedrock && !piAuthPresent() ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
|
isBedrock && !piAuthPresent() ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
|
||||||
@@ -362,33 +344,49 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Model must exist in the registry, for every provider — Bedrock IDs are the
|
// 3. Model must exist in the registry, for every provider and endpoint — Bedrock IDs
|
||||||
// easiest to get wrong, since region prefixes and version suffixes differ per
|
// are the easiest to get wrong, since region prefixes and version suffixes differ
|
||||||
// model (`us.anthropic.claude-opus-5` exists, bare `anthropic.` does not).
|
// per model (`us.anthropic.claude-opus-5` exists, bare `anthropic.` does not).
|
||||||
// A custom endpoint is exempt: it may serve models under its own names.
|
// An id the registry lacks is supplied by --models-config, not guessed at here.
|
||||||
const modelRuntime = await createModelRuntime(spec.providerId, credentials.apiKey);
|
const modelRuntime = await createModelRuntime(spec.providerId, credentials.apiKey);
|
||||||
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl, format);
|
|
||||||
if (!baseModel) {
|
// A model config that fails to parse or compose leaves pi with an empty or fallback
|
||||||
|
// provider, which would surface below as "model not found" and blame SHANNON_AI_MODEL
|
||||||
|
// for the file's fault. Report the real cause first.
|
||||||
|
const modelsConfig = modelsConfigPath();
|
||||||
|
if (modelsConfig) {
|
||||||
|
logger.info(`Model config: ${modelsConfig}`);
|
||||||
|
}
|
||||||
|
const modelConfigError = modelRuntime.getError();
|
||||||
|
if (modelConfigError) {
|
||||||
return err(
|
return err(
|
||||||
new PentestError(
|
new PentestError(
|
||||||
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL — browse valid providers and models at ${PI_CATALOG_URL}.`,
|
`Model configuration is invalid:\n${modelConfigError}`,
|
||||||
'config',
|
'config',
|
||||||
false,
|
false,
|
||||||
{ providerId: spec.providerId, modelId: spec.modelId },
|
{ providerId: spec.providerId, ...(modelsConfig && { modelsConfig }) },
|
||||||
ErrorCode.AUTH_FAILED,
|
ErrorCode.MODEL_CONFIG_INVALID,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!modelRuntime.getModel(spec.providerId, spec.modelId)) {
|
|
||||||
logger.warn(
|
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl);
|
||||||
`Model "${spec.modelId}" is not in the ${spec.providerId} catalogue; passing it to the custom endpoint as given. Cost figures will be approximate.`,
|
if (!baseModel) {
|
||||||
|
return err(
|
||||||
|
new PentestError(
|
||||||
|
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL — browse valid providers and models at ${PI_CATALOG_URL}. A model the catalogue does not carry can be defined in a model config passed with --models-config.`,
|
||||||
|
'config',
|
||||||
|
false,
|
||||||
|
{ providerId: spec.providerId, modelId: spec.modelId },
|
||||||
|
ErrorCode.MODEL_NOT_FOUND,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (credentials.baseUrl && spec.providerId === 'openai') {
|
if (credentials.baseUrl && spec.providerId === 'openai') {
|
||||||
logger.info(`Gateway API: ${format} (${baseModel.api})`);
|
logger.info(`Gateway API: ${baseModel.api}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. One real request, so a credential the account cannot use fails here
|
// 4. One real request, so a credential the account cannot use fails here
|
||||||
// rather than partway through the run. Bedrock included: pi resolves the
|
// rather than partway through the run. Bedrock included: pi resolves the
|
||||||
// bearer token from the primed credential and the region from AWS_REGION,
|
// bearer token from the primed credential and the region from AWS_REGION,
|
||||||
// so the probe exercises the same auth path the scan will.
|
// so the probe exercises the same auth path the scan will.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createHash } from 'node:crypto';
|
|||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { ApplicationFailure, Context, heartbeat } from '@temporalio/activity';
|
import { ApplicationFailure, Context, heartbeat } from '@temporalio/activity';
|
||||||
|
import { resolveModelSelection } from '../ai/models.js';
|
||||||
import { syncPermissionSystemConfig } from '../ai/pi/permission-system.js';
|
import { syncPermissionSystemConfig } from '../ai/pi/permission-system.js';
|
||||||
import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js';
|
import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js';
|
||||||
import { AuditSession } from '../audit/index.js';
|
import { AuditSession } from '../audit/index.js';
|
||||||
@@ -39,6 +40,12 @@ import {
|
|||||||
import { getAgentGitPaths } from '../services/agent-git-paths.js';
|
import { getAgentGitPaths } from '../services/agent-git-paths.js';
|
||||||
import { compactReportFindings as compactReportFindingsService } from '../services/compaction-core.js';
|
import { compactReportFindings as compactReportFindingsService } from '../services/compaction-core.js';
|
||||||
import { getContainer, getOrCreateContainer, removeContainer } from '../services/container.js';
|
import { getContainer, getOrCreateContainer, removeContainer } from '../services/container.js';
|
||||||
|
import {
|
||||||
|
type CyberAccessResult,
|
||||||
|
isCyberGatedProvider,
|
||||||
|
isCyberSafeguardDecline,
|
||||||
|
verifyCyberAccess,
|
||||||
|
} from '../services/cyber-access-verification.js';
|
||||||
import { classifyErrorForTemporal, PentestError } from '../services/error-handling.js';
|
import { classifyErrorForTemporal, PentestError } from '../services/error-handling.js';
|
||||||
import { RenumberError } from '../services/exact-output-commit.js';
|
import { RenumberError } from '../services/exact-output-commit.js';
|
||||||
import { ExploitationCheckerService } from '../services/exploitation-checker.js';
|
import { ExploitationCheckerService } from '../services/exploitation-checker.js';
|
||||||
@@ -862,6 +869,81 @@ export async function runPreflightValidation(input: ActivityInput): Promise<void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The provider-specific cyber-access failure type (see workflow-errors.ts); `openai-codex` maps to the OpenAI error. */
|
||||||
|
function cyberAccessErrorType(providerId: string): string {
|
||||||
|
return providerId === 'anthropic' ? 'AnthropicCyberAccessError' : 'OpenAiCyberAccessError';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cyber access verification activity. For OpenAI/Anthropic, hands the model a slice of the
|
||||||
|
* exploit agent's workload and gates on a decline (`stopReason: error`), failing the scan with the
|
||||||
|
* provider's own message. A setup/transport fault is not a decline and never gates.
|
||||||
|
*
|
||||||
|
* Returns `{ gated }` — true only for a provider that actually gates security workloads, so the
|
||||||
|
* caller records the cyber-access stage for those alone (a non-gated provider ran a no-op check).
|
||||||
|
*/
|
||||||
|
export async function runCyberAccessVerification(_input: ActivityInput): Promise<{ gated: boolean }> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const attemptNumber = Context.current().info.attempt;
|
||||||
|
|
||||||
|
const heartbeatInterval = setInterval(() => {
|
||||||
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
heartbeat({ phase: 'cyber-access', elapsedSeconds: elapsed, attempt: attemptNumber });
|
||||||
|
}, HEARTBEAT_INTERVAL_MS);
|
||||||
|
|
||||||
|
const logger = createActivityLogger();
|
||||||
|
|
||||||
|
let result: CyberAccessResult;
|
||||||
|
try {
|
||||||
|
const selection = await resolveModelSelection();
|
||||||
|
|
||||||
|
// Only OpenAI and Anthropic gate security workloads — never verify any other provider.
|
||||||
|
if (!isCyberGatedProvider(selection.providerId)) {
|
||||||
|
logger.info(`Cyber access verification: skipped (provider ${selection.providerId})`);
|
||||||
|
return { gated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Verifying cyber access via pi...');
|
||||||
|
result = await verifyCyberAccess(selection.model, selection.modelRuntime, selection.providerId);
|
||||||
|
} catch (error) {
|
||||||
|
// Setup/transport fault, not a decline — never gates the scan.
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.info(`Cyber access verification: skipped (${message.slice(0, 200)})`);
|
||||||
|
return { gated: false };
|
||||||
|
} finally {
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error !== undefined) {
|
||||||
|
logger.info(`Cyber access verification: ${result.providerId} inconclusive (${result.error.slice(0, 200)})`);
|
||||||
|
return { gated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.response?.stopReason === 'error') {
|
||||||
|
logger.info(
|
||||||
|
`Cyber access verification: declined by ${result.providerId}: ${(result.response.errorMessage ?? '').slice(0, 1000)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gate only on a confirmed cyber decline; any other errored turn is inconclusive.
|
||||||
|
if (!isCyberSafeguardDecline(result.providerId, result.response)) {
|
||||||
|
logger.info(`Cyber access verification: ${result.providerId} inconclusive (errored turn, not a cyber decline)`);
|
||||||
|
return { gated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gate with the provider-specific type (for the CLI guidance), bounded message.
|
||||||
|
const message = truncateErrorMessage(`${result.providerId} declined the exploit workload`);
|
||||||
|
const failure = ApplicationFailure.nonRetryable(message, cyberAccessErrorType(result.providerId), [
|
||||||
|
{ phase: 'cyber-access', attemptNumber, elapsed: Date.now() - startTime },
|
||||||
|
]);
|
||||||
|
truncateStackTrace(failure);
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const structured = result.structuredOutput !== undefined ? result.structuredValid : 'none';
|
||||||
|
logger.info(`Cyber access verification: ${result.providerId} OK (structured=${structured})`);
|
||||||
|
return { gated: true };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication validation activity. No-ops without an authentication
|
* Authentication validation activity. No-ops without an authentication
|
||||||
* block; otherwise surfaces a classified failure (failurePoint +
|
* block; otherwise surfaces a classified failure (failurePoint +
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ export interface PipelineInput {
|
|||||||
customerOutputPath?: string; // Stable mounted path for final customer copies only
|
customerOutputPath?: string; // Stable mounted path for final customer copies only
|
||||||
checkpointsEnabled?: boolean; // Enable checkpoint activities (default: false)
|
checkpointsEnabled?: boolean; // Enable checkpoint activities (default: false)
|
||||||
exploit?: boolean; // false skips the exploitation phase
|
exploit?: boolean; // false skips the exploitation phase
|
||||||
|
authOnly?: boolean; // true stops the run after auth validation (no pentest, no report)
|
||||||
|
validateModel?: boolean; // true stops the run after the preflight model checks (no pentest, no report)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What `loadResumeState` reconstructs from a prior workspace: independently verified, never assumed from session.json alone. */
|
/** What `loadResumeState` reconstructs from a prior workspace: independently verified, never assumed from session.json alone. */
|
||||||
@@ -184,6 +186,8 @@ export interface PipelineSummary {
|
|||||||
*/
|
*/
|
||||||
export interface PipelineState {
|
export interface PipelineState {
|
||||||
status: 'running' | 'completed' | 'failed' | 'cancelled' | 'partial';
|
status: 'running' | 'completed' | 'failed' | 'cancelled' | 'partial';
|
||||||
|
authOnly: boolean;
|
||||||
|
validateModel: boolean;
|
||||||
currentPhase: string | null;
|
currentPhase: string | null;
|
||||||
currentAgent: string | null;
|
currentAgent: string | null;
|
||||||
/** Agents that actually ran. Mutually exclusive from `skippedAgents`. */
|
/** Agents that actually ran. Mutually exclusive from `skippedAgents`. */
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export function toWorkflowSummary(
|
|||||||
Object.entries(state.operationalStages).map(([key, stage]) => [
|
Object.entries(state.operationalStages).map(([key, stage]) => [
|
||||||
key,
|
key,
|
||||||
{
|
{
|
||||||
|
status: stage.status,
|
||||||
...(stage.startedAt !== undefined && { startedAt: stage.startedAt }),
|
...(stage.startedAt !== undefined && { startedAt: stage.startedAt }),
|
||||||
...(stage.durationMs !== undefined && { durationMs: stage.durationMs }),
|
...(stage.durationMs !== undefined && { durationMs: stage.durationMs }),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
* TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233)
|
* TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { Client, Connection, type WorkflowHandle, WorkflowNotFoundError } from '@temporalio/client';
|
import { Client, Connection, type WorkflowHandle, WorkflowNotFoundError } from '@temporalio/client';
|
||||||
@@ -40,7 +41,8 @@ import { CAPELLA_FORMAT_VERSION, CAPELLA_PROMPT_SET_VERSION } from '../ai/sast/c
|
|||||||
import { summarizeOperationalMetrics } from '../audit/operational-summary.js';
|
import { summarizeOperationalMetrics } from '../audit/operational-summary.js';
|
||||||
import { sanitizeHostname } from '../audit/utils.js';
|
import { sanitizeHostname } from '../audit/utils.js';
|
||||||
import { distributeConfig, parseConfig } from '../config-parser.js';
|
import { distributeConfig, parseConfig } from '../config-parser.js';
|
||||||
import { deliverablesDir, resolveSessionJsonPath } from '../paths.js';
|
import { deliverablesDir, INTERNAL_DIR, resolveSessionJsonPath, STARTUP_ERROR_FILENAME } from '../paths.js';
|
||||||
|
import { PentestError } from '../services/error-handling.js';
|
||||||
import { isProviderFailureCategory } from '../types/errors.js';
|
import { isProviderFailureCategory } from '../types/errors.js';
|
||||||
import {
|
import {
|
||||||
ACCEPTED_CAPELLA_FAILURE_STAGES,
|
ACCEPTED_CAPELLA_FAILURE_STAGES,
|
||||||
@@ -73,6 +75,7 @@ import {
|
|||||||
runAuthVulnAgent,
|
runAuthVulnAgent,
|
||||||
runAuthzExploitAgent,
|
runAuthzExploitAgent,
|
||||||
runAuthzVulnAgent,
|
runAuthzVulnAgent,
|
||||||
|
runCyberAccessVerification,
|
||||||
runInjectionExploitAgent,
|
runInjectionExploitAgent,
|
||||||
runInjectionVulnAgent,
|
runInjectionVulnAgent,
|
||||||
runMiscellaneousExploitAgent,
|
runMiscellaneousExploitAgent,
|
||||||
@@ -145,6 +148,7 @@ export const PENTEST_ACTIVITY_NAMES = Object.freeze([
|
|||||||
'runMiscellaneousExploitAgent',
|
'runMiscellaneousExploitAgent',
|
||||||
'runReportAgent',
|
'runReportAgent',
|
||||||
'runPreflightValidation',
|
'runPreflightValidation',
|
||||||
|
'runCyberAccessVerification',
|
||||||
'runAuthenticationValidation',
|
'runAuthenticationValidation',
|
||||||
'initDeliverableGit',
|
'initDeliverableGit',
|
||||||
'syncPlaywrightStealthConfig',
|
'syncPlaywrightStealthConfig',
|
||||||
@@ -185,6 +189,7 @@ export const pentestActivities = Object.freeze({
|
|||||||
runMiscellaneousExploitAgent,
|
runMiscellaneousExploitAgent,
|
||||||
runReportAgent,
|
runReportAgent,
|
||||||
runPreflightValidation,
|
runPreflightValidation,
|
||||||
|
runCyberAccessVerification,
|
||||||
runAuthenticationValidation,
|
runAuthenticationValidation,
|
||||||
initDeliverableGit,
|
initDeliverableGit,
|
||||||
syncPlaywrightStealthConfig,
|
syncPlaywrightStealthConfig,
|
||||||
@@ -245,6 +250,8 @@ interface CliArgs {
|
|||||||
configPath?: string;
|
configPath?: string;
|
||||||
customerOutputPath?: string;
|
customerOutputPath?: string;
|
||||||
pipelineTestingMode: boolean;
|
pipelineTestingMode: boolean;
|
||||||
|
authOnly: boolean;
|
||||||
|
validateModel: boolean;
|
||||||
resumeFromWorkspace?: string;
|
resumeFromWorkspace?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +266,9 @@ function showUsage(): void {
|
|||||||
console.log(' --config <path> Configuration file path');
|
console.log(' --config <path> Configuration file path');
|
||||||
console.log(' --workspace <name> Resume from existing workspace');
|
console.log(' --workspace <name> Resume from existing workspace');
|
||||||
console.log(' --output <path> Stable mounted path for final customer report copies');
|
console.log(' --output <path> Stable mounted path for final customer report copies');
|
||||||
console.log(' --pipeline-testing Use minimal prompts for fast testing\n');
|
console.log(' --pipeline-testing Use minimal prompts for fast testing');
|
||||||
|
console.log(' --validate-auth Validate authentication only, then stop');
|
||||||
|
console.log(' --validate-model Validate the AI model only, then stop\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCliArgs(argv: string[]): CliArgs {
|
function parseCliArgs(argv: string[]): CliArgs {
|
||||||
@@ -275,6 +284,8 @@ function parseCliArgs(argv: string[]): CliArgs {
|
|||||||
let configPath: string | undefined;
|
let configPath: string | undefined;
|
||||||
let customerOutputPath: string | undefined;
|
let customerOutputPath: string | undefined;
|
||||||
let pipelineTestingMode = false;
|
let pipelineTestingMode = false;
|
||||||
|
let authOnly = false;
|
||||||
|
let validateModel = false;
|
||||||
let resumeFromWorkspace: string | undefined;
|
let resumeFromWorkspace: string | undefined;
|
||||||
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
@@ -311,6 +322,10 @@ function parseCliArgs(argv: string[]): CliArgs {
|
|||||||
}
|
}
|
||||||
} else if (arg === '--pipeline-testing') {
|
} else if (arg === '--pipeline-testing') {
|
||||||
pipelineTestingMode = true;
|
pipelineTestingMode = true;
|
||||||
|
} else if (arg === '--validate-auth') {
|
||||||
|
authOnly = true;
|
||||||
|
} else if (arg === '--validate-model') {
|
||||||
|
validateModel = true;
|
||||||
} else if (arg && !arg.startsWith('-')) {
|
} else if (arg && !arg.startsWith('-')) {
|
||||||
if (!webUrl) {
|
if (!webUrl) {
|
||||||
webUrl = arg;
|
webUrl = arg;
|
||||||
@@ -338,6 +353,8 @@ function parseCliArgs(argv: string[]): CliArgs {
|
|||||||
taskQueue,
|
taskQueue,
|
||||||
...(workflowId && { workflowId }),
|
...(workflowId && { workflowId }),
|
||||||
pipelineTestingMode,
|
pipelineTestingMode,
|
||||||
|
authOnly,
|
||||||
|
validateModel,
|
||||||
...(configPath && { configPath }),
|
...(configPath && { configPath }),
|
||||||
...(customerOutputPath && { customerOutputPath }),
|
...(customerOutputPath && { customerOutputPath }),
|
||||||
...(resumeFromWorkspace && { resumeFromWorkspace }),
|
...(resumeFromWorkspace && { resumeFromWorkspace }),
|
||||||
@@ -511,9 +528,13 @@ interface OrchestrationConfig {
|
|||||||
exploit?: boolean;
|
exploit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the scan config into orchestration values, or throw on a broken config. Failing (rather
|
||||||
|
* than falling back to defaults that quietly change scope) lets the caller persist parseConfig's
|
||||||
|
* error for the CLI instead of running a misconfigured scan.
|
||||||
|
*/
|
||||||
async function loadOrchestrationConfig(configPath: string | undefined): Promise<OrchestrationConfig> {
|
async function loadOrchestrationConfig(configPath: string | undefined): Promise<OrchestrationConfig> {
|
||||||
if (!configPath) return {};
|
if (!configPath) return {};
|
||||||
try {
|
|
||||||
const config = await parseConfig(configPath);
|
const config = await parseConfig(configPath);
|
||||||
const distributed = distributeConfig(config);
|
const distributed = distributeConfig(config);
|
||||||
const codePathAvoids = distributed.avoid.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
const codePathAvoids = distributed.avoid.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
||||||
@@ -531,11 +552,37 @@ async function loadOrchestrationConfig(configPath: string | undefined): Promise<
|
|||||||
}),
|
}),
|
||||||
exploit: distributed.exploit,
|
exploit: distributed.exploit,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
}
|
||||||
// A broken config must fail the run, not silently fall back to empty
|
|
||||||
// defaults that quietly change scope (vuln classes, exploit, retries).
|
// === Startup Failure Persistence ===
|
||||||
console.error('Worker configuration could not be loaded. Reference code: CONFIG_VALIDATION_FAILED');
|
|
||||||
process.exit(1);
|
/** Reason for a failure that happens before the workflow is created. */
|
||||||
|
interface StartupErrorRecord {
|
||||||
|
phase: string;
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a pre-workflow failure to the bind-mounted workspace so the CLI can surface it. The
|
||||||
|
* worker exits before the workflow exists, so Temporal has no record and `--rm` removes the
|
||||||
|
* container; the file under INTERNAL_DIR outlives it on the host mount. `workspace` is the CLI's
|
||||||
|
* `--workspace` name (the run directory); absent only when the worker is run off the CLI path.
|
||||||
|
* Best-effort — a persist failure must not mask the original error.
|
||||||
|
*/
|
||||||
|
function persistStartupError(workspace: string | undefined, error: unknown, phase: string): void {
|
||||||
|
if (!workspace) return;
|
||||||
|
const record: StartupErrorRecord = {
|
||||||
|
phase,
|
||||||
|
...(error instanceof PentestError && error.code !== undefined && { code: error.code }),
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const dir = path.join('./workspaces', workspace, INTERNAL_DIR);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(path.join(dir, STARTUP_ERROR_FILENAME), JSON.stringify(record, null, 2), 'utf8');
|
||||||
|
} catch {
|
||||||
|
// A broken bind mount must not compound the failure; the caller's console.error still fires.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,6 +603,8 @@ function buildPipelineInput(
|
|||||||
...(args.customerOutputPath !== undefined && { customerOutputPath: args.customerOutputPath }),
|
...(args.customerOutputPath !== undefined && { customerOutputPath: args.customerOutputPath }),
|
||||||
...(orchestration.agenticSast !== undefined && { agenticSast: orchestration.agenticSast }),
|
...(orchestration.agenticSast !== undefined && { agenticSast: orchestration.agenticSast }),
|
||||||
...(orchestration.exploit !== undefined && { exploit: orchestration.exploit }),
|
...(orchestration.exploit !== undefined && { exploit: orchestration.exploit }),
|
||||||
|
...(args.authOnly && { authOnly: true }),
|
||||||
|
...(args.validateModel && { validateModel: true }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,6 +659,10 @@ async function waitForWorkflowResult(
|
|||||||
}
|
}
|
||||||
} else if (result.status === 'cancelled') {
|
} else if (result.status === 'cancelled') {
|
||||||
console.log('\nScan cancelled before it finished.');
|
console.log('\nScan cancelled before it finished.');
|
||||||
|
} else if (result.authOnly) {
|
||||||
|
console.log('\nAuthentication validated. No pentest was run (--validate-auth).');
|
||||||
|
} else if (result.validateModel) {
|
||||||
|
console.log('\nModel validated. No pentest was run (--validate-model).');
|
||||||
} else {
|
} else {
|
||||||
console.log('\nScan completed.');
|
console.log('\nScan completed.');
|
||||||
}
|
}
|
||||||
@@ -655,24 +708,26 @@ async function waitForWorkflowResult(
|
|||||||
|
|
||||||
// === Main Entry Point ===
|
// === Main Entry Point ===
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
/** A scan whose workflow is durably submitted, with the handles run() needs to await it. */
|
||||||
// 1. Parse CLI args
|
interface StartedScan {
|
||||||
const args = parseCliArgs(process.argv.slice(2));
|
handle: WorkflowHandle<(input: PipelineInput) => Promise<PipelineState>>;
|
||||||
|
workspace: WorkspaceResolution;
|
||||||
// 2. Connect to Temporal server
|
worker: Worker;
|
||||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
workerDone: Promise<void>;
|
||||||
console.log(`Connecting to Temporal at ${address}...`);
|
}
|
||||||
|
|
||||||
const connection = await NativeConnection.connect({ address });
|
|
||||||
const clientConnection = await Connection.connect({ address });
|
|
||||||
const client = new Client({ connection: clientConnection });
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run every step that precedes the durable creation of the workflow: config parsing, workspace
|
||||||
|
* resolution, worker setup, and workflow submission. A failure anywhere here is a startup failure
|
||||||
|
* — Temporal holds no record yet — so the reason is persisted for the CLI before it propagates.
|
||||||
|
*/
|
||||||
|
async function startScan(client: Client, connection: NativeConnection, args: CliArgs): Promise<StartedScan> {
|
||||||
try {
|
try {
|
||||||
// 3. Validate orchestration and resume state before terminating any workflow.
|
// 1. Validate orchestration and resume state before terminating any workflow.
|
||||||
const orchestration = await loadOrchestrationConfig(args.configPath);
|
const orchestration = await loadOrchestrationConfig(args.configPath);
|
||||||
const workspace = await resolveWorkspace(client, args, orchestration.exploit ?? true);
|
const workspace = await resolveWorkspace(client, args, orchestration.exploit ?? true);
|
||||||
|
|
||||||
// 4. Bundle workflows and create the worker with the collision-checked activity registry.
|
// 2. Bundle workflows and create the worker with the collision-checked activity registry.
|
||||||
console.log('Preparing scan...');
|
console.log('Preparing scan...');
|
||||||
const workflowBundle = await bundleWorkflowCode({
|
const workflowBundle = await bundleWorkflowCode({
|
||||||
workflowsPath: path.join(__dirname, 'workflows.js'),
|
workflowsPath: path.join(__dirname, 'workflows.js'),
|
||||||
@@ -695,13 +750,11 @@ async function run(): Promise<void> {
|
|||||||
maxConcurrentActivityTaskExecutions: 25,
|
maxConcurrentActivityTaskExecutions: 25,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. Build the fixed-scope pipeline input.
|
// 3. Build the fixed-scope pipeline input and start worker polling in the background.
|
||||||
const input = buildPipelineInput(args, workspace, orchestration);
|
const input = buildPipelineInput(args, workspace, orchestration);
|
||||||
|
|
||||||
// 6. Start worker polling in the background.
|
|
||||||
const workerDone = worker.run();
|
const workerDone = worker.run();
|
||||||
|
|
||||||
// 7. Submit workflow to the same task queue.
|
// 4. Submit workflow to the same task queue. Past this point the run exists in Temporal.
|
||||||
const handle = await client.workflow.start<(input: PipelineInput) => Promise<PipelineState>>(
|
const handle = await client.workflow.start<(input: PipelineInput) => Promise<PipelineState>>(
|
||||||
'pentestPipelineWorkflow',
|
'pentestPipelineWorkflow',
|
||||||
{
|
{
|
||||||
@@ -711,10 +764,38 @@ async function run(): Promise<void> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 8. Wait for workflow result.
|
return { handle, workspace, worker, workerDone };
|
||||||
|
} catch (startupError) {
|
||||||
|
persistStartupError(args.resumeFromWorkspace, startupError, 'startup');
|
||||||
|
throw startupError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
// 1. Parse CLI args
|
||||||
|
const args = parseCliArgs(process.argv.slice(2));
|
||||||
|
|
||||||
|
// One scan per worker process, so an auth-only or model-validation run is a process-wide fact.
|
||||||
|
// The log writers read these to frame the log as a validation rather than a pentest.
|
||||||
|
if (args.authOnly) process.env.SHANNON_AUTH_ONLY = '1';
|
||||||
|
if (args.validateModel) process.env.SHANNON_VALIDATE_MODEL = '1';
|
||||||
|
|
||||||
|
// 2. Connect to Temporal server
|
||||||
|
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||||
|
console.log(`Connecting to Temporal at ${address}...`);
|
||||||
|
|
||||||
|
const connection = await NativeConnection.connect({ address });
|
||||||
|
const clientConnection = await Connection.connect({ address });
|
||||||
|
const client = new Client({ connection: clientConnection });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. Start the scan: parse config, resolve the workspace, and submit the workflow.
|
||||||
|
const { handle, workspace, worker, workerDone } = await startScan(client, connection, args);
|
||||||
|
|
||||||
|
// 4. Wait for workflow result.
|
||||||
await waitForWorkflowResult(handle, workspace);
|
await waitForWorkflowResult(handle, workspace);
|
||||||
|
|
||||||
// 9. Shut down worker gracefully. Final customer copies are workflow-owned.
|
// 5. Shut down worker gracefully. Final customer copies are workflow-owned.
|
||||||
worker.shutdown();
|
worker.shutdown();
|
||||||
await workerDone;
|
await workerDone;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -725,8 +806,10 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : undefined;
|
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : undefined;
|
||||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||||
run().catch(() => {
|
run().catch((error) => {
|
||||||
console.error('Worker failed. Reference code: WORKER_FAILED');
|
// startScan persists pre-workflow failures for the CLI; this also logs them in the container.
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`Worker failed: ${message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@ import { ErrorCode } from '../types/errors.js';
|
|||||||
*/
|
*/
|
||||||
const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
|
const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
|
||||||
AuthenticationError: ErrorCode.AUTH_FAILED,
|
AuthenticationError: ErrorCode.AUTH_FAILED,
|
||||||
|
ModelNotFoundError: ErrorCode.MODEL_NOT_FOUND,
|
||||||
|
ModelConfigError: ErrorCode.MODEL_CONFIG_INVALID,
|
||||||
ConfigurationError: ErrorCode.CONFIG_VALIDATION_FAILED,
|
ConfigurationError: ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||||
OutputValidationError: ErrorCode.OUTPUT_VALIDATION_FAILED,
|
OutputValidationError: ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||||
AgentExecutionError: ErrorCode.AGENT_EXECUTION_FAILED,
|
AgentExecutionError: ErrorCode.AGENT_EXECUTION_FAILED,
|
||||||
@@ -34,6 +36,8 @@ const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
|
|||||||
ReportSarifRenderError: ErrorCode.OUTPUT_VALIDATION_FAILED,
|
ReportSarifRenderError: ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||||
IncompatibleWorkspaceError: ErrorCode.CONFIG_VALIDATION_FAILED,
|
IncompatibleWorkspaceError: ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||||
WorkspaceNotFoundError: ErrorCode.CONFIG_NOT_FOUND,
|
WorkspaceNotFoundError: ErrorCode.CONFIG_NOT_FOUND,
|
||||||
|
OpenAiCyberAccessError: ErrorCode.PROVIDER_CYBER_ACCESS_REQUIRED,
|
||||||
|
AnthropicCyberAccessError: ErrorCode.PROVIDER_CYBER_ACCESS_REQUIRED,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function classifyErrorCode(error: unknown): ErrorCode | undefined {
|
export function classifyErrorCode(error: unknown): ErrorCode | undefined {
|
||||||
@@ -54,12 +58,18 @@ export function classifyErrorCode(error: unknown): ErrorCode | undefined {
|
|||||||
*/
|
*/
|
||||||
const REMEDIATION_HINTS: Record<string, string> = {
|
const REMEDIATION_HINTS: Record<string, string> = {
|
||||||
AuthenticationError: "Verify the selected provider's API key is valid and not expired.",
|
AuthenticationError: "Verify the selected provider's API key is valid and not expired.",
|
||||||
|
ModelNotFoundError: 'Check SHANNON_AI_MODEL against pi.dev/models, or supply the model with --models-config.',
|
||||||
|
ModelConfigError: 'Check the --models-config file parses and matches pi’s models.json schema.',
|
||||||
ConfigurationError: 'Check your CONFIG file path and contents.',
|
ConfigurationError: 'Check your CONFIG file path and contents.',
|
||||||
GitError: 'Check repository path and git state.',
|
GitError: 'Check repository path and git state.',
|
||||||
InvalidTargetError: 'Verify the target URL is correct and accessible.',
|
InvalidTargetError: 'Verify the target URL is correct and accessible.',
|
||||||
IncompatibleWorkspaceError: 'start a new scan with a different -w name.',
|
IncompatibleWorkspaceError: 'start a new scan with a different -w name.',
|
||||||
WorkspaceNotFoundError: 'check the -w name against: shannon scans',
|
WorkspaceNotFoundError: 'check the -w name against: shannon scans',
|
||||||
PipelineFailedError: 're-run the same -w to retry from the last checkpoint.',
|
PipelineFailedError: 're-run the same -w to retry from the last checkpoint.',
|
||||||
|
OpenAiCyberAccessError:
|
||||||
|
'Your OpenAI organization must be approved for cyber use. Apply for Daybreak access at https://openai.com/daybreak, then retry. Or use the gpt-5.4 model instead.',
|
||||||
|
AnthropicCyberAccessError:
|
||||||
|
'Your Anthropic organization must complete cyber verification. See https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet, then retry. Or use the claude-sonnet-4-6 model instead.',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +79,8 @@ const REMEDIATION_HINTS: Record<string, string> = {
|
|||||||
*/
|
*/
|
||||||
const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
||||||
AuthenticationError: 'Provider authentication failed.',
|
AuthenticationError: 'Provider authentication failed.',
|
||||||
|
ModelNotFoundError: 'The selected model was not found in the harness catalogue.',
|
||||||
|
ModelConfigError: 'The model configuration file could not be used.',
|
||||||
ConfigurationError: 'The scan configuration is invalid.',
|
ConfigurationError: 'The scan configuration is invalid.',
|
||||||
OutputValidationError: 'A scan step returned an unusable result.',
|
OutputValidationError: 'A scan step returned an unusable result.',
|
||||||
AgentExecutionError: 'An agent could not complete its work.',
|
AgentExecutionError: 'An agent could not complete its work.',
|
||||||
@@ -80,6 +92,8 @@ const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
|||||||
ReportSarifRenderError: 'The report SARIF output could not be rendered.',
|
ReportSarifRenderError: 'The report SARIF output could not be rendered.',
|
||||||
IncompatibleWorkspaceError: 'This workspace cannot be resumed.',
|
IncompatibleWorkspaceError: 'This workspace cannot be resumed.',
|
||||||
WorkspaceNotFoundError: 'The requested workspace was not found.',
|
WorkspaceNotFoundError: 'The requested workspace was not found.',
|
||||||
|
OpenAiCyberAccessError: 'OpenAI declined the security workload behind its cyber-access program.',
|
||||||
|
AnthropicCyberAccessError: 'Anthropic declined the security workload behind its cyber-access program.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const WORKFLOW_PHASE_SET = new Set<string>(WORKFLOW_PHASES);
|
const WORKFLOW_PHASE_SET = new Set<string>(WORKFLOW_PHASES);
|
||||||
@@ -125,10 +139,6 @@ export function formatWorkflowError(error: unknown, currentPhase: string | null,
|
|||||||
|
|
||||||
const segments: string[] = [phaseContext];
|
const segments: string[] = [phaseContext];
|
||||||
|
|
||||||
if (unwrapped.type) {
|
|
||||||
segments.push(unwrapped.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.push(
|
segments.push(
|
||||||
unwrapped.type === null
|
unwrapped.type === null
|
||||||
? 'The scan could not be completed.'
|
? 'The scan could not be completed.'
|
||||||
@@ -140,6 +150,7 @@ export function formatWorkflowError(error: unknown, currentPhase: string | null,
|
|||||||
if (hint) {
|
if (hint) {
|
||||||
segments.push(`Hint: ${hint}`);
|
segments.push(`Hint: ${hint}`);
|
||||||
}
|
}
|
||||||
|
segments.push(`Reference code: ${unwrapped.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments.join('|');
|
return segments.join('|');
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ const PRODUCTION_RETRY = {
|
|||||||
'InvalidTargetError',
|
'InvalidTargetError',
|
||||||
'AuthLoginFailedError',
|
'AuthLoginFailedError',
|
||||||
'PermanentError',
|
'PermanentError',
|
||||||
|
'OpenAiCyberAccessError',
|
||||||
|
'AnthropicCyberAccessError',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -379,11 +381,15 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
|||||||
const { workflowId } = workflowInfo();
|
const { workflowId } = workflowInfo();
|
||||||
const a = input.pipelineTestingMode ? testActs : acts;
|
const a = input.pipelineTestingMode ? testActs : acts;
|
||||||
const exploit = input.exploit ?? true;
|
const exploit = input.exploit ?? true;
|
||||||
|
const authOnly = input.authOnly ?? false;
|
||||||
|
const validateModel = input.validateModel ?? false;
|
||||||
const sessionId = input.sessionId || input.resumeFromWorkspace || workflowId;
|
const sessionId = input.sessionId || input.resumeFromWorkspace || workflowId;
|
||||||
const stateContext: 'fresh' | 'resume' = input.resumeFromWorkspace ? 'resume' : 'fresh';
|
const stateContext: 'fresh' | 'resume' = input.resumeFromWorkspace ? 'resume' : 'fresh';
|
||||||
|
|
||||||
const state: PipelineState = {
|
const state: PipelineState = {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
authOnly,
|
||||||
|
validateModel,
|
||||||
currentPhase: null,
|
currentPhase: null,
|
||||||
currentAgent: null,
|
currentAgent: null,
|
||||||
completedAgents: [],
|
completedAgents: [],
|
||||||
@@ -1287,7 +1293,7 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
|||||||
const durable = await deterministicReportActs.initializeDurableScanState(activityInput, exploit, stateContext);
|
const durable = await deterministicReportActs.initializeDurableScanState(activityInput, exploit, stateContext);
|
||||||
applyDurableSummary(durable);
|
applyDurableSummary(durable);
|
||||||
|
|
||||||
if (input.resumeFromWorkspace) {
|
if (!authOnly && input.resumeFromWorkspace) {
|
||||||
// The new workflow id lands in session.json before anything that can reject the resume, so a
|
// The new workflow id lands in session.json before anything that can reject the resume, so a
|
||||||
// validation or checkpoint-restore failure still leaves the CLI an attempt to follow.
|
// validation or checkpoint-restore failure still leaves the CLI an attempt to follow.
|
||||||
await deterministicReportActs.registerResumeAttempt(activityInput, input.terminatedWorkflows ?? []);
|
await deterministicReportActs.registerResumeAttempt(activityInput, input.terminatedWorkflows ?? []);
|
||||||
@@ -1337,7 +1343,30 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
|||||||
|
|
||||||
state.currentPhase = 'preflight';
|
state.currentPhase = 'preflight';
|
||||||
state.currentAgent = null;
|
state.currentAgent = null;
|
||||||
await preflightActs.runPreflightValidation(activityInput);
|
await runOperation('preflight', 'Preflight', () => preflightActs.runPreflightValidation(activityInput));
|
||||||
|
if (!authOnly) {
|
||||||
|
const startedAt = startOperation('cyber-access', 'Cyber access verification');
|
||||||
|
try {
|
||||||
|
const verification = await preflightActs.runCyberAccessVerification(activityInput);
|
||||||
|
if (verification.gated) {
|
||||||
|
completeOperation('cyber-access', 'Cyber access verification', startedAt);
|
||||||
|
} else {
|
||||||
|
delete state.operationalStages['cyber-access'];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failOperation('cyber-access', 'Cyber access verification', startedAt);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateModel) {
|
||||||
|
state.status = 'completed';
|
||||||
|
state.currentPhase = null;
|
||||||
|
state.summary = computeSummary(state, usageAccountingComplete());
|
||||||
|
await a.logWorkflowComplete(activityInput, toWorkflowSummary(state, 'completed'));
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
await preflightActs.syncPlaywrightStealthConfig(activityInput);
|
await preflightActs.syncPlaywrightStealthConfig(activityInput);
|
||||||
|
|
||||||
state.currentPhase = 'auth-validation';
|
state.currentPhase = 'auth-validation';
|
||||||
@@ -1346,6 +1375,21 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
|||||||
if (authMetrics !== null) state.agentMetrics['validate-authentication'] = authMetrics;
|
if (authMetrics !== null) state.agentMetrics['validate-authentication'] = authMetrics;
|
||||||
state.currentAgent = null;
|
state.currentAgent = null;
|
||||||
|
|
||||||
|
// Auth-only runs stop here; a null result means no authentication block, which is a misconfig.
|
||||||
|
if (authOnly) {
|
||||||
|
if (authMetrics === null) {
|
||||||
|
throw ApplicationFailure.nonRetryable(
|
||||||
|
'An auth-validation run needs an authentication block in the config. Add one, or drop --validate-auth.',
|
||||||
|
'ConfigurationError',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.status = 'completed';
|
||||||
|
state.currentPhase = null;
|
||||||
|
state.summary = computeSummary(state, usageAccountingComplete());
|
||||||
|
await a.logWorkflowComplete(activityInput, toWorkflowSummary(state, 'completed'));
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
await a.initDeliverableGit(activityInput);
|
await a.initDeliverableGit(activityInput);
|
||||||
await a.syncCodePathDenyRules(activityInput);
|
await a.syncCodePathDenyRules(activityInput);
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export enum ErrorCode {
|
|||||||
TARGET_UNREACHABLE = 'TARGET_UNREACHABLE',
|
TARGET_UNREACHABLE = 'TARGET_UNREACHABLE',
|
||||||
AUTH_FAILED = 'AUTH_FAILED',
|
AUTH_FAILED = 'AUTH_FAILED',
|
||||||
AUTH_LOGIN_FAILED = 'AUTH_LOGIN_FAILED',
|
AUTH_LOGIN_FAILED = 'AUTH_LOGIN_FAILED',
|
||||||
|
MODEL_NOT_FOUND = 'MODEL_NOT_FOUND',
|
||||||
|
MODEL_CONFIG_INVALID = 'MODEL_CONFIG_INVALID',
|
||||||
|
PROVIDER_CYBER_ACCESS_REQUIRED = 'PROVIDER_CYBER_ACCESS_REQUIRED',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PentestErrorType = 'config' | 'network' | 'prompt' | 'filesystem' | 'validation' | 'unknown';
|
export type PentestErrorType = 'config' | 'network' | 'prompt' | 'filesystem' | 'validation' | 'unknown';
|
||||||
|
|||||||
@@ -128,6 +128,9 @@
|
|||||||
text(fill: white, weight: "bold", size: 7.5pt, tracking: 0.3pt, upper(label)),
|
text(fill: white, weight: "bold", size: 7.5pt, tracking: 0.3pt, upper(label)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#let finding-anchor(id) = label("finding-" + id)
|
||||||
|
#let finding-link(id) = link(finding-anchor(id), text(weight: "semibold")[#id])
|
||||||
|
|
||||||
#let categories-in-order = if mode == "exploits" {
|
#let categories-in-order = if mode == "exploits" {
|
||||||
data.exploitedByType.map(entry => entry.category)
|
data.exploitedByType.map(entry => entry.category)
|
||||||
} else {
|
} else {
|
||||||
@@ -338,7 +341,7 @@
|
|||||||
#if "bullets" in entry and entry.bullets != none [
|
#if "bullets" in entry and entry.bullets != none [
|
||||||
#list(
|
#list(
|
||||||
..entry.bullets.map(b => [
|
..entry.bullets.map(b => [
|
||||||
#text(weight: "semibold")[#b.id] — #inline-code(b.description)
|
#finding-link(b.id) — #inline-code(b.description)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -474,7 +477,7 @@
|
|||||||
..(if show-confidence-col { (text(size: 9.5pt, weight: "semibold")[Confidence],) } else { () }),
|
..(if show-confidence-col { (text(size: 9.5pt, weight: "semibold")[Confidence],) } else { () }),
|
||||||
),
|
),
|
||||||
..data.findings.map(f => (
|
..data.findings.map(f => (
|
||||||
text(weight: "semibold")[#f.id],
|
finding-link(f.id),
|
||||||
inline-code(f.title),
|
inline-code(f.title),
|
||||||
text(size: 9.5pt)[#f.category],
|
text(size: 9.5pt)[#f.category],
|
||||||
sev-chip(f.severity),
|
sev-chip(f.severity),
|
||||||
@@ -531,7 +534,7 @@
|
|||||||
|
|
||||||
#let render-exploit(f) = {
|
#let render-exploit(f) = {
|
||||||
block(breakable: false)[
|
block(breakable: false)[
|
||||||
#heading(level: 2)[#f.id: #inline-code(f.title)]
|
#heading(level: 2)[#f.id: #inline-code(f.title)]#finding-anchor(f.id)
|
||||||
#sev-chip(f.severity)
|
#sev-chip(f.severity)
|
||||||
#v(8pt)
|
#v(8pt)
|
||||||
#render-finding-owasp(f)
|
#render-finding-owasp(f)
|
||||||
@@ -553,7 +556,7 @@
|
|||||||
|
|
||||||
#let render-analysis(f) = {
|
#let render-analysis(f) = {
|
||||||
block(breakable: false)[
|
block(breakable: false)[
|
||||||
#heading(level: 2)[#f.id: #inline-code(f.title)]
|
#heading(level: 2)[#f.id: #inline-code(f.title)]#finding-anchor(f.id)
|
||||||
#sev-chip(f.severity)
|
#sev-chip(f.severity)
|
||||||
#h(4pt)
|
#h(4pt)
|
||||||
#confidence-chip(f.confidence)
|
#confidence-chip(f.confidence)
|
||||||
|
|||||||
+146
-30
@@ -28,12 +28,15 @@ Shannon forwards only the selected provider's credential into the scan container
|
|||||||
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=your-api-key # the provider's API key
|
export SHANNON_AI_API_KEY=your-api-key # the provider's key — or the gateway's when a base URL is set
|
||||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||||
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route through a proxy or LLM gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
||||||
|
|
||||||
|
A model the catalogue does not carry is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration).
|
||||||
|
|
||||||
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -48,7 +51,15 @@ Review each vendor's guidance and complete the verification or enrollment they a
|
|||||||
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
||||||
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
||||||
|
|
||||||
This applies to the Anthropic and OpenAI providers, including when either is reached through a gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
This applies to the Anthropic and OpenAI providers, including when either is reached through an LLM gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||||
|
|
||||||
|
To confirm your model is ready before committing to a full scan, add `--validate-model` to `start`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @keygraph/shannon start -u https://your-app.com -r /path/to/repo --validate-model
|
||||||
|
```
|
||||||
|
|
||||||
|
The run performs the preflight model checks only — credential and registry resolution for any provider, plus a single cyber-access verification against Anthropic and OpenAI that trips the cyber safeguard if your account is not approved — then stops. No pentest or report is produced, and it needs no config. A decline fails the run with the vendor's enrollment link.
|
||||||
|
|
||||||
## Suggested models
|
## Suggested models
|
||||||
|
|
||||||
@@ -56,9 +67,9 @@ These are the models `npx @keygraph/shannon setup` offers, best-first. They are
|
|||||||
|
|
||||||
| Provider | Suggested model IDs |
|
| Provider | Suggested model IDs |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `anthropic` | `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
| `anthropic` | `claude-sonnet-5`, `claude-opus-5`, `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
||||||
| `openai` | `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
| `openai` | `gpt-6-sol`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
||||||
| `xai` | `grok-4.6`, `grok-4.5` |
|
| `xai` | `grok-4.7` |
|
||||||
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
||||||
|
|
||||||
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
||||||
@@ -78,14 +89,14 @@ OpenAI:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-...
|
export SHANNON_AI_API_KEY=sk-...
|
||||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
export SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
```
|
```
|
||||||
|
|
||||||
xAI:
|
xAI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=xai-...
|
export SHANNON_AI_API_KEY=xai-...
|
||||||
export SHANNON_AI_MODEL=xai:grok-4.5
|
export SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
```
|
```
|
||||||
|
|
||||||
Source-build mode reads the same variables from a `.env` file.
|
Source-build mode reads the same variables from a `.env` file.
|
||||||
@@ -104,17 +115,18 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens,
|
|||||||
|
|
||||||
## Custom base URL
|
## Custom base URL
|
||||||
|
|
||||||
To route model traffic through your own infrastructure — a corporate proxy, an LLM gateway such as LiteLLM, or a regional endpoint — set a base URL alongside your normal model selection. The provider half of `SHANNON_AI_MODEL` decides which key is sent and which API Shannon speaks, so pick the one your gateway serves:
|
`SHANNON_AI_BASE_URL` routes model traffic through a proxy or LLM gateway instead of the provider's default endpoint — an LLM gateway such as LiteLLM, a regional endpoint, or any other host you choose. It is a plain endpoint override: it changes only *where* requests go. The provider half of `SHANNON_AI_MODEL` still decides which credential is sent and which API dialect is spoken, and that is unchanged by the base URL.
|
||||||
|
|
||||||
| Gateway serves | Model prefix | API key |
|
This works for **any** provider, curated or not, subject to two rules. A provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect:
|
||||||
| --- | --- | --- |
|
|
||||||
| Anthropic Messages | `anthropic:` | `SHANNON_AI_API_KEY` |
|
|
||||||
| OpenAI Chat Completions | `openai:` | `SHANNON_AI_API_KEY` |
|
|
||||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `SHANNON_AI_API_KEY` |
|
|
||||||
|
|
||||||
The model ID is whatever name your gateway serves it under; it does not have to exist in Shannon's catalogue.
|
| Provider prefix | Dialect the endpoint must speak |
|
||||||
|
| --- | --- |
|
||||||
|
| `anthropic:` | Anthropic Messages |
|
||||||
|
| `openai:` | OpenAI Responses |
|
||||||
|
|
||||||
Anthropic Messages:
|
And the model ID must still resolve in the harness catalogue. A base URL changes only the address; it grants no exemption from that check. A gateway serving a model under its own name needs that name described in a [custom model configuration](#custom-model-configuration) file.
|
||||||
|
|
||||||
|
Anthropic Messages LLM gateway:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-ant-...
|
export SHANNON_AI_API_KEY=sk-ant-...
|
||||||
@@ -122,27 +134,130 @@ export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenAI Chat Completions:
|
OpenAI Responses LLM gateway:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-...
|
export SHANNON_AI_API_KEY=sk-...
|
||||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
export SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
`SHANNON_AI_MODEL` is always `<provider>:<model-id>`, gateway or not.
|
`npx @keygraph/shannon setup` configures a base URL two ways: **Custom Base URL** covers the common Anthropic Messages and OpenAI Responses LLM gateways, and **Other provider** takes any provider ID plus an optional base URL of its own.
|
||||||
|
|
||||||
OpenAI is the one provider serving two APIs, so a gateway run picks one:
|
## Custom model configuration
|
||||||
|
|
||||||
|
A custom model configuration is a Pi `models.json` file that describes a model the harness catalogue does not carry: one a router or gateway serves under its own ID, or a local server (see [Local and self-hosted models](#local-and-self-hosted-models)). You pass it with `--models-config`, and Shannon merges its definitions over the catalogue so `SHANNON_AI_MODEL` can then name the model like any other:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_OPENAI_FORMAT=responses # default: chat-completions
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Chat Completions is the default because that is what most gateway software exposes. Set `responses` for a gateway that passes the Responses API through — it preserves reasoning state between turns, which Chat Completions cannot. `openai:gpt-5` with no base URL always calls OpenAI's Responses API directly.
|
```bash
|
||||||
|
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||||
|
```
|
||||||
|
|
||||||
The variable is rejected in preflight where it cannot take effect: with a non-`openai` model, since Anthropic, xAI, and Bedrock each serve one API, and with no `SHANNON_AI_BASE_URL`, since a direct OpenAI run is always Responses.
|
[pi.dev/models](https://pi.dev/models) supplies the file contents. Find the model under the provider you want, since the same model has a different ID per provider, then open its page and expand **Show configuration** for a ready-to-paste snippet:
|
||||||
|
|
||||||
`npx @keygraph/shannon setup` covers this under **Custom Base URL**, which asks which API your gateway serves and configures the matching provider for you.
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"apiKey": "YOUR_API_KEY",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "z-ai/glm-5.3",
|
||||||
|
"name": "Z.ai: GLM 5.3",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"thinkingLevelMap": {
|
||||||
|
"off": null,
|
||||||
|
"minimal": null,
|
||||||
|
"low": "low",
|
||||||
|
"medium": null,
|
||||||
|
"high": "high",
|
||||||
|
"xhigh": null,
|
||||||
|
"max": "max"
|
||||||
|
},
|
||||||
|
"contextWindow": 1048576,
|
||||||
|
"maxTokens": 943718,
|
||||||
|
"cost": {
|
||||||
|
"input": 1.4,
|
||||||
|
"output": 4.4,
|
||||||
|
"cacheRead": 0.26,
|
||||||
|
"cacheWrite": 0
|
||||||
|
},
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"thinkingFormat": "openrouter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": "openai-completions",
|
||||||
|
"baseUrl": "https://openrouter.ai/api/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then name the model the usual way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SHANNON_AI_API_KEY=your-api-key
|
||||||
|
export SHANNON_AI_MODEL=openrouter:z-ai/glm-5.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave `YOUR_API_KEY` exactly as it is. Shannon sends the credential from your environment, and that takes precedence over anything the file declares, so the file describes the model and never has to hold a secret.
|
||||||
|
|
||||||
|
Pi's [models documentation](https://pi.dev/docs/latest/models) describes the full format, including provider routing preferences and compatibility flags.
|
||||||
|
|
||||||
|
## Local and self-hosted models
|
||||||
|
|
||||||
|
Ollama, LM Studio, vLLM, and any other OpenAI-compatible server are reached through the same mechanism. Describe the server as a provider in a model config file, then name its model with `SHANNON_AI_MODEL`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Use `host.docker.internal`, not `localhost`. The scan runs inside a container, so `localhost` points at the container itself rather than at your machine.
|
||||||
|
|
||||||
|
A `models.json` for Ollama:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://host.docker.internal:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "ollama",
|
||||||
|
"models": [
|
||||||
|
{ "id": "<model-id>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then name the model and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SHANNON_AI_API_KEY=ollama # any value, see below
|
||||||
|
export SHANNON_AI_MODEL=ollama:<model-id>
|
||||||
|
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||||
|
```
|
||||||
|
|
||||||
|
LM Studio and vLLM take the same shape on their own ports, `http://host.docker.internal:1234/v1` and `http://host.docker.internal:8000/v1` respectively. The provider name is yours to choose, and only has to match the prefix in `SHANNON_AI_MODEL`.
|
||||||
|
|
||||||
|
`SHANNON_AI_API_KEY` is still required even though a local server ignores it. Shannon checks that the selected provider has a credential before it starts, so set it to any placeholder value. It is sent to your server and discarded.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Shannon drives every phase through multi-turn tool use. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves.
|
||||||
|
|
||||||
|
Some servers need compatibility flags. If a reasoning-capable model is rejected, turn off the roles it does not understand, at either provider or model level:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Pi's [models documentation](https://pi.dev/docs/latest/models) lists the full set of compatibility flags and local-runtime options.
|
||||||
|
|
||||||
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
||||||
|
|
||||||
@@ -151,36 +266,36 @@ A ChatGPT Plus or Pro Codex subscription can run Shannon. Shannon reuses a login
|
|||||||
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
|
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
|
||||||
|
|
||||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `openai-codex` entry.
|
2. Start Pi by running `pi` in your terminal, then run `/login`, choose **Sign in with an account**, then choose **OpenAI Codex (legacy)** and complete the browser sign-in. This creates `~/.pi/agent/auth.json` with an `openai-codex` entry.
|
||||||
|
|
||||||
3. Select a Codex model and enable Pi authentication:
|
3. Select a Codex model and enable Pi authentication:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_USE_PI_AUTH=1
|
export SHANNON_USE_PI_AUTH=1
|
||||||
export SHANNON_AI_MODEL=openai-codex:gpt-5.5
|
export SHANNON_AI_MODEL=openai-codex:gpt-6-sol
|
||||||
```
|
```
|
||||||
|
|
||||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||||
|
|
||||||
Supported Codex models are `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
Supported Codex models are `gpt-6-sol`, `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
||||||
|
|
||||||
## xAI (Grok subscription)
|
## xAI (Grok subscription)
|
||||||
|
|
||||||
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
||||||
|
|
||||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
2. Start Pi by running `pi` in your terminal, then run `/login`, choose **Sign in with an account**, then choose **xAI** and complete the browser sign-in. This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
||||||
|
|
||||||
3. Select an xAI model and enable Pi authentication:
|
3. Select an xAI model and enable Pi authentication:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_USE_PI_AUTH=1
|
export SHANNON_USE_PI_AUTH=1
|
||||||
export SHANNON_AI_MODEL=xai:grok-4.6
|
export SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
```
|
```
|
||||||
|
|
||||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||||
|
|
||||||
Suggested Grok models are `grok-4.6` and `grok-4.5`.
|
The suggested Grok model is `grok-4.7`.
|
||||||
|
|
||||||
## Claude Code subscription
|
## Claude Code subscription
|
||||||
|
|
||||||
@@ -209,7 +324,8 @@ These instructions apply only to `shannon-v1`.
|
|||||||
|
|
||||||
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
||||||
|
|
||||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
|
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). To run a model the catalogue does not carry, describe it with [`--models-config`](#custom-model-configuration).
|
||||||
|
- **Model configuration** — when `--models-config` is passed, the file is parsed and schema-checked before the scan starts, and a fault fails preflight with the offending field named.
|
||||||
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
||||||
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
||||||
|
|
||||||
|
|||||||
@@ -179,3 +179,14 @@ login_flow:
|
|||||||
- "If prompted for 2FA, type $totp in <exact code field label or placeholder>"
|
- "If prompted for 2FA, type $totp in <exact code field label or placeholder>"
|
||||||
- "Click <exact button text>"
|
- "Click <exact button text>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Validating Authentication Only
|
||||||
|
|
||||||
|
To confirm your login flow works before committing to a full scan, add `--validate-auth` to `start`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @keygraph/shannon start -u https://your-app.com -r /path/to/repo -c config.yaml --validate-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
The run performs preflight and the single real login, then stops. No pentest, reconciliation, or report
|
||||||
|
is produced. It requires an `authentication` block in the config.
|
||||||
@@ -122,6 +122,9 @@ npx @keygraph/shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
|||||||
# Stream the log until the scan finishes, then exit on its outcome (useful in CI).
|
# Stream the log until the scan finishes, then exit on its outcome (useful in CI).
|
||||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --follow
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --follow
|
||||||
|
|
||||||
|
# Validate the configured login only, then stop (no pentest or report).
|
||||||
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth
|
||||||
|
|
||||||
# List running and completed scans.
|
# List running and completed scans.
|
||||||
npx @keygraph/shannon scans
|
npx @keygraph/shannon scans
|
||||||
```
|
```
|
||||||
@@ -134,6 +137,7 @@ Source-build examples:
|
|||||||
./shannon start -u https://example.com -r /path/to/repo -o ./my-reports
|
./shannon start -u https://example.com -r /path/to/repo -o ./my-reports
|
||||||
./shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
./shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
||||||
./shannon start -u https://example.com -r /path/to/repo --follow
|
./shannon start -u https://example.com -r /path/to/repo --follow
|
||||||
|
./shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth
|
||||||
./shannon scans
|
./shannon scans
|
||||||
|
|
||||||
# Rebuild the worker image.
|
# Rebuild the worker image.
|
||||||
|
|||||||
+56
-88
@@ -1,133 +1,101 @@
|
|||||||
# Keygraph Enterprise Platform
|
# Keygraph Enterprise Platform
|
||||||
|
|
||||||
Shannon 3.0 makes advanced, code-informed autonomous pentesting available to everyone. The open-source CLI maps routes and data flows, understands application architecture, executes real attacks, and produces PDF and SARIF results—locally, in CI/CD, or fully air-gapped with your own model.
|
Shannon 3.0 is an open-source pentester. It reads your source, maps routes and data flows, runs real attacks against a live target, and writes PDF and SARIF reports. It runs locally, in CI, or air-gapped with your own model. Shannon Open Source is a complete pentester, not a trial edition.
|
||||||
|
|
||||||
The **Keygraph Enterprise Platform** is the commercial AppSec operating system for organizations that need to run that process continuously across many repositories, teams, and environments. It adds exhaustive agentic SAST, business-logic and source-to-sink analysis, broader scanner coverage, centralized vulnerability management, automated remediation and targeted verification, enterprise governance, and organization-wide reporting.
|
Keygraph Enterprise runs an enterprise-hardened fork of Shannon continuously across hundreds of repositories and adds what a security team needs around it: audit-depth static analysis on a parsed code graph, business-logic testing, SCA and secrets scanning, one deduplicated record per vulnerability across scans and scanners, generated fixes, fix verification, and SSO, RBAC, and audit logs. It is for security teams that own vulnerability management across many engineering teams and need one place to triage, assign, fix, and verify.
|
||||||
|
|
||||||
> Shannon Open Source is a complete autonomous pentester, not a trial edition. Keygraph Enterprise is for teams that need greater analysis depth, shared control, and a closed-loop vulnerability-management program.
|
Both editions are BYOK. Keygraph never receives your source and never proxies model traffic, open source or commercial. Shannon Open Source runs from your machine or CI runner. Keygraph Enterprise deploys as a platform inside your cloud or data center, including fully air-gapped.
|
||||||
|
|
||||||
## Who It Is For
|
## Shannon Open Source vs. Keygraph Enterprise
|
||||||
|
|
||||||
Keygraph Enterprise is designed for organizations that need to:
|
| | Shannon Open Source | Keygraph Enterprise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Best for | Developers and teams running repository-level pentests locally or in CI | Security organizations running continuous AppSec across many teams and repositories |
|
||||||
|
| Code analysis | Agent pass over architecture, entry points, and data flows to seed the pentest, sized to finish inside a CI run | Persistent code property graph plus a long-running analysis harness with interprocedural taint, sanitizer modeling, cross-repo context, exploit chains, and multi-pass review |
|
||||||
|
| Pentesting | On-demand, source-aware white-box pentesting with optional authenticated testing, focused on injection, XSS, SSRF, broken authentication, and broken authorization, with proof by exploitation | Enterprise-hardened Shannon fork run continuously, with grey-box and black-box targets and business-logic invariant testing |
|
||||||
|
| SCA and secrets | Not included | SCA with reachability and secrets scanning including history |
|
||||||
|
| Findings | Per-run PDF, Markdown, JSON, and SARIF, with SARIF ingestion into GitHub code scanning | One record per vulnerability per repo across scans and scanners, plus ownership, SLAs, dashboards, and audit evidence |
|
||||||
|
| Fixes and verification | Not included | Fix PRs with verification by re-analysis and exploit replay, with no full rescan required |
|
||||||
|
| CI/CD and source control | GitHub Action and GitLab CI component for pull-request, release, and scheduled runs, with gates on `status: exploited` | GitHub, GitLab, Azure DevOps, and Bitbucket with organization-wide policy and centrally managed integrations |
|
||||||
|
| Deployment and models | Runs locally or on a CI runner with BYOK to any Anthropic- or OpenAI-compatible endpoint or local model | Deployed in your AWS, GCP, Azure, or on-prem environment. Customer-hosted services and stored platform data remain inside your environment. Model requests go directly to the provider, private endpoint, gateway, or local model you configure. A local model supports fully disconnected deployments |
|
||||||
|
| Governance, license, support | AGPL-3.0 and community support | SSO, SCIM, RBAC, and audit logs, plus a commercial license, enterprise support, and SOC 2 Type II |
|
||||||
|
|
||||||
- continuously test hundreds or thousands of repositories, services, applications, and APIs;
|
## How it fits your pipeline
|
||||||
- combine agentic pentesting, SAST, SCA, secrets, and business-logic findings in one system;
|
|
||||||
- enforce security policy in GitHub Actions, GitLab CI, and enterprise delivery pipelines;
|
|
||||||
- give developers one canonical, actionable record for each vulnerability instead of duplicate scanner alerts;
|
|
||||||
- assign owners, apply SLAs, track status, and measure risk and remediation performance across the organization;
|
|
||||||
- generate fixes and verify them without rerunning an entire scan;
|
|
||||||
- enforce enterprise identity, authorization, audit, and API-access controls; and
|
|
||||||
- deploy fully on-premises or air-gapped with customer-controlled models, keys, and routing.
|
|
||||||
|
|
||||||
## Close the Entire AppSec Loop
|
1. Scans run on pull requests, releases, and a schedule against repositories in GitHub, GitLab, Azure DevOps, or Bitbucket.
|
||||||
|
2. Pipelines gate on exploited severity. A code-analysis hypothesis never fails a build.
|
||||||
|
3. Findings from every scanner and every run land as one record per vulnerability per repository, with an owner and an SLA. The same finding across ten runs is one record, not ten alerts.
|
||||||
|
4. From a finding, Keygraph opens a fix PR into your normal review flow.
|
||||||
|
5. Verification confirms the fix against the changed code and the original exploit. No full rescan is required.
|
||||||
|
|
||||||
The platform connects discovery, triage, remediation, and verification in one continuous workflow:
|
## What is different technically
|
||||||
|
|
||||||
1. **Analyze** every repository with exhaustive agentic SAST and complementary scanners.
|
### Static analysis on a code property graph
|
||||||
2. **Prove** exploitability with source-aware white-box, black-box, and grey-box pentesting.
|
|
||||||
3. **Normalize and deduplicate** results into a canonical finding per vulnerability and repository.
|
|
||||||
4. **Prioritize and assign** using severity, reachability, exploit evidence, ownership, policy, and business context.
|
|
||||||
5. **Remediate** with an AI-authored patch delivered as a reviewable pull request.
|
|
||||||
6. **Verify** the specific fix with deterministic checks and adversarial agent reasoning—without rerunning the full scan.
|
|
||||||
7. **Track and govern** status, exceptions, SLAs, audit history, trends, and compliance evidence until closure.
|
|
||||||
|
|
||||||
## Exhaustive Agentic SAST
|
Shannon Open Source's code analysis is sized to finish inside a CI run: agents read the repository, map the attack surface, and hand candidates to the pentester. Enterprise is built for depth instead. It first parses each repository into a persistent code property graph, then runs an analysis harness derived from one built for long-running vulnerability audits, heavily adapted to query the graph rather than read files. The harness decomposes the application into risk, taint-flow, framework, and specialist tasks and supports longer-running audit workflows beyond typical CI job windows.
|
||||||
|
|
||||||
Shannon 3.0's open-source code analysis runs a multi-stage agentic workflow. It models application architecture, trust boundaries, exposed interfaces, and data flows, opens targeted investigations, reviews the candidates they turn up, and hands the survivors to live pentesting agents. That workflow is built for practical local and CI/CD runs.
|
On the graph, it performs:
|
||||||
|
|
||||||
The Enterprise engine goes further, for audits at organization scale. It parses the codebase and builds persistent structural context before agents start reasoning about security:
|
- Interprocedural taint tracking across functions, files, fields, containers, and framework request lifecycles.
|
||||||
|
- Source, sink, and sanitizer modeling that records where validation, encoding, or authorization changes a path.
|
||||||
|
- Cross-repository modeling of services, entry points, and trust boundaries.
|
||||||
|
- Semantic deduplication of variants of the same defect, and exploit-chain analysis for combinations with higher impact than any single issue.
|
||||||
|
- Multiple review passes per candidate, checking the agent's claim against the graph and available deployment and configuration context. Candidates that cannot be substantiated are not reported.
|
||||||
|
|
||||||
- **Repository and architecture modeling** identifies services, frameworks, entry points, assets, trust boundaries, and cross-repository relationships.
|
### Business-logic invariants
|
||||||
- **Interprocedural call and data-flow analysis** traces values across functions, files, fields, containers, and framework-managed request lifecycles.
|
|
||||||
- **Source, sink, and sanitizer modeling** follows untrusted input to sensitive operations and records where validation, encoding, authorization, or other controls alter the path.
|
|
||||||
- **Threat-driven decomposition** breaks large applications into risk, taint-flow, framework, and specialist analysis tasks so deep scans remain systematic.
|
|
||||||
- **Exhaustive adversarial verification** challenges candidates across multiple review passes, weighing structural evidence against what the agents found, then asks whether each one is viable in the application's production configuration.
|
|
||||||
- **Semantic deduplication and exploit-chain analysis** consolidate variants of the same defect and identify combinations whose impact is greater than any isolated issue.
|
|
||||||
- **Business-logic invariant testing** derives rules the code is supposed to preserve—such as tenant isolation, workflow order, approval limits, balances, and state transitions—then agents fuzz those invariants for application-specific flaws.
|
|
||||||
|
|
||||||
The result is broad vulnerability hunting with precise paths back to the relevant code, not a flat list of pattern matches.
|
Shannon Open Source focuses on injection, XSS, SSRF, and broken authentication and authorization. Enterprise adds testing for the bugs that do not fit a vulnerability class: it derives invariants the application is supposed to hold (tenant isolation, workflow ordering, approval limits, balance conservation, state transitions) and tests them against the running application. This is where application-specific vulnerabilities live and where pattern-based SAST often provides little or no signal.
|
||||||
|
|
||||||
|
### Proof by exploitation
|
||||||
|
|
||||||
|
The pentesting engine is a hardened fork of Shannon with the same rule: a pentest finding requires a working exploit. No exploit, no finding. Enterprise stores the exploit and replays it later to verify the fix.
|
||||||
|
|
||||||
|
SCA prioritizes vulnerable dependencies that application code actually reaches. Secrets scanning covers current source and repository history.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise SAST results grouped into business-logic issues, point issues, and secrets" width="100%">
|
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise findings grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Complete Application-Security Coverage
|
## Findings
|
||||||
|
|
||||||
Agentic SAST and pentesting work alongside additional first-class scanners:
|
Shannon Open Source hands you a report per scan. Enterprise dedupes across runs and across scanners, deterministically and semantically, into one record per vulnerability per repository. Each record carries evidence, source location, severity, scan history, status, owner, resolution, and last-verified state.
|
||||||
|
|
||||||
- **SCA with reachability** prioritizes vulnerable dependencies that application code can actually reach.
|
Workflows cover assignment, triage, false-positive and risk-acceptance decisions, and SLA policies with escalation and aging. Dashboards report open risk, coverage, new versus resolved, SLA compliance, and MTTR, exportable as evidence for customers and auditors.
|
||||||
- **Full secrets scanning** detects credentials, tokens, and keys across source and repository history.
|
|
||||||
- **Agentic pentesting** correlates code intelligence with live application behavior and attempts real exploitation. The core rule remains: no exploit, no pentest finding.
|
|
||||||
|
|
||||||
## One System of Record for Every Finding
|
Findings still require human review. Enterprise's extra review passes reduce weakly supported findings, but they do not eliminate them.
|
||||||
|
|
||||||
Keygraph ingests results from every analysis source, correlates them, and maintains one canonical finding per vulnerability per repository. Security and engineering teams work from the same record, with evidence, source location, severity, scan history, status, assignee, resolution, and last-verification state.
|
|
||||||
|
|
||||||
The vulnerability-management layer provides:
|
|
||||||
|
|
||||||
- deterministic and semantic deduplication across scans and scanners;
|
|
||||||
- ownership, assignment, triage, false-positive, risk-acceptance, and resolution workflows;
|
|
||||||
- SLA policies, escalation, aging, and last-verified tracking;
|
|
||||||
- bidirectional developer-workflow integrations and APIs;
|
|
||||||
- dashboards for risk, coverage, trends, new versus resolved findings, SLA compliance, and MTTR; and
|
|
||||||
- exportable evidence for customers, auditors, and compliance programs.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise canonical findings inventory with severity, status, source, and verification filters" width="100%">
|
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise findings inventory with severity, status, source, and verification filters" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Remediate, Then Verify the Fix
|
### Fix and verify
|
||||||
|
|
||||||
From an individual finding, a user can ask Keygraph to produce a focused patch. The remediation agent reasons from the root cause and evidence, changes only the required code, and opens a pull request into the existing review process. It does not silently apply fixes to a protected branch.
|
From a finding, Keygraph generates a patch scoped to that finding and opens a pull request. It never commits to a protected branch.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
After a patch is available, targeted verification re-analyzes the affected code and, for dynamic pentest findings, re-tests the original proof of concept against the target. Deterministic checks and adversarial agent reasoning produce a clear verdict without the cost and delay of rerunning the entire scan.
|
Verification re-analyzes the changed code and, for pentest findings, replays the original exploit against the patched target. The verdict comes from deterministic checks plus a review pass, without rerunning the full scan.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise targeted finding-verification workflow" width="100%">
|
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise finding-verification workflow" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Enterprise Governance and Integrations
|
## Deployment and access control
|
||||||
|
|
||||||
Keygraph is built for shared operation across security, platform, and engineering teams:
|
Keygraph Enterprise deploys entirely inside your AWS, GCP, Azure, or on-prem environment, including networks with no internet egress. There is no Keygraph-operated control plane. Customer-hosted services and stored platform data remain inside your environment for the life of the deployment.
|
||||||
|
|
||||||
- SAML 2.0 or OIDC single sign-on and SCIM provisioning;
|
Model access is BYOK and BYOM. Route workloads to Anthropic, OpenAI, xAI, or Bedrock, a private cloud endpoint, your own gateway such as LiteLLM with your routing and policy applied, or local models on vLLM or Ollama. Model requests go directly to the endpoint you configure. Keygraph never receives or proxies them. A local model supports a fully disconnected deployment.
|
||||||
- organization, team, and user management;
|
|
||||||
- built-in and custom roles with granular relationship-, attribute-, and role-based authorization (ReBAC, ABAC, and RBAC);
|
Access control: SAML/OIDC SSO, SCIM, roles with repository-scoped visibility (RBAC, plus attribute and relationship rules where needed), full audit log, scoped API keys.
|
||||||
- repository, pentest-profile, scanner, finding, and administration boundaries;
|
|
||||||
- full audit logging and scoped API keys;
|
|
||||||
- integrations with source control, CI/CD, ticketing, chat, and cloud environments; and
|
|
||||||
- commercial support and enterprise onboarding.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise granular roles and repository visibility controls" width="100%">
|
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise roles and repository visibility controls" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## On-Premises, Air-Gapped, and Customer-Controlled AI
|
Keygraph maintains a SOC 2 Type II audit. The report is available to customers under NDA.
|
||||||
|
|
||||||
Keygraph Enterprise can run entirely inside your AWS, GCP, Azure, or on-premises environment, including networks with no public internet access. Deployments can keep source code, scan artifacts, findings, prompts, completions, and model traffic inside your security perimeter.
|
|
||||||
|
|
||||||
AI access is bring-your-own-key and bring-your-own-model. Organizations can route workloads through approved commercial providers, private cloud endpoints, an internal LLM gateway, or local open-source models, with granular routing and policy controlled by the customer. There is no requirement for a Keygraph-operated control plane or model proxy.
|
|
||||||
|
|
||||||
Keygraph maintains a SOC 2 Type II audit and makes the current report available to customers under appropriate confidentiality terms.
|
|
||||||
|
|
||||||
## Shannon 3.0 vs. Keygraph Enterprise
|
|
||||||
|
|
||||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Best for | Individual developers and teams running pentests locally or in CI/CD | Security organizations running a continuous AppSec program across many teams and repositories |
|
|
||||||
| Code analysis | Multi-stage agentic review maps architecture, trust boundaries, exposed interfaces, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code analysis: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic invariant testing |
|
|
||||||
| Pentesting | On-demand, source-aware white-box pentesting with proof by exploitation | Continuous white-box, black-box, and grey-box pentesting across applications and environments |
|
|
||||||
| Additional AppSec coverage | Not included | SCA with reachability, secrets scanning, and business-logic invariant testing |
|
|
||||||
| CI/CD and reporting | Official GitHub Action and reusable GitLab CI/CD component; staging, release, merge-request, and scheduled pentests; demonstrated-vulnerability severity gates; PDF, Markdown, JSON, SARIF, artifacts, and native security-workflow ingestion | Organization-wide policies and gating, centrally managed integrations, canonical findings, dashboards, analytics, SLA tracking, and compliance evidence |
|
|
||||||
| Automated remediation and verification | Not included | AI-authored pull requests with targeted code and exploit verification |
|
|
||||||
| Enterprise governance | N/A — local, single-operator CLI | SSO, SCIM, teams, ReBAC/ABAC/RBAC, audit logs, API keys, ownership, and SLA policies |
|
|
||||||
| Deployment and AI | Self-hosted, no telemetry, BYOM, and fully air-gapped with a local model | Fully on-premises or air-gapped, BYOK/BYOM, and granular routing through customer-controlled gateways |
|
|
||||||
| License and support | AGPL-3.0 and community support | Commercial license, enterprise support, and SOC 2 Type II controls |
|
|
||||||
|
|
||||||
## Talk to Keygraph
|
## Talk to Keygraph
|
||||||
|
|
||||||
Visit [keygraph.io](https://keygraph.io), book a [Keygraph demo](https://cal.com/team/keygraph/shannon-pro), or contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
Visit [keygraph.io](https://keygraph.io), book a [demo](https://cal.com/team/keygraph/shannon-pro), or email [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||||
@@ -132,6 +132,6 @@ Exploit-mode scans write SARIF 2.1.0 by default, so findings land in GitHub code
|
|||||||
|
|
||||||
### Conclusion
|
### Conclusion
|
||||||
|
|
||||||
The complaint we hear most from CISOs about AI pentesting isn't accuracy, it's cost. At $4,000 a scan, the tier Doyensec purchased from both XBOW and Aikido, the math works for an annual check-up. It doesn't work per release, and teams are shipping faster than ever. A large enterprise with 5,000 repositories is looking at $20 million for a single pass.
|
The complaint we hear most from CISOs about AI pentesting is the economics of continuous coverage. Doyensec paid $4,000 for each Aikido and XBOW assessment. At that benchmark price, treating 5,000 repositories as separate assessment targets would imply $20 million for one portfolio-wide pass before enterprise discounts. That may be viable for selective annual testing, but not for testing an entire portfolio at release cadence.
|
||||||
|
|
||||||
Shannon v3 running DeepSeek v4 Flash scanned Photoview for $6.10 and caught the same critical SQL injection the $4,000 platforms caught. The same enterprise would pay about $30,000 for that pass. The cheap model doesn't catch everything: our Claude Opus 5 run found 6 of 7 patched vulnerabilities to DeepSeek's 3, for $115. That's the point. Run the cheap model on every change, run the heavy one on a schedule, and continuous pentesting becomes affordable.
|
Shannon v3 running DeepSeek v4 Flash scanned Photoview for $6.10 and caught the same critical SQL injection the $4,000 platforms caught. At the observed DeepSeek model cost, 5,000 equivalent Shannon runs would consume about $30,500 in model tokens, before infrastructure and operational costs. The cheap model doesn't catch everything: our Claude Opus 5 run found 6 of 7 patched vulnerabilities to DeepSeek's 3, for $115. These results support a tiered testing strategy in which teams use lower-cost models for frequent scans and more capable models for periodic deeper assessments, making continuous pentesting economically practical.
|
||||||
+306
-169
@@ -1,20 +1,23 @@
|
|||||||
# Shannon Full LLM Context
|
# Shannon Full LLM Context
|
||||||
|
|
||||||
> Combined README and documentation for AI agents and LLMs. This file is a hand-maintained copy of the repository Markdown files, updated by hand whenever those files change. For the concise index, see [llms.txt](llms.txt).
|
> Combined README and documentation for AI agents and LLMs, generated verbatim from the local files referenced in llms.txt. For the concise index, see [llms.txt](llms.txt).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# File: README.md
|
# File: README.md
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Shannon 3.0 is live:** deeper security code analysis, a rebuilt terminal experience, native CI/CD workflows, professional PDF reports, and SARIF—still fully open source, self-hosted, and bring-your-own-model.
|
> **[Shannon 3.0 is live](https://github.com/KeygraphHQ/shannon/discussions/439):** deeper security code analysis, more thoroughly vetted findings, a rebuilt CLI, native CI/CD, professional PDF reports, and SARIF.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="./assets/github-banner-dark.png">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="./assets/github-banner-light.png">
|
||||||
|
<img src="./assets/github-banner-light.png" alt="Shannon, AI Pentester for Web Apps and APIs, by Keygraph" width="100%">
|
||||||
|
</picture>
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/15604" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15604" alt="KeygraphHQ%2Fshannon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Shannon is an autonomous, AI pentester for web applications and APIs.
|
### Shannon is an autonomous, AI pentester for web applications and APIs.
|
||||||
|
|
||||||
@@ -22,13 +25,21 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
|||||||
|
|
||||||
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
||||||
|
|
||||||
---
|
<p><strong>Launch Shannon</strong></p>
|
||||||
|
|
||||||
 
|
```bash
|
||||||
|
npx @keygraph/shannon@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
<sub>The interactive launcher will guide you through setup and your first pentest.</sub>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a> <a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **AI agents and LLMs:** start with [llms.txt](llms.txt) for a concise map of this repository, or use [llms-full.txt](llms-full.txt) for the README and docs combined into one file.
|
> **AI agents and LLMs:** start with [llms.txt](llms.txt) for a concise map of this repository, or use [llms-full.txt](llms-full.txt) for the README and docs combined into one file.
|
||||||
@@ -37,19 +48,33 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
|||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
- [What is Shannon?](#what-is-shannon)
|
- [What is Shannon?](#what-is-shannon)
|
||||||
|
- [Why Shannon Exists](#why-shannon-exists)
|
||||||
|
- [Why "Shannon"?](#why-shannon)
|
||||||
|
- [Not a replacement for human pentesters](#not-a-replacement-for-human-pentesters)
|
||||||
- [Shannon in Action](#shannon-in-action)
|
- [Shannon in Action](#shannon-in-action)
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Run Shannon](#run-shannon)
|
||||||
- [Key Capabilities](#key-capabilities)
|
- [Key Capabilities](#key-capabilities)
|
||||||
- [CI/CD Integrations](#cicd-integrations)
|
- [CI/CD Integrations](#cicd-integrations)
|
||||||
|
- [GitHub Actions](#github-actions)
|
||||||
- [Editions](#editions)
|
- [Editions](#editions)
|
||||||
- [Architecture](#architecture)
|
- [Architecture](#architecture)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
|
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
- [About Keygraph](#about-keygraph)
|
- [About Keygraph](#about-keygraph)
|
||||||
- [Community and Support](#community-and-support)
|
- [Community and Support](#community-and-support)
|
||||||
- [Common Questions](#common-questions)
|
- [Common Questions](#common-questions)
|
||||||
|
- [Can I self-host Shannon?](#can-i-self-host-shannon)
|
||||||
|
- [Does Shannon support bring your own key (BYOK)?](#does-shannon-support-bring-your-own-key-byok)
|
||||||
|
- [Does Shannon output SARIF?](#does-shannon-output-sarif)
|
||||||
|
- [Which AI providers does Shannon support?](#which-ai-providers-does-shannon-support)
|
||||||
|
- [Can I run Shannon on a local or self-hosted model?](#can-i-run-shannon-on-a-local-or-self-hosted-model)
|
||||||
|
- [Does Shannon actually exploit vulnerabilities, or just scan?](#does-shannon-actually-exploit-vulnerabilities-or-just-scan)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -61,33 +86,50 @@ Shannon analyzes your web application's source code to identify potential attack
|
|||||||
|
|
||||||
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
||||||
|
|
||||||
### Why Shannon Exists
|
<a id="why-shannon-exists"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Why Shannon Exists</strong></summary>
|
||||||
|
|
||||||
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
||||||
|
|
||||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||||
|
|
||||||
### Why "Shannon"?
|
</details>
|
||||||
|
|
||||||
|
<a id="why-shannon"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Why "Shannon"?</strong></summary>
|
||||||
|
|
||||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||||
|
|
||||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<a id="not-a-replacement-for-human-pentesters"></a>
|
||||||
|
<details>
|
||||||
|
<summary><strong>Not a replacement for human pentesters</strong></summary>
|
||||||
|
|
||||||
|
Shannon is built to work alongside expert pentesters and red teamers, not replace them. Great pentesters understand the business, chain attacks in ways nobody anticipated, and bring years of judgment that current models can't match.
|
||||||
|
|
||||||
|
Shannon solves a different problem: there is far more software to test than security teams have time to cover. Critical systems get periodic expert assessments, while the long tail of internal apps, APIs, and fast-moving services rarely gets tested at all.
|
||||||
|
|
||||||
|
Shannon shifts pentesting left into the software development lifecycle (SDLC). Use it to run exploitation-backed tests against staging environments and releases at the cadence they actually ship, and save expert human time for the risks that need someone who knows the organization.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Shannon in Action
|
## Shannon in Action
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Sample penetration test reports from intentionally vulnerable applications, produced by Shannon Open Source:
|
These reports are from Shannon Open Source scans of Photoview 2.4.0, one of the applications in Doyensec's comparison of Aikido and XBOW. We ran Shannon against the same application version and evaluated its results separately. Read the [Doyensec study](https://doyensec.com/resources/ComparingAIApplicationSecurityTestingPlatforms_Doyensec.pdf) and our [Shannon follow-up comparison](docs/shannon-xbow-aikido-benchmark.md) for the methodology, limitations, costs, and results.
|
||||||
|
|
||||||
|
|
||||||
| Target | Summary | Report |
|
|
||||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
||||||
| OWASP Juice Shop | 20+ vulnerabilities, including authentication bypass, SQL injection, IDOR, and SSRF. | [View report](sample-reports/shannon-report-juice-shop.md) |
|
|
||||||
| c{api}tal API | Approximately 15 critical and high-severity API findings, including command injection, auth bypass, and mass assignment. | [View report](sample-reports/shannon-report-capital-api.md) |
|
|
||||||
| OWASP crAPI | 15+ critical and high-severity findings across JWT, injection, SSRF, and API authorization paths. | [View report](sample-reports/shannon-report-crapi.md) |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Model | Report | SARIF |
|
||||||
|
| ----------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||||
|
| DeepSeek v4 Flash | [View report](benchmark/photoview-deepseek-v4-flash.pdf) | [SARIF](benchmark/photoview-deepseek-v4-flash.sarif) |
|
||||||
|
| Grok 4.6 | [View report](benchmark/photoview-grok-4-6.pdf) | [SARIF](benchmark/photoview-grok-4-6.sarif) |
|
||||||
|
| Claude Opus 5 | [View report](benchmark/photoview-opus-5.pdf) | [SARIF](benchmark/photoview-opus-5.sarif) |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -97,7 +139,7 @@ Sample penetration test reports from intentionally vulnerable applications, prod
|
|||||||
|
|
||||||
- **Docker**: required for the worker container.
|
- **Docker**: required for the worker container.
|
||||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a [custom base URL](docs/ai-providers.md#custom-base-url). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue — each of which you can point at a proxy or LLM gateway through a [custom base URL](docs/ai-providers.md#custom-base-url), and a model the catalogue does not carry can be described with a [custom model configuration](docs/ai-providers.md#custom-model-configuration). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
||||||
|
|
||||||
|
|
||||||
@@ -132,17 +174,17 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
|||||||
|
|
||||||
## Key Capabilities
|
## Key Capabilities
|
||||||
|
|
||||||
- **No exploit, no report**: Shannon includes a vulnerability only after validating it with a working, reproducible proof of concept—eliminating the speculative warnings typical of scanners.
|
- **No exploit, no report**: Reports only vulnerabilities confirmed with a reproducible proof of concept, reducing speculative scanner noise.
|
||||||
- **Advanced security code analysis**: Before it sends a single payload, Shannon reads the codebase and builds a picture of the application: architecture, trust boundaries, exposed interfaces, data flows, and the assets worth attacking. From there it opens targeted investigations and filters the candidates they turn up. What survives goes to the live pentesting agents.
|
- **Advanced code analysis**: Maps architecture, trust boundaries, interfaces, data flows, and critical assets before sending credible attack paths to live pentesting agents.
|
||||||
- **Autonomous execution**: Shannon launches reconnaissance, vulnerability analysis, exploitation, and report generation from a single command.
|
- **Autonomous execution**: Runs reconnaissance, analysis, exploitation, and reporting from a single command.
|
||||||
- **Live terminal experience**: A rebuilt CLI makes scans easy to configure and shows agent progress and clean results without requiring operators to inspect the underlying orchestration logs.
|
- **Live terminal experience**: Simplifies scan setup and shows agent progress and results without exposing orchestration logs.
|
||||||
- **Authenticated testing**: configuration files can describe login flows, test credentials, TOTP, email-based login flows, focus areas, and rules of engagement.
|
- **Authenticated testing**: Supports credentials, login flows, TOTP, email authentication, focus areas, and rules of engagement through configuration.
|
||||||
- **OWASP-focused coverage**: Shannon targets exploitable Injection, XSS, SSRF, Broken Authentication, and Broken Authorization issues.
|
- **OWASP-focused coverage**: Tests for exploitable injection, XSS, SSRF, broken authentication, and broken authorization.
|
||||||
- **Resumable workspaces**: Shannon can resume interrupted runs without re-running completed agents.
|
- **Resumable workspaces**: Resumes interrupted scans without repeating completed work.
|
||||||
- **Native CI/CD integrations**: Run Shannon through the official GitHub Action or reusable GitLab CI/CD component. Preserve reports, SARIF, and logs as pipeline artifacts; publish findings into native security workflows; and gate releases only on vulnerabilities Shannon actually demonstrates.
|
- **Native CI/CD integrations**: Runs through the official GitHub Action or GitLab CI/CD component, preserves artifacts, publishes findings, and gates releases on proven vulnerabilities.
|
||||||
- **Professional and machine-readable reports**: Shannon generates evidence-rich PDF and Markdown reports plus structured JSON and SARIF 2.1.0. SARIF is enabled by default on exploit-mode scans and can be disabled with `report.sarif: "false"`.
|
- **Multi-format reports**: Produces evidence-rich PDF and Markdown reports plus JSON and SARIF 2.1.0. SARIF is enabled by default for exploit-mode scans.
|
||||||
- **Bring your own key, provider-agnostic**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any endpoint speaking the Anthropic Messages API or the OpenAI Chat Completions or Responses API, including self-hosted models served through Ollama, vLLM, or LM Studio and gateways such as OpenRouter and LiteLLM. You supply the credentials and choose exactly where model traffic goes. Local and self-hosted models are supported.
|
- **Provider agnostic and BYOK**: Supports Anthropic, OpenAI, xAI, AWS Bedrock, compatible APIs and LLM gateways, and local models served through Ollama, vLLM, or LM Studio.
|
||||||
- **Private by design**: Shannon runs inside your infrastructure and writes results to a local workspace. Model requests go straight to the provider or endpoint you configure, and they carry source and application context with them, so choose that endpoint deliberately. Point Shannon at a local model endpoint and nothing leaves your environment.
|
- **Private by design**: Runs in your infrastructure, stores results locally, and sends model requests directly to your chosen endpoint. A local endpoint keeps data inside your environment.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -201,24 +243,11 @@ See the [Shannon GitHub Action documentation](https://github.com/KeygraphHQ/shan
|
|||||||
|
|
||||||
## Editions
|
## Editions
|
||||||
|
|
||||||
**Shannon Open Source** is the complete autonomous pentester for developers and security teams. It is optimized for fast local and CI/CD runs: understand the application, execute real attacks, and report only proven vulnerabilities.
|
**Shannon Open Source** is a complete autonomous pentester, especially well suited to individual developers and small teams running focused security tests locally or in CI/CD.
|
||||||
|
|
||||||
**Keygraph Enterprise Platform** turns Shannon's proof engine into an organization-wide AppSec program, adding exhaustive analysis, centralized vulnerability management, automated remediation, enterprise governance, and continuous operation at scale.
|
**Keygraph Enterprise Platform** is for organizations that need a shared platform for continuous agentic pentesting/AppSec across many teams, repositories, and environments. It centralizes deeper analysis, vulnerability management, remediation, verification, governance, and reporting so teams do not have to assemble and maintain those workflows themselves.
|
||||||
|
|
||||||
|
[Learn about the Keygraph Enterprise Platform and compare editions →](docs/keygraph-platform.md)
|
||||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
|
||||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| Best for | Local and CI/CD pentesting | Continuous AppSec across teams and repositories |
|
|
||||||
| Security analysis | Multi-stage agentic review models architecture, trust boundaries, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code agentic SAST: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic testing |
|
|
||||||
| Additional coverage | Not included | SCA with reachability, secrets scanning, and business-logic testing |
|
|
||||||
| AppSec operations | N/A — standalone CLI | Canonical findings, deduplication, SLAs, analytics, automated remediation, and targeted verification |
|
|
||||||
| Governance | N/A — local, single-operator CLI | SSO, SCIM, granular access control, APIs, and full audit logging |
|
|
||||||
| Deployment | Self-hosted, air-gapped, BYOM, AGPL-3.0 | On-premises or air-gapped, granular model routing, commercial support |
|
|
||||||
|
|
||||||
|
|
||||||
Shannon Open Source is not a trial edition. Choose Keygraph Enterprise when you need deeper analysis and a governed, closed-loop AppSec program.
|
|
||||||
|
|
||||||
[Explore the Keygraph Enterprise Platform →](docs/keygraph-platform.md)
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -265,7 +294,7 @@ Use these guides for operational detail:
|
|||||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. |
|
| [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, and report filters. |
|
| [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, and report filters. |
|
||||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom gateways. |
|
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom LLM gateways. |
|
||||||
| [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. |
|
| [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. |
|
| [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. |
|
| [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. |
|
||||||
@@ -285,7 +314,7 @@ Important limitations:
|
|||||||
|
|
||||||
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
||||||
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
||||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any Anthropic Messages API or OpenAI Chat Completions or Responses API endpoint works through a custom base URL. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any other provider in the harness catalogue works too — each reachable through a custom base URL that points it at a proxy or LLM gateway. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||||
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
||||||
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
||||||
|
|
||||||
@@ -299,6 +328,14 @@ Commercial and enterprise licensing is available for organizations that need dif
|
|||||||
|
|
||||||
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Thanks to [Pi](https://github.com/earendil-works/pi),
|
||||||
|
[Playwright CLI](https://github.com/microsoft/playwright-cli),
|
||||||
|
and [Mantis](https://github.com/google/mantis).
|
||||||
|
|
||||||
|
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for licensing and attribution details.
|
||||||
|
|
||||||
## About Keygraph
|
## About Keygraph
|
||||||
|
|
||||||
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
|
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
|
||||||
@@ -346,17 +383,18 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re
|
|||||||
|
|
||||||
### Which AI providers does Shannon support?
|
### Which AI providers does Shannon support?
|
||||||
|
|
||||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any endpoint that implements the Anthropic Messages API or the OpenAI Chat Completions or Responses API, reached through a custom base URL. The rule is the API format, not the vendor. Shannon uses a single unified model setting throughout a pentest.
|
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any provider in the Pi harness catalogue, named the same `<provider>:<model-id>` way. Any provider can be pointed at a proxy or LLM gateway through a custom base URL, which overrides only the endpoint and keeps that provider's API dialect. A model the catalogue does not carry, such as one a router or gateway serves under its own ID, or a self-hosted model, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and passed with `--models-config`. Shannon uses a single unified model setting throughout a pentest.
|
||||||
|
|
||||||
### Can I run Shannon on a local or self-hosted model?
|
### Can I run Shannon on a local or self-hosted model?
|
||||||
|
|
||||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and gateways such as LiteLLM. Point Shannon at the endpoint with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [AI providers](docs/ai-providers.md#custom-base-url).
|
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and LLM gateways such as LiteLLM. A model the harness catalogue does not carry, which most self-hosted models are, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file passed with `--models-config`; routers and gateways can also be reached with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [Local and self-hosted models](docs/ai-providers.md#local-and-self-hosted-models).
|
||||||
|
|
||||||
### Does Shannon actually exploit vulnerabilities, or just scan?
|
### Does Shannon actually exploit vulnerabilities, or just scan?
|
||||||
|
|
||||||
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a passive scanner.
|
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a passive scanner.
|
||||||
|
|
||||||
**Built by [Keygraph](https://keygraph.io)**
|
**Built by [Keygraph](https://keygraph.io)**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# File: docs/development.md
|
# File: docs/development.md
|
||||||
@@ -485,6 +523,9 @@ npx @keygraph/shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
|||||||
# Stream the log until the scan finishes, then exit on its outcome (useful in CI).
|
# Stream the log until the scan finishes, then exit on its outcome (useful in CI).
|
||||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --follow
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --follow
|
||||||
|
|
||||||
|
# Validate the configured login only, then stop (no pentest or report).
|
||||||
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth
|
||||||
|
|
||||||
# List running and completed scans.
|
# List running and completed scans.
|
||||||
npx @keygraph/shannon scans
|
npx @keygraph/shannon scans
|
||||||
```
|
```
|
||||||
@@ -497,6 +538,7 @@ Source-build examples:
|
|||||||
./shannon start -u https://example.com -r /path/to/repo -o ./my-reports
|
./shannon start -u https://example.com -r /path/to/repo -o ./my-reports
|
||||||
./shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
./shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
||||||
./shannon start -u https://example.com -r /path/to/repo --follow
|
./shannon start -u https://example.com -r /path/to/repo --follow
|
||||||
|
./shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth
|
||||||
./shannon scans
|
./shannon scans
|
||||||
|
|
||||||
# Rebuild the worker image.
|
# Rebuild the worker image.
|
||||||
@@ -713,6 +755,17 @@ login_flow:
|
|||||||
- "Click <exact button text>"
|
- "Click <exact button text>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Validating Authentication Only
|
||||||
|
|
||||||
|
To confirm your login flow works before committing to a full scan, add `--validate-auth` to `start`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @keygraph/shannon start -u https://your-app.com -r /path/to/repo -c config.yaml --validate-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
The run performs preflight and the single real login, then stops. No pentest, reconciliation, or report
|
||||||
|
is produced. It requires an `authentication` block in the config.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# File: docs/ai-providers.md
|
# File: docs/ai-providers.md
|
||||||
@@ -747,12 +800,15 @@ Shannon forwards only the selected provider's credential into the scan container
|
|||||||
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=your-api-key # the provider's API key
|
export SHANNON_AI_API_KEY=your-api-key # the provider's key — or the gateway's when a base URL is set
|
||||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||||
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route through a proxy or LLM gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
||||||
|
|
||||||
|
A model the catalogue does not carry is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration).
|
||||||
|
|
||||||
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -767,7 +823,15 @@ Review each vendor's guidance and complete the verification or enrollment they a
|
|||||||
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
||||||
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
||||||
|
|
||||||
This applies to the Anthropic and OpenAI providers, including when either is reached through a gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
This applies to the Anthropic and OpenAI providers, including when either is reached through an LLM gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||||
|
|
||||||
|
To confirm your model is ready before committing to a full scan, add `--validate-model` to `start`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @keygraph/shannon start -u https://your-app.com -r /path/to/repo --validate-model
|
||||||
|
```
|
||||||
|
|
||||||
|
The run performs the preflight model checks only — credential and registry resolution for any provider, plus a single cyber-access verification against Anthropic and OpenAI that trips the cyber safeguard if your account is not approved — then stops. No pentest or report is produced, and it needs no config. A decline fails the run with the vendor's enrollment link.
|
||||||
|
|
||||||
## Suggested models
|
## Suggested models
|
||||||
|
|
||||||
@@ -775,9 +839,9 @@ These are the models `npx @keygraph/shannon setup` offers, best-first. They are
|
|||||||
|
|
||||||
| Provider | Suggested model IDs |
|
| Provider | Suggested model IDs |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `anthropic` | `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
| `anthropic` | `claude-sonnet-5`, `claude-opus-5`, `claude-sonnet-4-6`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-haiku-4-5-20251001` |
|
||||||
| `openai` | `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
| `openai` | `gpt-6-sol`, `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.4` |
|
||||||
| `xai` | `grok-4.6`, `grok-4.5` |
|
| `xai` | `grok-4.7` |
|
||||||
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
| `amazon-bedrock` | `us.anthropic.claude-sonnet-4-6`, `us.anthropic.claude-opus-4-8`, `us.anthropic.claude-opus-4-7` |
|
||||||
|
|
||||||
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
Bedrock IDs are region-prefixed and must be enabled in your account, so the ID that works for you may differ from the one listed here.
|
||||||
@@ -797,14 +861,14 @@ OpenAI:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-...
|
export SHANNON_AI_API_KEY=sk-...
|
||||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
export SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
```
|
```
|
||||||
|
|
||||||
xAI:
|
xAI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=xai-...
|
export SHANNON_AI_API_KEY=xai-...
|
||||||
export SHANNON_AI_MODEL=xai:grok-4.5
|
export SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
```
|
```
|
||||||
|
|
||||||
Source-build mode reads the same variables from a `.env` file.
|
Source-build mode reads the same variables from a `.env` file.
|
||||||
@@ -823,17 +887,18 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens,
|
|||||||
|
|
||||||
## Custom base URL
|
## Custom base URL
|
||||||
|
|
||||||
To route model traffic through your own infrastructure — a corporate proxy, an LLM gateway such as LiteLLM, or a regional endpoint — set a base URL alongside your normal model selection. The provider half of `SHANNON_AI_MODEL` decides which key is sent and which API Shannon speaks, so pick the one your gateway serves:
|
`SHANNON_AI_BASE_URL` routes model traffic through a proxy or LLM gateway instead of the provider's default endpoint — an LLM gateway such as LiteLLM, a regional endpoint, or any other host you choose. It is a plain endpoint override: it changes only *where* requests go. The provider half of `SHANNON_AI_MODEL` still decides which credential is sent and which API dialect is spoken, and that is unchanged by the base URL.
|
||||||
|
|
||||||
| Gateway serves | Model prefix | API key |
|
This works for **any** provider, curated or not, subject to two rules. A provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect:
|
||||||
| --- | --- | --- |
|
|
||||||
| Anthropic Messages | `anthropic:` | `SHANNON_AI_API_KEY` |
|
|
||||||
| OpenAI Chat Completions | `openai:` | `SHANNON_AI_API_KEY` |
|
|
||||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `SHANNON_AI_API_KEY` |
|
|
||||||
|
|
||||||
The model ID is whatever name your gateway serves it under; it does not have to exist in Shannon's catalogue.
|
| Provider prefix | Dialect the endpoint must speak |
|
||||||
|
| --- | --- |
|
||||||
|
| `anthropic:` | Anthropic Messages |
|
||||||
|
| `openai:` | OpenAI Responses |
|
||||||
|
|
||||||
Anthropic Messages:
|
And the model ID must still resolve in the harness catalogue. A base URL changes only the address; it grants no exemption from that check. A gateway serving a model under its own name needs that name described in a [custom model configuration](#custom-model-configuration) file.
|
||||||
|
|
||||||
|
Anthropic Messages LLM gateway:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-ant-...
|
export SHANNON_AI_API_KEY=sk-ant-...
|
||||||
@@ -841,27 +906,130 @@ export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
|||||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenAI Chat Completions:
|
OpenAI Responses LLM gateway:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_API_KEY=sk-...
|
export SHANNON_AI_API_KEY=sk-...
|
||||||
export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
export SHANNON_AI_MODEL=openai:gpt-6-sol
|
||||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
`SHANNON_AI_MODEL` is always `<provider>:<model-id>`, gateway or not.
|
`npx @keygraph/shannon setup` configures a base URL two ways: **Custom Base URL** covers the common Anthropic Messages and OpenAI Responses LLM gateways, and **Other provider** takes any provider ID plus an optional base URL of its own.
|
||||||
|
|
||||||
OpenAI is the one provider serving two APIs, so a gateway run picks one:
|
## Custom model configuration
|
||||||
|
|
||||||
|
A custom model configuration is a Pi `models.json` file that describes a model the harness catalogue does not carry: one a router or gateway serves under its own ID, or a local server (see [Local and self-hosted models](#local-and-self-hosted-models)). You pass it with `--models-config`, and Shannon merges its definitions over the catalogue so `SHANNON_AI_MODEL` can then name the model like any other:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_AI_OPENAI_FORMAT=responses # default: chat-completions
|
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Chat Completions is the default because that is what most gateway software exposes. Set `responses` for a gateway that passes the Responses API through — it preserves reasoning state between turns, which Chat Completions cannot. `openai:gpt-5` with no base URL always calls OpenAI's Responses API directly.
|
```bash
|
||||||
|
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||||
|
```
|
||||||
|
|
||||||
The variable is rejected in preflight where it cannot take effect: with a non-`openai` model, since Anthropic, xAI, and Bedrock each serve one API, and with no `SHANNON_AI_BASE_URL`, since a direct OpenAI run is always Responses.
|
[pi.dev/models](https://pi.dev/models) supplies the file contents. Find the model under the provider you want, since the same model has a different ID per provider, then open its page and expand **Show configuration** for a ready-to-paste snippet:
|
||||||
|
|
||||||
`npx @keygraph/shannon setup` covers this under **Custom Base URL**, which asks which API your gateway serves and configures the matching provider for you.
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"apiKey": "YOUR_API_KEY",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "z-ai/glm-5.3",
|
||||||
|
"name": "Z.ai: GLM 5.3",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"thinkingLevelMap": {
|
||||||
|
"off": null,
|
||||||
|
"minimal": null,
|
||||||
|
"low": "low",
|
||||||
|
"medium": null,
|
||||||
|
"high": "high",
|
||||||
|
"xhigh": null,
|
||||||
|
"max": "max"
|
||||||
|
},
|
||||||
|
"contextWindow": 1048576,
|
||||||
|
"maxTokens": 943718,
|
||||||
|
"cost": {
|
||||||
|
"input": 1.4,
|
||||||
|
"output": 4.4,
|
||||||
|
"cacheRead": 0.26,
|
||||||
|
"cacheWrite": 0
|
||||||
|
},
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"thinkingFormat": "openrouter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api": "openai-completions",
|
||||||
|
"baseUrl": "https://openrouter.ai/api/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then name the model the usual way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SHANNON_AI_API_KEY=your-api-key
|
||||||
|
export SHANNON_AI_MODEL=openrouter:z-ai/glm-5.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave `YOUR_API_KEY` exactly as it is. Shannon sends the credential from your environment, and that takes precedence over anything the file declares, so the file describes the model and never has to hold a secret.
|
||||||
|
|
||||||
|
Pi's [models documentation](https://pi.dev/docs/latest/models) describes the full format, including provider routing preferences and compatibility flags.
|
||||||
|
|
||||||
|
## Local and self-hosted models
|
||||||
|
|
||||||
|
Ollama, LM Studio, vLLM, and any other OpenAI-compatible server are reached through the same mechanism. Describe the server as a provider in a model config file, then name its model with `SHANNON_AI_MODEL`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Use `host.docker.internal`, not `localhost`. The scan runs inside a container, so `localhost` points at the container itself rather than at your machine.
|
||||||
|
|
||||||
|
A `models.json` for Ollama:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://host.docker.internal:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "ollama",
|
||||||
|
"models": [
|
||||||
|
{ "id": "<model-id>" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then name the model and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SHANNON_AI_API_KEY=ollama # any value, see below
|
||||||
|
export SHANNON_AI_MODEL=ollama:<model-id>
|
||||||
|
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||||
|
```
|
||||||
|
|
||||||
|
LM Studio and vLLM take the same shape on their own ports, `http://host.docker.internal:1234/v1` and `http://host.docker.internal:8000/v1` respectively. The provider name is yours to choose, and only has to match the prefix in `SHANNON_AI_MODEL`.
|
||||||
|
|
||||||
|
`SHANNON_AI_API_KEY` is still required even though a local server ignores it. Shannon checks that the selected provider has a credential before it starts, so set it to any placeholder value. It is sent to your server and discarded.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Shannon drives every phase through multi-turn tool use. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves.
|
||||||
|
|
||||||
|
Some servers need compatibility flags. If a reasoning-capable model is rejected, turn off the roles it does not understand, at either provider or model level:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
Pi's [models documentation](https://pi.dev/docs/latest/models) lists the full set of compatibility flags and local-runtime options.
|
||||||
|
|
||||||
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
||||||
|
|
||||||
@@ -870,36 +1038,36 @@ A ChatGPT Plus or Pro Codex subscription can run Shannon. Shannon reuses a login
|
|||||||
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
|
Before running a pentest, review the [cyber safeguards requirements](#cyber-safeguards-do-this-before-your-first-scan).
|
||||||
|
|
||||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `openai-codex` entry.
|
2. Start Pi by running `pi` in your terminal, then run `/login`, choose **Sign in with an account**, then choose **OpenAI Codex (legacy)** and complete the browser sign-in. This creates `~/.pi/agent/auth.json` with an `openai-codex` entry.
|
||||||
|
|
||||||
3. Select a Codex model and enable Pi authentication:
|
3. Select a Codex model and enable Pi authentication:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_USE_PI_AUTH=1
|
export SHANNON_USE_PI_AUTH=1
|
||||||
export SHANNON_AI_MODEL=openai-codex:gpt-5.5
|
export SHANNON_AI_MODEL=openai-codex:gpt-6-sol
|
||||||
```
|
```
|
||||||
|
|
||||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||||
|
|
||||||
Supported Codex models are `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
Supported Codex models are `gpt-6-sol`, `gpt-5.6-sol`, `gpt-5.5`, and `gpt-5.4`.
|
||||||
|
|
||||||
## xAI (Grok subscription)
|
## xAI (Grok subscription)
|
||||||
|
|
||||||
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
An xAI subscription can run Shannon. Shannon reuses a login created by Pi.
|
||||||
|
|
||||||
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
1. Install Pi by following the instructions at [pi.dev](https://pi.dev).
|
||||||
2. Log in with your subscription using Pi's [subscription authentication guide](https://pi.dev/docs/latest/providers#subscriptions). This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
2. Start Pi by running `pi` in your terminal, then run `/login`, choose **Sign in with an account**, then choose **xAI** and complete the browser sign-in. This creates `~/.pi/agent/auth.json` with an `xai` entry.
|
||||||
|
|
||||||
3. Select an xAI model and enable Pi authentication:
|
3. Select an xAI model and enable Pi authentication:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SHANNON_USE_PI_AUTH=1
|
export SHANNON_USE_PI_AUTH=1
|
||||||
export SHANNON_AI_MODEL=xai:grok-4.6
|
export SHANNON_AI_MODEL=xai:grok-4.7
|
||||||
```
|
```
|
||||||
|
|
||||||
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
4. In npx mode, run `npx @keygraph/shannon start ...` from the same shell. In source-build mode, add the two variables to `.env` and run `./shannon start ...`.
|
||||||
|
|
||||||
Suggested Grok models are `grok-4.6` and `grok-4.5`.
|
The suggested Grok model is `grok-4.7`.
|
||||||
|
|
||||||
## Claude Code subscription
|
## Claude Code subscription
|
||||||
|
|
||||||
@@ -928,7 +1096,8 @@ These instructions apply only to `shannon-v1`.
|
|||||||
|
|
||||||
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
||||||
|
|
||||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
|
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). To run a model the catalogue does not carry, describe it with [`--models-config`](#custom-model-configuration).
|
||||||
|
- **Model configuration** — when `--models-config` is passed, the file is parsed and schema-checked before the scan starts, and a fault fails preflight with the offending field named.
|
||||||
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
||||||
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
||||||
|
|
||||||
@@ -1189,134 +1358,102 @@ For organizations that need broader static and organizational coverage now, see
|
|||||||
|
|
||||||
# Keygraph Enterprise Platform
|
# Keygraph Enterprise Platform
|
||||||
|
|
||||||
Shannon 3.0 makes advanced, code-informed autonomous pentesting available to everyone. The open-source CLI maps routes and data flows, understands application architecture, executes real attacks, and produces PDF and SARIF results—locally, in CI/CD, or fully air-gapped with your own model.
|
Shannon 3.0 is an open-source pentester. It reads your source, maps routes and data flows, runs real attacks against a live target, and writes PDF and SARIF reports. It runs locally, in CI, or air-gapped with your own model. Shannon Open Source is a complete pentester, not a trial edition.
|
||||||
|
|
||||||
The **Keygraph Enterprise Platform** is the commercial AppSec operating system for organizations that need to run that process continuously across many repositories, teams, and environments. It adds exhaustive agentic SAST, business-logic and source-to-sink analysis, broader scanner coverage, centralized vulnerability management, automated remediation and targeted verification, enterprise governance, and organization-wide reporting.
|
Keygraph Enterprise runs an enterprise-hardened fork of Shannon continuously across hundreds of repositories and adds what a security team needs around it: audit-depth static analysis on a parsed code graph, business-logic testing, SCA and secrets scanning, one deduplicated record per vulnerability across scans and scanners, generated fixes, fix verification, and SSO, RBAC, and audit logs. It is for security teams that own vulnerability management across many engineering teams and need one place to triage, assign, fix, and verify.
|
||||||
|
|
||||||
> Shannon Open Source is a complete autonomous pentester, not a trial edition. Keygraph Enterprise is for teams that need greater analysis depth, shared control, and a closed-loop vulnerability-management program.
|
Both editions are BYOK. Keygraph never receives your source and never proxies model traffic, open source or commercial. Shannon Open Source runs from your machine or CI runner. Keygraph Enterprise deploys as a platform inside your cloud or data center, including fully air-gapped.
|
||||||
|
|
||||||
## Who It Is For
|
## Shannon Open Source vs. Keygraph Enterprise
|
||||||
|
|
||||||
Keygraph Enterprise is designed for organizations that need to:
|
| | Shannon Open Source | Keygraph Enterprise |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Best for | Developers and teams running repository-level pentests locally or in CI | Security organizations running continuous AppSec across many teams and repositories |
|
||||||
|
| Code analysis | Agent pass over architecture, entry points, and data flows to seed the pentest, sized to finish inside a CI run | Persistent code property graph plus a long-running analysis harness with interprocedural taint, sanitizer modeling, cross-repo context, exploit chains, and multi-pass review |
|
||||||
|
| Pentesting | On-demand, source-aware white-box pentesting with optional authenticated testing, focused on injection, XSS, SSRF, broken authentication, and broken authorization, with proof by exploitation | Enterprise-hardened Shannon fork run continuously, with grey-box and black-box targets and business-logic invariant testing |
|
||||||
|
| SCA and secrets | Not included | SCA with reachability and secrets scanning including history |
|
||||||
|
| Findings | Per-run PDF, Markdown, JSON, and SARIF, with SARIF ingestion into GitHub code scanning | One record per vulnerability per repo across scans and scanners, plus ownership, SLAs, dashboards, and audit evidence |
|
||||||
|
| Fixes and verification | Not included | Fix PRs with verification by re-analysis and exploit replay, with no full rescan required |
|
||||||
|
| CI/CD and source control | GitHub Action and GitLab CI component for pull-request, release, and scheduled runs, with gates on `status: exploited` | GitHub, GitLab, Azure DevOps, and Bitbucket with organization-wide policy and centrally managed integrations |
|
||||||
|
| Deployment and models | Runs locally or on a CI runner with BYOK to any Anthropic- or OpenAI-compatible endpoint or local model | Deployed in your AWS, GCP, Azure, or on-prem environment. Customer-hosted services and stored platform data remain inside your environment. Model requests go directly to the provider, private endpoint, gateway, or local model you configure. A local model supports fully disconnected deployments |
|
||||||
|
| Governance, license, support | AGPL-3.0 and community support | SSO, SCIM, RBAC, and audit logs, plus a commercial license, enterprise support, and SOC 2 Type II |
|
||||||
|
|
||||||
- continuously test hundreds or thousands of repositories, services, applications, and APIs;
|
## How it fits your pipeline
|
||||||
- combine agentic pentesting, SAST, SCA, secrets, and business-logic findings in one system;
|
|
||||||
- enforce security policy in GitHub Actions, GitLab CI, and enterprise delivery pipelines;
|
|
||||||
- give developers one canonical, actionable record for each vulnerability instead of duplicate scanner alerts;
|
|
||||||
- assign owners, apply SLAs, track status, and measure risk and remediation performance across the organization;
|
|
||||||
- generate fixes and verify them without rerunning an entire scan;
|
|
||||||
- enforce enterprise identity, authorization, audit, and API-access controls; and
|
|
||||||
- deploy fully on-premises or air-gapped with customer-controlled models, keys, and routing.
|
|
||||||
|
|
||||||
## Close the Entire AppSec Loop
|
1. Scans run on pull requests, releases, and a schedule against repositories in GitHub, GitLab, Azure DevOps, or Bitbucket.
|
||||||
|
2. Pipelines gate on exploited severity. A code-analysis hypothesis never fails a build.
|
||||||
|
3. Findings from every scanner and every run land as one record per vulnerability per repository, with an owner and an SLA. The same finding across ten runs is one record, not ten alerts.
|
||||||
|
4. From a finding, Keygraph opens a fix PR into your normal review flow.
|
||||||
|
5. Verification confirms the fix against the changed code and the original exploit. No full rescan is required.
|
||||||
|
|
||||||
The platform connects discovery, triage, remediation, and verification in one continuous workflow:
|
## What is different technically
|
||||||
|
|
||||||
1. **Analyze** every repository with exhaustive agentic SAST and complementary scanners.
|
### Static analysis on a code property graph
|
||||||
2. **Prove** exploitability with source-aware white-box, black-box, and grey-box pentesting.
|
|
||||||
3. **Normalize and deduplicate** results into a canonical finding per vulnerability and repository.
|
|
||||||
4. **Prioritize and assign** using severity, reachability, exploit evidence, ownership, policy, and business context.
|
|
||||||
5. **Remediate** with an AI-authored patch delivered as a reviewable pull request.
|
|
||||||
6. **Verify** the specific fix with deterministic checks and adversarial agent reasoning—without rerunning the full scan.
|
|
||||||
7. **Track and govern** status, exceptions, SLAs, audit history, trends, and compliance evidence until closure.
|
|
||||||
|
|
||||||
## Exhaustive Agentic SAST
|
Shannon Open Source's code analysis is sized to finish inside a CI run: agents read the repository, map the attack surface, and hand candidates to the pentester. Enterprise is built for depth instead. It first parses each repository into a persistent code property graph, then runs an analysis harness derived from one built for long-running vulnerability audits, heavily adapted to query the graph rather than read files. The harness decomposes the application into risk, taint-flow, framework, and specialist tasks and supports longer-running audit workflows beyond typical CI job windows.
|
||||||
|
|
||||||
Shannon 3.0's open-source code analysis runs a multi-stage agentic workflow. It models application architecture, trust boundaries, exposed interfaces, and data flows, opens targeted investigations, reviews the candidates they turn up, and hands the survivors to live pentesting agents. That workflow is built for practical local and CI/CD runs.
|
On the graph, it performs:
|
||||||
|
|
||||||
The Enterprise engine goes further, for audits at organization scale. It parses the codebase and builds persistent structural context before agents start reasoning about security:
|
- Interprocedural taint tracking across functions, files, fields, containers, and framework request lifecycles.
|
||||||
|
- Source, sink, and sanitizer modeling that records where validation, encoding, or authorization changes a path.
|
||||||
|
- Cross-repository modeling of services, entry points, and trust boundaries.
|
||||||
|
- Semantic deduplication of variants of the same defect, and exploit-chain analysis for combinations with higher impact than any single issue.
|
||||||
|
- Multiple review passes per candidate, checking the agent's claim against the graph and available deployment and configuration context. Candidates that cannot be substantiated are not reported.
|
||||||
|
|
||||||
- **Repository and architecture modeling** identifies services, frameworks, entry points, assets, trust boundaries, and cross-repository relationships.
|
### Business-logic invariants
|
||||||
- **Interprocedural call and data-flow analysis** traces values across functions, files, fields, containers, and framework-managed request lifecycles.
|
|
||||||
- **Source, sink, and sanitizer modeling** follows untrusted input to sensitive operations and records where validation, encoding, authorization, or other controls alter the path.
|
|
||||||
- **Threat-driven decomposition** breaks large applications into risk, taint-flow, framework, and specialist analysis tasks so deep scans remain systematic.
|
|
||||||
- **Exhaustive adversarial verification** challenges candidates across multiple review passes, weighing structural evidence against what the agents found, then asks whether each one is viable in the application's production configuration.
|
|
||||||
- **Semantic deduplication and exploit-chain analysis** consolidate variants of the same defect and identify combinations whose impact is greater than any isolated issue.
|
|
||||||
- **Business-logic invariant testing** derives rules the code is supposed to preserve—such as tenant isolation, workflow order, approval limits, balances, and state transitions—then agents fuzz those invariants for application-specific flaws.
|
|
||||||
|
|
||||||
The result is broad vulnerability hunting with precise paths back to the relevant code, not a flat list of pattern matches.
|
Shannon Open Source focuses on injection, XSS, SSRF, and broken authentication and authorization. Enterprise adds testing for the bugs that do not fit a vulnerability class: it derives invariants the application is supposed to hold (tenant isolation, workflow ordering, approval limits, balance conservation, state transitions) and tests them against the running application. This is where application-specific vulnerabilities live and where pattern-based SAST often provides little or no signal.
|
||||||
|
|
||||||
|
### Proof by exploitation
|
||||||
|
|
||||||
|
The pentesting engine is a hardened fork of Shannon with the same rule: a pentest finding requires a working exploit. No exploit, no finding. Enterprise stores the exploit and replays it later to verify the fix.
|
||||||
|
|
||||||
|
SCA prioritizes vulnerable dependencies that application code actually reaches. Secrets scanning covers current source and repository history.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise SAST results grouped into business-logic issues, point issues, and secrets" width="100%">
|
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise findings grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Complete Application-Security Coverage
|
## Findings
|
||||||
|
|
||||||
Agentic SAST and pentesting work alongside additional first-class scanners:
|
Shannon Open Source hands you a report per scan. Enterprise dedupes across runs and across scanners, deterministically and semantically, into one record per vulnerability per repository. Each record carries evidence, source location, severity, scan history, status, owner, resolution, and last-verified state.
|
||||||
|
|
||||||
- **SCA with reachability** prioritizes vulnerable dependencies that application code can actually reach.
|
Workflows cover assignment, triage, false-positive and risk-acceptance decisions, and SLA policies with escalation and aging. Dashboards report open risk, coverage, new versus resolved, SLA compliance, and MTTR, exportable as evidence for customers and auditors.
|
||||||
- **Full secrets scanning** detects credentials, tokens, and keys across source and repository history.
|
|
||||||
- **Agentic pentesting** correlates code intelligence with live application behavior and attempts real exploitation. The core rule remains: no exploit, no pentest finding.
|
|
||||||
|
|
||||||
## One System of Record for Every Finding
|
Findings still require human review. Enterprise's extra review passes reduce weakly supported findings, but they do not eliminate them.
|
||||||
|
|
||||||
Keygraph ingests results from every analysis source, correlates them, and maintains one canonical finding per vulnerability per repository. Security and engineering teams work from the same record, with evidence, source location, severity, scan history, status, assignee, resolution, and last-verification state.
|
|
||||||
|
|
||||||
The vulnerability-management layer provides:
|
|
||||||
|
|
||||||
- deterministic and semantic deduplication across scans and scanners;
|
|
||||||
- ownership, assignment, triage, false-positive, risk-acceptance, and resolution workflows;
|
|
||||||
- SLA policies, escalation, aging, and last-verified tracking;
|
|
||||||
- bidirectional developer-workflow integrations and APIs;
|
|
||||||
- dashboards for risk, coverage, trends, new versus resolved findings, SLA compliance, and MTTR; and
|
|
||||||
- exportable evidence for customers, auditors, and compliance programs.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise canonical findings inventory with severity, status, source, and verification filters" width="100%">
|
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise findings inventory with severity, status, source, and verification filters" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Remediate, Then Verify the Fix
|
### Fix and verify
|
||||||
|
|
||||||
From an individual finding, a user can ask Keygraph to produce a focused patch. The remediation agent reasons from the root cause and evidence, changes only the required code, and opens a pull request into the existing review process. It does not silently apply fixes to a protected branch.
|
From a finding, Keygraph generates a patch scoped to that finding and opens a pull request. It never commits to a protected branch.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
After a patch is available, targeted verification re-analyzes the affected code and, for dynamic pentest findings, re-tests the original proof of concept against the target. Deterministic checks and adversarial agent reasoning produce a clear verdict without the cost and delay of rerunning the entire scan.
|
Verification re-analyzes the changed code and, for pentest findings, replays the original exploit against the patched target. The verdict comes from deterministic checks plus a review pass, without rerunning the full scan.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise targeted finding-verification workflow" width="100%">
|
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise finding-verification workflow" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Enterprise Governance and Integrations
|
## Deployment and access control
|
||||||
|
|
||||||
Keygraph is built for shared operation across security, platform, and engineering teams:
|
Keygraph Enterprise deploys entirely inside your AWS, GCP, Azure, or on-prem environment, including networks with no internet egress. There is no Keygraph-operated control plane. Customer-hosted services and stored platform data remain inside your environment for the life of the deployment.
|
||||||
|
|
||||||
- SAML 2.0 or OIDC single sign-on and SCIM provisioning;
|
Model access is BYOK and BYOM. Route workloads to Anthropic, OpenAI, xAI, or Bedrock, a private cloud endpoint, your own gateway such as LiteLLM with your routing and policy applied, or local models on vLLM or Ollama. Model requests go directly to the endpoint you configure. Keygraph never receives or proxies them. A local model supports a fully disconnected deployment.
|
||||||
- organization, team, and user management;
|
|
||||||
- built-in and custom roles with granular relationship-, attribute-, and role-based authorization (ReBAC, ABAC, and RBAC);
|
Access control: SAML/OIDC SSO, SCIM, roles with repository-scoped visibility (RBAC, plus attribute and relationship rules where needed), full audit log, scoped API keys.
|
||||||
- repository, pentest-profile, scanner, finding, and administration boundaries;
|
|
||||||
- full audit logging and scoped API keys;
|
|
||||||
- integrations with source control, CI/CD, ticketing, chat, and cloud environments; and
|
|
||||||
- commercial support and enterprise onboarding.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise granular roles and repository visibility controls" width="100%">
|
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise roles and repository visibility controls" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## On-Premises, Air-Gapped, and Customer-Controlled AI
|
Keygraph maintains a SOC 2 Type II audit. The report is available to customers under NDA.
|
||||||
|
|
||||||
Keygraph Enterprise can run entirely inside your AWS, GCP, Azure, or on-premises environment, including networks with no public internet access. Deployments can keep source code, scan artifacts, findings, prompts, completions, and model traffic inside your security perimeter.
|
|
||||||
|
|
||||||
AI access is bring-your-own-key and bring-your-own-model. Organizations can route workloads through approved commercial providers, private cloud endpoints, an internal LLM gateway, or local open-source models, with granular routing and policy controlled by the customer. There is no requirement for a Keygraph-operated control plane or model proxy.
|
|
||||||
|
|
||||||
Keygraph maintains a SOC 2 Type II audit and makes the current report available to customers under appropriate confidentiality terms.
|
|
||||||
|
|
||||||
## Shannon 3.0 vs. Keygraph Enterprise
|
|
||||||
|
|
||||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Best for | Individual developers and teams running pentests locally or in CI/CD | Security organizations running a continuous AppSec program across many teams and repositories |
|
|
||||||
| Code analysis | Multi-stage agentic review maps architecture, trust boundaries, exposed interfaces, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code analysis: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic invariant testing |
|
|
||||||
| Pentesting | On-demand, source-aware white-box pentesting with proof by exploitation | Continuous white-box, black-box, and grey-box pentesting across applications and environments |
|
|
||||||
| Additional AppSec coverage | Not included | SCA with reachability, secrets scanning, and business-logic invariant testing |
|
|
||||||
| CI/CD and reporting | Official GitHub Action and reusable GitLab CI/CD component; staging, release, merge-request, and scheduled pentests; demonstrated-vulnerability severity gates; PDF, Markdown, JSON, SARIF, artifacts, and native security-workflow ingestion | Organization-wide policies and gating, centrally managed integrations, canonical findings, dashboards, analytics, SLA tracking, and compliance evidence |
|
|
||||||
| Automated remediation and verification | Not included | AI-authored pull requests with targeted code and exploit verification |
|
|
||||||
| Enterprise governance | N/A — local, single-operator CLI | SSO, SCIM, teams, ReBAC/ABAC/RBAC, audit logs, API keys, ownership, and SLA policies |
|
|
||||||
| Deployment and AI | Self-hosted, no telemetry, BYOM, and fully air-gapped with a local model | Fully on-premises or air-gapped, BYOK/BYOM, and granular routing through customer-controlled gateways |
|
|
||||||
| License and support | AGPL-3.0 and community support | Commercial license, enterprise support, and SOC 2 Type II controls |
|
|
||||||
|
|
||||||
## Talk to Keygraph
|
## Talk to Keygraph
|
||||||
|
|
||||||
Visit [keygraph.io](https://keygraph.io), book a [Keygraph demo](https://cal.com/team/keygraph/shannon-pro), or contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
Visit [keygraph.io](https://keygraph.io), book a [demo](https://cal.com/team/keygraph/shannon-pro), or email [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||||
@@ -6,14 +6,13 @@ Use this file as the concise entry point for AI agents and LLMs reading this rep
|
|||||||
|
|
||||||
## Start Here
|
## Start Here
|
||||||
|
|
||||||
- [README](README.md): Main project overview, editions, quick start, Shannon capabilities, Keygraph platform positioning, common questions, safety notes, licensing, and support links.
|
- [README](README.md): Main project overview, editions, quick start, Shannon capabilities, CI/CD integrations, common questions, safety notes, licensing, and support links.
|
||||||
- [Full Combined Context](llms-full.txt): README and documentation combined into one file for agents that need maximum local context.
|
|
||||||
|
|
||||||
## Shannon
|
## Shannon
|
||||||
|
|
||||||
- [Development](docs/development.md): Source-build workflow, common CLI commands, repository paths, and output locations.
|
- [Development](docs/development.md): Source-build workflow, common CLI commands, repository paths, and output locations.
|
||||||
- [Configuration](docs/configuration.md): Authenticated testing, login flows, rules of engagement, report filters, credential precedence, and rate-limit settings.
|
- [Configuration](docs/configuration.md): Authenticated testing, login flows, rules of engagement (including rate-limit guidance), report filters, and credential precedence.
|
||||||
- [AI Providers](docs/ai-providers.md): Anthropic, OpenAI, xAI, AWS Bedrock, any other Pi-supported provider, and custom gateway setup.
|
- [AI Providers](docs/ai-providers.md): Anthropic, OpenAI, xAI, AWS Bedrock, any other Pi-supported provider, custom LLM gateway setup, custom model configuration for models not yet in the Pi catalogue, and local self-hosted runtimes (Ollama, LM Studio, vLLM).
|
||||||
- [Platforms and Networking](docs/platforms.md): Windows/WSL2, Linux, macOS, Docker networking, local applications, and custom hostnames.
|
- [Platforms and Networking](docs/platforms.md): Windows/WSL2, Linux, macOS, Docker networking, local applications, and custom hostnames.
|
||||||
- [Workspaces and Resuming](docs/workspaces.md): Workspace storage, naming, resuming interrupted scans, and examples.
|
- [Workspaces and Resuming](docs/workspaces.md): Workspace storage, naming, resuming interrupted scans, and examples.
|
||||||
- [Safety and Limitations](docs/safety.md): Authorized-use requirements, non-production guidance, mutative effects, model caveats, scope limits, cost, and performance.
|
- [Safety and Limitations](docs/safety.md): Authorized-use requirements, non-production guidance, mutative effects, model caveats, scope limits, cost, and performance.
|
||||||
@@ -28,9 +27,3 @@ Use this file as the concise entry point for AI agents and LLMs reading this rep
|
|||||||
- [Keygraph website](https://keygraph.io): Company and commercial product information.
|
- [Keygraph website](https://keygraph.io): Company and commercial product information.
|
||||||
- [Keygraph demo](https://cal.com/team/keygraph/shannon-pro): Demo and trial contact path.
|
- [Keygraph demo](https://cal.com/team/keygraph/shannon-pro): Demo and trial contact path.
|
||||||
- [Community Discord](https://discord.gg/cmctpMBXwE): Community support and discussion.
|
- [Community Discord](https://discord.gg/cmctpMBXwE): Community support and discussion.
|
||||||
|
|
||||||
## Optional
|
|
||||||
|
|
||||||
- [Sample Juice Shop report](sample-reports/shannon-report-juice-shop.md): Shannon sample report for OWASP Juice Shop.
|
|
||||||
- [Sample c{api}tal API report](sample-reports/shannon-report-capital-api.md): Shannon sample report for c{api}tal API.
|
|
||||||
- [Sample crAPI report](sample-reports/shannon-report-crapi.md): Shannon sample report for OWASP crAPI.
|
|
||||||
Generated
+40
-73
@@ -46,17 +46,17 @@ importers:
|
|||||||
apps/worker:
|
apps/worker:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@earendil-works/pi-agent-core':
|
'@earendil-works/pi-agent-core':
|
||||||
specifier: ^0.84.2
|
specifier: ^0.84.4
|
||||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-ai':
|
'@earendil-works/pi-ai':
|
||||||
specifier: ^0.84.2
|
specifier: ^0.84.4
|
||||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-coding-agent':
|
'@earendil-works/pi-coding-agent':
|
||||||
specifier: ^0.84.2
|
specifier: ^0.84.4
|
||||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@gotgenes/pi-permission-system':
|
'@gotgenes/pi-permission-system':
|
||||||
specifier: ^10.9.0
|
specifier: ^10.9.0
|
||||||
version: 10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)
|
version: 10.9.0(@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.4)
|
||||||
'@temporalio/activity':
|
'@temporalio/activity':
|
||||||
specifier: 1.15.0
|
specifier: 1.15.0
|
||||||
version: 1.15.0
|
version: 1.15.0
|
||||||
@@ -295,34 +295,34 @@ packages:
|
|||||||
'@clack/prompts@1.1.0':
|
'@clack/prompts@1.1.0':
|
||||||
resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==}
|
resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==}
|
||||||
|
|
||||||
'@earendil-works/pi-agent-core@0.84.2':
|
'@earendil-works/pi-agent-core@0.84.4':
|
||||||
resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==}
|
resolution: {integrity: sha512-HyUnjaOXj6oN/6SNcr8A1J/ElRQA50FtIE0XUTSKAQVqmdlb9qdojOyUQwF/jULE5+yOEtGuVgi/N1RnBiNG+g==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
'@earendil-works/pi-ai@0.84.2':
|
'@earendil-works/pi-ai@0.84.4':
|
||||||
resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==}
|
resolution: {integrity: sha512-AClAZxf5+c4RRu44NJPS6wyQy+Nmq+Mzyyrdvm4ZVMNuixelO02RZX4G4Aq1F145Yzp43wnM5S+hLlSI7ypfVw==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@earendil-works/pi-client@0.84.2':
|
'@earendil-works/pi-client@0.84.4':
|
||||||
resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==}
|
resolution: {integrity: sha512-q398WY/3ZQHTizk7IKxApzqFV0xt4yM9LkSkwyqeLK5Bj5RwRjOWxESt26z4LgNp4O+8hqhqFPf/8fj4H5rE4A==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
'@earendil-works/pi-coding-agent@0.84.2':
|
'@earendil-works/pi-coding-agent@0.84.4':
|
||||||
resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==}
|
resolution: {integrity: sha512-jmOlrqUmvhh/siNWFRXjYLJzhKFIHNsAQaysRwzQPQFnPAaV/vhqHsLH/MBsIISA1Rjj7WTUFR3nJrpXoLx39w==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@earendil-works/pi-protocol@0.84.2':
|
'@earendil-works/pi-protocol@0.84.4':
|
||||||
resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==}
|
resolution: {integrity: sha512-acyE9ozxkMiWiz/xyWpU0O9vwnYv0hyG889Vniv6Sg9c9zfsX+8MePnDNphBacY2Fvm1rxdsGmiVDSZl9yuDFA==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
'@earendil-works/pi-telemetry@0.84.2':
|
'@earendil-works/pi-telemetry@0.84.4':
|
||||||
resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==}
|
resolution: {integrity: sha512-8e2CuxM+ht+hedQXTZmi5JVl6/xDK9RpSDL2+MbITevKYQhMZ/z6lJOTFgox3HQyGxO8mOZEtYGVeQNaD4OzqA==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
'@earendil-works/pi-tui@0.84.2':
|
'@earendil-works/pi-tui@0.84.4':
|
||||||
resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==}
|
resolution: {integrity: sha512-nPUnwDkLtupPXnZQYrCwPFcuTydCDqTY6ZbFqhsL4S4kVq0AT418kPa/6uXwtaCD+MjBNBltb7ScTYX65yeE1w==}
|
||||||
engines: {node: '>=22.19.0'}
|
engines: {node: '>=22.19.0'}
|
||||||
|
|
||||||
'@emnapi/core@1.9.1':
|
'@emnapi/core@1.9.1':
|
||||||
@@ -588,10 +588,6 @@ packages:
|
|||||||
'@nodable/entities@2.1.1':
|
'@nodable/entities@2.1.1':
|
||||||
resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==}
|
resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.122.0':
|
'@oxc-project/types@0.122.0':
|
||||||
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||||
|
|
||||||
@@ -1360,10 +1356,6 @@ packages:
|
|||||||
glob-to-regexp@0.4.1:
|
glob-to-regexp@0.4.1:
|
||||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||||
|
|
||||||
glob@13.0.6:
|
|
||||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
|
||||||
engines: {node: 18 || 20 || >=22}
|
|
||||||
|
|
||||||
google-auth-library@10.7.0:
|
google-auth-library@10.7.0:
|
||||||
resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==}
|
resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1575,10 +1567,6 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
minipass@7.1.3:
|
|
||||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1665,10 +1653,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
path-scurry@2.0.2:
|
|
||||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
|
||||||
engines: {node: 18 || 20 || >=22}
|
|
||||||
|
|
||||||
path-to-regexp@8.4.2:
|
path-to-regexp@8.4.2:
|
||||||
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
|
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
|
||||||
|
|
||||||
@@ -2456,10 +2440,10 @@ snapshots:
|
|||||||
'@clack/core': 1.1.0
|
'@clack/core': 1.1.0
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
'@earendil-works/pi-agent-core@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
'@earendil-works/pi-agent-core@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
'@earendil-works/pi-ai': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-telemetry': 0.84.2
|
'@earendil-works/pi-telemetry': 0.84.4
|
||||||
diff: 8.0.4
|
diff: 8.0.4
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
typebox: 1.3.7
|
typebox: 1.3.7
|
||||||
@@ -2472,13 +2456,12 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
'@earendil-works/pi-ai@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
||||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||||
'@earendil-works/pi-telemetry': 0.84.2
|
'@earendil-works/pi-telemetry': 0.84.4
|
||||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
|
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@smithy/node-http-handler': 4.7.3
|
'@smithy/node-http-handler': 4.7.3
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
@@ -2493,22 +2476,21 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@earendil-works/pi-client@0.84.2':
|
'@earendil-works/pi-client@0.84.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@earendil-works/pi-protocol': 0.84.2
|
'@earendil-works/pi-protocol': 0.84.4
|
||||||
|
|
||||||
'@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
'@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@earendil-works/pi-agent-core': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
'@earendil-works/pi-agent-core': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
'@earendil-works/pi-ai': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-client': 0.84.2
|
'@earendil-works/pi-client': 0.84.4
|
||||||
'@earendil-works/pi-protocol': 0.84.2
|
'@earendil-works/pi-protocol': 0.84.4
|
||||||
'@earendil-works/pi-tui': 0.84.2
|
'@earendil-works/pi-tui': 0.84.4
|
||||||
'@silvia-odwyer/photon-node': 0.3.4
|
'@silvia-odwyer/photon-node': 0.3.4
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
diff: 8.0.4
|
diff: 8.0.4
|
||||||
glob: 13.0.6
|
|
||||||
grok-mermaid: 0.2.2
|
grok-mermaid: 0.2.2
|
||||||
highlight.js: 10.7.3
|
highlight.js: 10.7.3
|
||||||
hosted-git-info: 9.0.3
|
hosted-git-info: 9.0.3
|
||||||
@@ -2530,13 +2512,13 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@earendil-works/pi-protocol@0.84.2':
|
'@earendil-works/pi-protocol@0.84.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
typebox: 1.3.7
|
typebox: 1.3.7
|
||||||
|
|
||||||
'@earendil-works/pi-telemetry@0.84.2': {}
|
'@earendil-works/pi-telemetry@0.84.4': {}
|
||||||
|
|
||||||
'@earendil-works/pi-tui@0.84.2':
|
'@earendil-works/pi-tui@0.84.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
get-east-asian-width: 1.6.0
|
get-east-asian-width: 1.6.0
|
||||||
marked: 18.0.5
|
marked: 18.0.5
|
||||||
@@ -2570,10 +2552,10 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)':
|
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@earendil-works/pi-coding-agent': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
'@earendil-works/pi-coding-agent': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||||
'@earendil-works/pi-tui': 0.84.2
|
'@earendil-works/pi-tui': 0.84.4
|
||||||
tree-sitter-bash: 0.25.1
|
tree-sitter-bash: 0.25.1
|
||||||
web-tree-sitter: 0.26.9
|
web-tree-sitter: 0.26.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -2820,8 +2802,6 @@ snapshots:
|
|||||||
|
|
||||||
'@nodable/entities@2.1.1': {}
|
'@nodable/entities@2.1.1': {}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
|
||||||
|
|
||||||
'@oxc-project/types@0.122.0': {}
|
'@oxc-project/types@0.122.0': {}
|
||||||
|
|
||||||
'@protobufjs/aspromise@1.1.2': {}
|
'@protobufjs/aspromise@1.1.2': {}
|
||||||
@@ -3599,12 +3579,6 @@ snapshots:
|
|||||||
|
|
||||||
glob-to-regexp@0.4.1: {}
|
glob-to-regexp@0.4.1: {}
|
||||||
|
|
||||||
glob@13.0.6:
|
|
||||||
dependencies:
|
|
||||||
minimatch: 10.2.5
|
|
||||||
minipass: 7.1.3
|
|
||||||
path-scurry: 2.0.2
|
|
||||||
|
|
||||||
google-auth-library@10.7.0:
|
google-auth-library@10.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -3813,8 +3787,6 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
minipass@7.1.3: {}
|
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
ms@3.0.0-canary.1: {}
|
ms@3.0.0-canary.1: {}
|
||||||
@@ -3877,11 +3849,6 @@ snapshots:
|
|||||||
|
|
||||||
path-key@3.1.1: {}
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
path-scurry@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
lru-cache: 11.5.1
|
|
||||||
minipass: 7.1.3
|
|
||||||
|
|
||||||
path-to-regexp@8.4.2:
|
path-to-regexp@8.4.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user