diff --git a/CLAUDE.md b/CLAUDE.md index 3782e48e..560966e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. -**Options:** `-c ` (YAML config), `-o ` (output directory), `-w ` (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 ` (YAML config), `--models-config ` (pi `models.json` defining models pi's catalogue lacks), `-o ` (output directory), `-w ` (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) ## 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/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/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-`) - `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 @@ -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`) - **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:']`, 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 -- **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=:` (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 and API dialect are unchanged. A base URL only changes the address — `resolveModel` (`apps/worker/src/ai/models.ts`) carries it onto the model descriptor and nothing else. pi's builtin `openai` provider serves a single API (Responses) and dispatches on the provider rather than `model.api`, so an `openai:` gateway run always speaks Responses; a Chat-Completions-only gateway must be reached through a completions-native provider such as `openrouter` instead. `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 is no adaptive-thinking support and no `CLAUDE_ADAPTIVE_THINKING` / `core.adaptive_thinking` setting. Browser automation via `playwright-cli` with session isolation (`-s=`). TOTP generation via `generate-totp` CLI tool. Login flow template at `apps/worker/prompts/shared/login-instructions.txt` supports form, SSO, API, and basic auth. On authenticated whitebox scans, the `validate-authentication` preflight performs the single real login and saves the browser session to `auth-state.json` in the per-session audit directory (path from `authStateFile()` in `apps/worker/src/audit/utils.ts`, derived from `generateAuditPath()`). The validation activity (`apps/worker/src/services/validate-authentication.ts`) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete; `logWorkflowComplete` deletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by `@include(shared/_shared-session.txt)` before their `` block — the partial restores the session and falls through to the full login flow if verification fails. `vuln-auth`/`exploit-auth` omit the include and own their own login +- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/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=:` (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). A model too new for the pinned pi release does not require an SDK bump: `--models-config ` 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 `/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 provider's list. Omitted fields take pi's defaults (`contextWindow` 128000, `maxTokens` 16384), so a large-context model needs them stated. Credentials are unaffected: `RuntimeCredentialStore` outranks any `apiKey` the file carries, so a snippet pasted from `pi.dev/models` can keep its placeholder key while the real secret stays in the environment — and pi's `!command` config-value form is never reached through `apiKey`. Preflight reports `modelRuntime.getError()` **before** the "model not found" check, because a file that fails to parse, fails schema validation, or composes badly otherwise leaves pi with an empty or fallback provider and surfaces as a model-id error that blames `SHANNON_AI_MODEL`. `SHANNON_AI_BASE_URL` overrides the endpoint for any provider (proxies/gateways); the credential and API dialect are unchanged. It is applied after model resolution, so it also wins over a `baseUrl` set in a model config. A base URL only changes the address — `resolveModel` (`apps/worker/src/ai/models.ts`) carries it onto the model descriptor and nothing else, and it grants **no exemption from registry validation**: the model id must resolve for every provider and endpoint alike. A gateway serving a model under its own name, like one newer than the pinned pi release, is described in a `--models-config` file, which puts a real descriptor in the registry rather than guessing one. pi's builtin `openai` provider serves a single API (Responses) and dispatches on the provider rather than `model.api`, so an `openai:` gateway run always speaks Responses; a Chat-Completions-only gateway must be reached through a completions-native provider such as `openrouter` instead. `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 is no adaptive-thinking support and no `CLAUDE_ADAPTIVE_THINKING` / `core.adaptive_thinking` setting. Browser automation via `playwright-cli` with session isolation (`-s=`). TOTP generation via `generate-totp` CLI tool. Login flow template at `apps/worker/prompts/shared/login-instructions.txt` supports form, SSO, API, and basic auth. On authenticated whitebox scans, the `validate-authentication` preflight performs the single real login and saves the browser session to `auth-state.json` in the per-session audit directory (path from `authStateFile()` in `apps/worker/src/audit/utils.ts`, derived from `generateAuditPath()`). The validation activity (`apps/worker/src/services/validate-authentication.ts`) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete; `logWorkflowComplete` deletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by `@include(shared/_shared-session.txt)` before their `` block — the partial restores the session and falls through to the full login flow if verification fails. `vuln-auth`/`exploit-auth` omit the include and own their own login - **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:`) or an `xai` Grok subscription (`SHANNON_AI_MODEL=xai:`); 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/.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 ` (`--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`) diff --git a/README.md b/README.md index c9bf9a97..bdac34c0 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ These reports are from Shannon Open Source scans of Photoview 2.4.0, one of the - **Docker**: required for the worker container. - **Node.js 18+**: required for the recommended `npx` workflow. -- **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). 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 yet 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). @@ -375,11 +375,11 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re ### 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 provider in the Pi harness catalogue, named the same `:` 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. 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 `:` 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 yet carry, such as one released after Shannon's pinned harness version, runs without waiting for a Shannon release. Describe it in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and pass it with `--models-config`. Shannon uses a single unified model setting throughout a pentest. ### 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 LLM 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? diff --git a/apps/cli/src/commands/start.ts b/apps/cli/src/commands/start.ts index ecc4b313..4816c880 100644 --- a/apps/cli/src/commands/start.ts +++ b/apps/cli/src/commands/start.ts @@ -22,6 +22,7 @@ import { FINAL_REPORT_PDF_FILENAME, INTERNAL_DIR, resolveConfig, + resolveModelsConfig, resolveRepo, resolveRunFile, } from '../paths.js'; @@ -37,6 +38,7 @@ export interface StartArgs { url: string; repo: string; config?: string; + modelsConfig?: string; workspace?: string; output?: string; pipelineTesting: boolean; @@ -231,6 +233,7 @@ export async function start(args: StartArgs): Promise { } const repo = resolveRepo(args.repo); const config = args.config ? resolveConfig(args.config) : undefined; + const modelsConfig = args.modelsConfig ? resolveModelsConfig(args.modelsConfig) : undefined; const workspacesDir = getWorkspacesDir(); const workspace = args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`; @@ -322,6 +325,7 @@ export async function start(args: StartArgs): Promise { containerName, envFlags: buildEnvFlags(), ...(config && { config }), + ...(modelsConfig && { modelsConfig }), ...(promptsDir && { promptsDir }), ...(outputDir && { outputDir }), workspace, @@ -530,6 +534,10 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa if (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) { console.log(' Mode: Pipeline Testing'); } diff --git a/apps/cli/src/docker.ts b/apps/cli/src/docker.ts index 3640fb2b..6720db00 100644 --- a/apps/cli/src/docker.ts +++ b/apps/cli/src/docker.ts @@ -407,6 +407,7 @@ export interface WorkerOptions { containerName: string; envFlags: string[]; config?: { hostPath: string; containerPath: string }; + modelsConfig?: { hostPath: string; containerPath: string }; promptsDir?: string; outputDir?: string; workspace: string; @@ -469,6 +470,12 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess { 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. if (opts.outputDir) { args.push('-v', `${opts.outputDir}:/app/output`); diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts index 3c22a7a7..a8cf29f9 100644 --- a/apps/cli/src/help.ts +++ b/apps/cli/src/help.ts @@ -29,6 +29,7 @@ export const START_OPTIONS: readonly (readonly [string, string])[] = [ ['-u, --url ', 'Target URL (required)'], ['-r, --repo ', 'Repository path (required)'], ['-c, --config ', 'Configuration file (YAML)'], + ['--models-config ', "pi model config (models.json) defining models pi's catalogue lacks"], ['-o, --output ', 'Copy deliverables to this directory after the run'], ['-w, --workspace ', 'Named workspace (auto-resumes if it exists)'], ['-f, --follow', 'Stream the scan log until it finishes'], diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 75233dbe..85a27a99 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -171,6 +171,7 @@ interface ParsedStartArgs { url: string; repo: string; config?: string; + modelsConfig?: string; workspace?: string; output?: string; pipelineTesting: boolean; @@ -184,6 +185,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs { url: ['-u', '--url'], repo: ['-r', '--repo'], config: ['-c', '--config'], + modelsConfig: ['--models-config'], output: ['-o', '--output'], workspace: ['-w', '--workspace'], }, @@ -213,6 +215,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs { keepContainer: !!flags.keepContainer, follow: !!flags.follow, ...(values.config && { config: values.config }), + ...(values.modelsConfig && { modelsConfig: values.modelsConfig }), ...(values.workspace && { workspace: values.workspace }), ...(values.output && { output: values.output }), }; diff --git a/apps/cli/src/paths.ts b/apps/cli/src/paths.ts index ff3bc014..3b962c80 100644 --- a/apps/cli/src/paths.ts +++ b/apps/cli/src/paths.ts @@ -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'; @@ -108,3 +108,32 @@ export function resolveConfig(configArg: string): MountPair { 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, + }; +} diff --git a/apps/worker/package.json b/apps/worker/package.json index f1156f79..e88bdf5f 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -39,9 +39,9 @@ "clean": "rm -rf dist" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.2", - "@earendil-works/pi-ai": "^0.84.2", - "@earendil-works/pi-coding-agent": "^0.84.2", + "@earendil-works/pi-agent-core": "^0.84.4", + "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-coding-agent": "^0.84.4", "@gotgenes/pi-permission-system": "^10.9.0", "@temporalio/activity": "1.15.0", "@temporalio/client": "1.15.0", diff --git a/apps/worker/src/ai/models.ts b/apps/worker/src/ai/models.ts index 8555c9f6..8c0ff2f7 100644 --- a/apps/worker/src/ai/models.ts +++ b/apps/worker/src/ai/models.ts @@ -20,6 +20,11 @@ * Resolution returns a pi `Model` plus the `ModelRuntime` that owns its auth, * built over an in-memory credential store primed from the environment. * + * A model too new for the pinned pi release is reachable by passing its descriptor 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 * `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 @@ -32,6 +37,7 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai'; 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, @@ -196,20 +202,43 @@ export function piAuthPresent(): boolean { 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; +} + +/** + * Where pi persists remote model catalogues. Pinned to the writable agent dir because pi + * otherwise derives it from `dirname(modelsPath)`, which is a read-only mount. + */ +function modelsStorePath(): string { + return path.join(getAgentDir(), 'models-store.json'); +} + /** * Build a ModelRuntime whose only credential is the one supplied. Model catalogs * stay offline (`allowModelNetwork` defaults to false) so a scan never blocks on * a catalog refresh. * + * `modelsPath` is always explicit, never pi's default of `/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. + * * 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 * refreshes persist to the host for subsequent runs. */ export async function createModelRuntime(providerId: string, apiKey: string | undefined): Promise { + const modelsPath = modelsConfigPath(); + const modelSources = { + modelsPath: modelsPath ?? null, + ...(modelsPath ? { modelsStorePath: modelsStorePath() } : {}), + }; + 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 { @@ -221,16 +250,13 @@ export interface ModelSelection { } /** - * Resolve a model against a runtime. + * Resolve a model against a runtime, returning undefined when the id is unknown. * - * 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. + * The model must exist in the runtime's registry, whether or not an endpoint override + * is in play — a base URL changes the address and nothing else. A gateway serving a + * model under its own name, or one newer than the pinned pi release, is described in a + * `--models-config` file, which puts a real descriptor in the registry rather than + * guessing one from an unrelated model. */ export function resolveModel( modelRuntime: ModelRuntime, @@ -239,15 +265,9 @@ export function resolveModel( baseUrl: string | undefined, ): Model | undefined { const found = modelRuntime.getModel(providerId, modelId); - if (found) { - return baseUrl ? { ...found, baseUrl } : found; - } - if (!baseUrl) return undefined; + if (!found) return undefined; - const reference = modelRuntime.getModels(providerId)[0]; - if (!reference) return undefined; - - return { ...reference, id: modelId, name: modelId, baseUrl }; + return baseUrl ? { ...found, baseUrl } : found; } /** diff --git a/apps/worker/src/ai/sast/capella/temporal/activity-types.ts b/apps/worker/src/ai/sast/capella/temporal/activity-types.ts index 24841066..921cd12c 100644 --- a/apps/worker/src/ai/sast/capella/temporal/activity-types.ts +++ b/apps/worker/src/ai/sast/capella/temporal/activity-types.ts @@ -276,7 +276,7 @@ function policy( } 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'), 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'), diff --git a/apps/worker/src/audit/safe-fields.ts b/apps/worker/src/audit/safe-fields.ts index d4421b9c..67489709 100644 --- a/apps/worker/src/audit/safe-fields.ts +++ b/apps/worker/src/audit/safe-fields.ts @@ -34,6 +34,9 @@ const SAFE_ERROR_MESSAGES: Readonly> = { [ErrorCode.TARGET_UNREACHABLE]: 'The target could not be reached.', [ErrorCode.AUTH_FAILED]: 'Authentication validation failed.', [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.', }; const ERROR_CATEGORIES = new Set([ diff --git a/apps/worker/src/paths.ts b/apps/worker/src/paths.ts index 1cca61f4..0540ce59 100644 --- a/apps/worker/src/paths.ts +++ b/apps/worker/src/paths.ts @@ -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/) */ 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 */ export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables'; diff --git a/apps/worker/src/services/error-handling.ts b/apps/worker/src/services/error-handling.ts index 29921b62..474e9497 100644 --- a/apps/worker/src/services/error-handling.ts +++ b/apps/worker/src/services/error-handling.ts @@ -332,6 +332,14 @@ function classifyByErrorCode(code: ErrorCode, retryableFromError: boolean): { ty case ErrorCode.AUTH_FAILED: 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: return { type: 'AuthLoginFailedError', retryable: false }; diff --git a/apps/worker/src/services/preflight.ts b/apps/worker/src/services/preflight.ts index 3b98dfe4..2375f5be 100644 --- a/apps/worker/src/services/preflight.ts +++ b/apps/worker/src/services/preflight.ts @@ -40,6 +40,7 @@ import { createModelRuntime, GENERIC_API_KEY_ENV, type ModelSpec, + modelsConfigPath, PI_CATALOG_URL, piAuthPresent, resolveModel, @@ -317,7 +318,7 @@ async function validateCredentials(logger: ActivityLogger): Promise = { AuthenticationError: ErrorCode.AUTH_FAILED, + ModelNotFoundError: ErrorCode.MODEL_NOT_FOUND, + ModelConfigError: ErrorCode.MODEL_CONFIG_INVALID, ConfigurationError: ErrorCode.CONFIG_VALIDATION_FAILED, OutputValidationError: ErrorCode.OUTPUT_VALIDATION_FAILED, AgentExecutionError: ErrorCode.AGENT_EXECUTION_FAILED, @@ -54,6 +56,8 @@ export function classifyErrorCode(error: unknown): ErrorCode | undefined { */ const REMEDIATION_HINTS: Record = { 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.', GitError: 'Check repository path and git state.', InvalidTargetError: 'Verify the target URL is correct and accessible.', @@ -69,6 +73,8 @@ const REMEDIATION_HINTS: Record = { */ const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly> = { 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.', OutputValidationError: 'A scan step returned an unusable result.', AgentExecutionError: 'An agent could not complete its work.', diff --git a/apps/worker/src/types/errors.ts b/apps/worker/src/types/errors.ts index 9d45992c..df016672 100644 --- a/apps/worker/src/types/errors.ts +++ b/apps/worker/src/types/errors.ts @@ -40,6 +40,8 @@ export enum ErrorCode { TARGET_UNREACHABLE = 'TARGET_UNREACHABLE', AUTH_FAILED = 'AUTH_FAILED', AUTH_LOGIN_FAILED = 'AUTH_LOGIN_FAILED', + MODEL_NOT_FOUND = 'MODEL_NOT_FOUND', + MODEL_CONFIG_INVALID = 'MODEL_CONFIG_INVALID', } export type PentestErrorType = 'config' | 'network' | 'prompt' | 'filesystem' | 'validation' | 'unknown'; diff --git a/docs/ai-providers.md b/docs/ai-providers.md index 4b06abd0..97405293 100644 --- a/docs/ai-providers.md +++ b/docs/ai-providers.md @@ -35,6 +35,8 @@ export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route thr 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 yet carry, such as one released after Shannon's pinned Pi version, is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration). + `npx @keygraph/shannon setup` exposes this as the **Other provider** option. > [!IMPORTANT] @@ -107,13 +109,15 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens, `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. -This works for **any** provider, curated or not. The one rule is that a provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect: +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: | Provider prefix | Dialect the endpoint must speak | | --- | --- | | `anthropic:` | Anthropic Messages | | `openai:` | OpenAI Responses | +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 @@ -132,6 +136,121 @@ export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1 `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. +## Custom model configuration + +A model released after Shannon's pinned Pi version is not in the harness catalogue yet, so `SHANNON_AI_MODEL` alone cannot reach it. Rather than wait for a Shannon release, describe the model yourself and pass the file with `--models-config`: + +```bash +npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json +``` + +```bash +./shannon start -u https://example.com -r ./my-repo --models-config ./models.json +``` + +[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: + +```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": "" } + ] + } + } +} +``` + +Then name the model and run: + +```bash +export SHANNON_AI_API_KEY=ollama # any value, see below +export SHANNON_AI_MODEL=ollama: +./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) A ChatGPT Plus or Pro Codex subscription can run Shannon. Shannon reuses a login created by Pi. @@ -197,7 +316,8 @@ These instructions apply only to `shannon-v1`. 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 an LLM 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 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. diff --git a/llms-full.txt b/llms-full.txt index fd69321b..ac72ba76 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -97,7 +97,7 @@ Sample penetration test reports from intentionally vulnerable applications, prod - **Docker**: required for the worker container. - **Node.js 18+**: required for the recommended `npx` workflow. -- **AI provider credentials**: 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). 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 yet 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). @@ -346,11 +346,11 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re ### 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 provider in the Pi harness catalogue, named the same `:` 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. 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 `:` 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 yet carry, such as one released after Shannon's pinned harness version, runs without waiting for a Shannon release. Describe it in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and pass it with `--models-config`. Shannon uses a single unified model setting throughout a pentest. ### 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 LLM 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? @@ -754,6 +754,8 @@ export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route thr 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 yet carry, such as one released after Shannon's pinned Pi version, is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration). + `npx @keygraph/shannon setup` exposes this as the **Other provider** option. > [!IMPORTANT] @@ -826,13 +828,15 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens, `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. -This works for **any** provider, curated or not. The one rule is that a provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect: +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: | Provider prefix | Dialect the endpoint must speak | | --- | --- | | `anthropic:` | Anthropic Messages | | `openai:` | OpenAI Responses | +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 @@ -851,6 +855,121 @@ export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1 `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. +## Custom model configuration + +A model released after Shannon's pinned Pi version is not in the harness catalogue yet, so `SHANNON_AI_MODEL` alone cannot reach it. Rather than wait for a Shannon release, describe the model yourself and pass the file with `--models-config`: + +```bash +npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json +``` + +```bash +./shannon start -u https://example.com -r ./my-repo --models-config ./models.json +``` + +[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: + +```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": "" } + ] + } + } +} +``` + +Then name the model and run: + +```bash +export SHANNON_AI_API_KEY=ollama # any value, see below +export SHANNON_AI_MODEL=ollama: +./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) A ChatGPT Plus or Pro Codex subscription can run Shannon. Shannon reuses a login created by Pi. @@ -916,7 +1035,8 @@ These instructions apply only to `shannon-v1`. 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 an LLM 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 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. diff --git a/llms.txt b/llms.txt index 5aabdabe..be73b3e4 100644 --- a/llms.txt +++ b/llms.txt @@ -13,7 +13,7 @@ Use this file as the concise entry point for AI agents and LLMs reading this rep - [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. -- [AI Providers](docs/ai-providers.md): Anthropic, OpenAI, xAI, AWS Bedrock, any other Pi-supported provider, and custom LLM 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. - [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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b307c965..f725cb76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,17 +46,17 @@ importers: apps/worker: dependencies: '@earendil-works/pi-agent-core': - specifier: ^0.84.2 - version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6) + specifier: ^0.84.4 + 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': - specifier: ^0.84.2 - version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6) + specifier: ^0.84.4 + 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': - specifier: ^0.84.2 - version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6) + specifier: ^0.84.4 + 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': 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': specifier: 1.15.0 version: 1.15.0 @@ -295,34 +295,34 @@ packages: '@clack/prompts@1.1.0': resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} - '@earendil-works/pi-agent-core@0.84.2': - resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==} + '@earendil-works/pi-agent-core@0.84.4': + resolution: {integrity: sha512-HyUnjaOXj6oN/6SNcr8A1J/ElRQA50FtIE0XUTSKAQVqmdlb9qdojOyUQwF/jULE5+yOEtGuVgi/N1RnBiNG+g==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.84.2': - resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==} + '@earendil-works/pi-ai@0.84.4': + resolution: {integrity: sha512-AClAZxf5+c4RRu44NJPS6wyQy+Nmq+Mzyyrdvm4ZVMNuixelO02RZX4G4Aq1F145Yzp43wnM5S+hLlSI7ypfVw==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-client@0.84.2': - resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==} + '@earendil-works/pi-client@0.84.4': + resolution: {integrity: sha512-q398WY/3ZQHTizk7IKxApzqFV0xt4yM9LkSkwyqeLK5Bj5RwRjOWxESt26z4LgNp4O+8hqhqFPf/8fj4H5rE4A==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-coding-agent@0.84.2': - resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==} + '@earendil-works/pi-coding-agent@0.84.4': + resolution: {integrity: sha512-jmOlrqUmvhh/siNWFRXjYLJzhKFIHNsAQaysRwzQPQFnPAaV/vhqHsLH/MBsIISA1Rjj7WTUFR3nJrpXoLx39w==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-protocol@0.84.2': - resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==} + '@earendil-works/pi-protocol@0.84.4': + resolution: {integrity: sha512-acyE9ozxkMiWiz/xyWpU0O9vwnYv0hyG889Vniv6Sg9c9zfsX+8MePnDNphBacY2Fvm1rxdsGmiVDSZl9yuDFA==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-telemetry@0.84.2': - resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==} + '@earendil-works/pi-telemetry@0.84.4': + resolution: {integrity: sha512-8e2CuxM+ht+hedQXTZmi5JVl6/xDK9RpSDL2+MbITevKYQhMZ/z6lJOTFgox3HQyGxO8mOZEtYGVeQNaD4OzqA==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-tui@0.84.2': - resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==} + '@earendil-works/pi-tui@0.84.4': + resolution: {integrity: sha512-nPUnwDkLtupPXnZQYrCwPFcuTydCDqTY6ZbFqhsL4S4kVq0AT418kPa/6uXwtaCD+MjBNBltb7ScTYX65yeE1w==} engines: {node: '>=22.19.0'} '@emnapi/core@1.9.1': @@ -588,10 +588,6 @@ packages: '@nodable/entities@2.1.1': 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': resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} @@ -1360,10 +1356,6 @@ packages: glob-to-regexp@0.4.1: 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: resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==} engines: {node: '>=18'} @@ -1575,10 +1567,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1665,10 +1653,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 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: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -2456,10 +2440,10 @@ snapshots: '@clack/core': 1.1.0 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: - '@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-telemetry': 0.84.2 + '@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.4 diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -2472,13 +2456,12 @@ snapshots: - ws - 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: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@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)) - '@opentelemetry/api': 1.9.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -2493,22 +2476,21 @@ snapshots: - ws - zod - '@earendil-works/pi-client@0.84.2': + '@earendil-works/pi-client@0.84.4': 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: - '@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-ai': 0.84.2(@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-protocol': 0.84.2 - '@earendil-works/pi-tui': 0.84.2 + '@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.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6) + '@earendil-works/pi-client': 0.84.4 + '@earendil-works/pi-protocol': 0.84.4 + '@earendil-works/pi-tui': 0.84.4 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 diff: 8.0.4 - glob: 13.0.6 grok-mermaid: 0.2.2 highlight.js: 10.7.3 hosted-git-info: 9.0.3 @@ -2530,13 +2512,13 @@ snapshots: - ws - zod - '@earendil-works/pi-protocol@0.84.2': + '@earendil-works/pi-protocol@0.84.4': dependencies: 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: get-east-asian-width: 1.6.0 marked: 18.0.5 @@ -2570,10 +2552,10 @@ snapshots: - supports-color - 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: - '@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 + '@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 tree-sitter-bash: 0.25.1 web-tree-sitter: 0.26.9 transitivePeerDependencies: @@ -2820,8 +2802,6 @@ snapshots: '@nodable/entities@2.1.1': {} - '@opentelemetry/api@1.9.0': {} - '@oxc-project/types@0.122.0': {} '@protobufjs/aspromise@1.1.2': {} @@ -3599,12 +3579,6 @@ snapshots: 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: dependencies: base64-js: 1.5.1 @@ -3813,8 +3787,6 @@ snapshots: minimist@1.2.8: {} - minipass@7.1.3: {} - ms@2.1.3: {} ms@3.0.0-canary.1: {} @@ -3877,11 +3849,6 @@ snapshots: 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: optional: true