{"repo": "0xsteph/pentest-ai", "files": {"docs/llm-providers.md": {"e42f8212e270e50b3dbf8dd70cd84e84ac8c15ff": {"t": 1785674493.291458, "n": 8957, "h": "5b24d23ce852cc2f", "c": "# LLM providers\n\n`ptai` is LLM-driven by default. You point it at any one of four supported provider families and it does the rest. This page covers how to configure each one, the gotchas, and the env vars that override anything.\n\n## Quick start\n\nThe fastest way to get going:\n\n```bash\n# Use Claude (if you have an Anthropic key)\nexport ANTHROPIC_API_KEY=sk-ant-...\nptai start http://target\n\n# Use GPT\nexport OPENAI_API_KEY=sk-...\nptai start http://target\n\n# Use a local Ollama model (free, no API key)\nexport PENTEST_AI_LLM_PROVIDER=ollama\nptai start http://target\n\n# Skip the LLM entirely (deterministic tools only)\nptai start http://target --no-llm\n```\n\nIf you set both `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`, anthropic wins. To force a specific provider regardless of which keys you have set, use `PENTEST_AI_LLM_PROVIDER`.\n\n## Env vars\n\n| Variable | Purpose |\n|----------|---------|\n| `PENTEST_AI_LLM_PROVIDER` | Force a provider: `anthropic`, `openai`, `ollama`, `litellm`, or any name LiteLLM recognises (e.g. `deepseek`, `groq`, `together`). When unset, auto-detected from whichever API key you have. |\n| `PENTEST_AI_MODEL` | Force a model name. Honoured by every provider. Useful when you're pointing at an OpenAI-compatible endpoint that doesn't serve `gpt-4o`. |\n| `PENTEST_AI_API_KEY` | Provider-agnostic key. Falls back to `OPENAI_API_KEY` then `ANTHROPIC_API_KEY` if not set. |\n| `ANTHROPIC_API_KEY` | Used when provider is `anthropic`. |\n| `OPENAI_API_KEY` | Used when provider is `openai`. |\n| `OPENAI_BASE_URL` | Override the OpenAI endpoint. This is the knob that makes every OpenAI-compatible third-party API work (DeepSeek, Groq, Together AI, local llama.cpp servers, vLLM, etc.). |\n| `OLLAMA_HOST` | Where Ollama is running. Defaults to `http://localhost:11434`. Same env var the official Ollama CLI uses. |\n| `OLLAMA_BASE_URL` | Older alias for `OLLAMA_HOST`. Both work; `OLLAMA_HOST` wins if both are set. |\n| `PENTEST_AI_PRICE_LIMIT` | USD spending cap for one engagement. Default `10`. Set to `0` for unlimited. Standalone CLI path only - MCP-driven engagements use the client's own billing. |\n\n## Per-provider setup\n\n### Anthropic (Claude)\n\nThe default if you have `ANTHROPIC_API_KEY` set.\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\nptai start http://target\n```\n\nTo pin a specific model:\n\n```bash\nexport PENTEST_AI_MODEL=claude-opus-4-5\nptai start http://target\n```\n\nDefault model is `claude-sonnet-4-20250514` (Sonnet 4).\n\n### OpenAI (and OpenAI-compatible endpoints)\n\nThe OpenAI provider works against the real OpenAI API and against any third-party API that speaks the OpenAI Chat Completions protocol. The Chat Completions API has become the de-facto wire format for hosted LLM endpoints, so this provider covers most \"cloud-deployed model\" cases.\n\nReal OpenAI:\n\n```bash\nexport OPENAI_API_KEY=sk-...\nptai start http://target\n```\n\n**DeepSeek cloud** (and any other OpenAI-compatible endpoint):\n\n```bash\nexport PENTEST_AI_LLM_PROVIDER=openai\nexport OPENAI_API_KEY=\nexport OPENAI_BASE_URL=https://api.deepseek.com/v1\nexport PENTEST_AI_MODEL=deepseek-chat\nptai start http://target\n```\n\nThe three things you usually need to override for a non-OpenAI endpoint:\n\n1. `OPENAI_BASE_URL` - where the API lives.\n2. `OPENAI_API_KEY` - your key for that service.\n3. `PENTEST_AI_MODEL` - what your endpoint actually serves. The default `gpt-4o` will 404 against DeepSeek, Groq, etc.\n\nSame recipe works for **Groq**:\n\n```bash\nexport OPENAI_API_KEY=\nexport OPENAI_BASE_URL=https://api.groq.com/openai/v1\nexport PENTEST_AI_MODEL=llama-3.3-70b-versatile\n```\n\n**Together AI**:\n\n```bash\nexport OPENAI_API_KEY=\nexport OPENAI_BASE_URL=https://api.together.xyz/v1\nexport PENTEST_AI_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo\n```\n\n**Local llama.cpp / vLLM / LM Studio**:\n\n```bash\nexport PENTEST_AI_LLM_PROVIDER=openai\nexport OPENAI_API_KEY=dummy\nexport OPENAI_BASE_URL=http://localhost:8080/v1\nexport PENTEST_AI_MODEL=\n```\n\n### Ollama (local)\n\nNo API key needed. Just point at a running Ollama instance and tell `ptai` which model to use.\n\n```bash\n# Assumes ollama is running at localhost:11434\nexport PENTEST_AI_LLM_PROVIDER=ollama\nptai start http://target\n```\n\nPin a specific model:\n\n```bash\nexport PENTEST_AI_LLM_PROVIDER=ollama\nexport PENTEST_AI_MODEL=qwen2.5-coder:7b\nptai start http://target\n```\n\nDefault model is `llama3.1`. Make sure you've pulled the model first: `ollama pull qwen2.5-coder:7b`.\n\nRemote Ollama:\n\n```bash\nexport PENTEST_AI_LLM_PROVIDER=ollama\nexport OLLAMA_HOST=http://192.168.1.50:11434\nexport PENTEST_AI_MODEL=qwen2.5-coder:7b\nptai start http://target\n```\n\n### LiteLLM (everything else)\n\nIf your provider isn't on the list above, `ptai` will try LiteLLM, which speaks to 300+ provider backends. Install it first:\n\n```bash\npip install litellm\n```\n\nThen use the LiteLLM model-naming convention:\n\n```bash\nexport PENTEST_AI_LLM_PROVIDER=litellm\nexport PENTEST_AI_MODEL=openrouter/anthropic/claude-sonnet-4\nexport OPENROUTER_API_KEY=...\nptai start http://target\n```\n\nOther examples:\n\n```bash\n# Azure OpenAI\nexport PENTEST_AI_LLM_PROVIDER=litellm\nexport PENTEST_AI_MODEL=azure/gpt-4o\nexport AZURE_API_KEY=...\nexport AZURE_API_BASE=https://your-resource.openai.azure.com\n\n# Bedrock\nexport PENTEST_AI_LLM_PROVIDER=litellm\nexport PENTEST_AI_MODEL=bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0\nexport AWS_ACCESS_KEY_ID=...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Vertex AI\nexport PENTEST_AI_LLM_PROVIDER=litellm\nexport PENTEST_AI_MODEL=vertex_ai/gemini-2.0-flash\nexport GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json\n```\n\nLiteLLM's [provider docs](https://docs.litellm.ai/docs/providers) cover the model naming for everything else.\n\n## Troubleshooting\n\n### \"Scanning...\" then silent exit\n\nYou were probably hitting [issue #12](https://github.com/0xSteph/pentest-ai/issues/12). Fixed in v0.16.1. Upgrade with `pipx upgrade ptai`.\n\n### 4xx / 5xx from a provider\n\nSince 0.16.3, the error message includes the full response body from the upstream endpoint, e.g.:\n\n OpenAI-compatible endpoint at https://api.deepseek.com/v1 returned HTTP 400 for model='deepseek-chat': {\"error\":{\"message\":\"\",\"type\":\"invalid_request_error\",...}}\n\nThat body is your diagnostic. Common causes:\n\n- **400** - the request body is malformed for that endpoint. Sometimes the model name needs an exact match (e.g. DeepSeek renamed `deepseek-coder` to `deepseek-chat` mid-2024).\n- **401** - wrong API key, or key is for a different provider than the one your config picked.\n- **404** - `OPENAI_BASE_URL` works but `PENTEST_AI_MODEL` doesn't exist on that endpoint. Set it to a model the endpoint actually serves.\n- **422** - endpoint speaks OpenAI Chat Completions but doesn't accept one of the fields ptai sends (rare; report it).\n\n### \"How do I view the logs?\"\n\n`ptai` logs to stderr at INFO level by default. Bump to DEBUG for full request/response details:\n\n PYTHONLOGLEVEL=DEBUG ptai start http://target 2>&1 | tee ptai.log\n\nOr filter the warnings the agent loop emits when LLM calls fail:\n\n ptai start http://target 2>&1 | grep -i \"llm\\|error\"\n\nIf the agent reports `LLM call failed: ` and the message includes the upstream response body (since 0.16.3), that body is the diagnosis.\n\n### Check your config without running a scan\n\nIf you just want to confirm ptai will pick the right provider + model for your env, this one-liner prints what the factory resolves:\n\n python -c \"from engine.llm.factory import _create_inner_client as c; cl, m = c(); print(type(cl).__name__, m, getattr(cl, 'base_url', ''))\"\n\nOutput is something like `OpenAIProvider deepseek-chat https://api.deepseek.com/v1` - sanity-check that against what you intended.\n\n### Ollama hangs forever\n\nDefault timeout is 300 s. If you set `OLLAMA_HOST` to an unreachable address, every call will sit at TCP-connect timeout. Confirm reachability first:\n\n```bash\ncurl $OLLAMA_HOST/api/tags\n```\n\n### LiteLLM ImportError\n\nInstall it: `pip install litellm`. The library's a big optional dep; `ptai` doesn't bundle it by default.\n\n### Want to disable the LLM entirely\n\n```bash\nptai start http://target --no-llm\n```\n\nThe probes still run - you lose the LLM-driven planner, not the security tools.\n\n## Driving ptai over MCP instead\n\nEverything on this page is for the standalone CLI (Path 3). If you're running ptai from inside Claude Code, Cursor, Copilot, or another MCP client, the client provides the LLM and none of these env vars matter. See the README for MCP install paths.\n\n## Spending caps\n\nThe standalone CLI agent loop watches an engagement-wide spend cap. Defaults to $10 USD. Override per-run:\n\n```bash\nPENTEST_AI_PRICE_LIMIT=50 ptai start http://target\n```\n\n`PENTEST_AI_PRICE_LIMIT=0` means unlimited. The cap only fires when the configured provider exposes per-request usage data; local Ollama returns zero by default, so the cap is effectively a no-op there.\n"}}, "CHANGELOG.md": {"e42f8212e270e50b3dbf8dd70cd84e84ac8c15ff": {"t": 1785746395.7564845, "n": 73570, "h": "f7267e8fea57f4e2", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [1.1.0] - 2026-06-29\n\nVerification coverage and resilience. This release roughly doubles the set of vulnerability classes ptai can oracle-verify, and fixes the bug that produced 0 verified findings on a live, otherwise-vulnerable app.\n\n### Added\n\n- **Ten new verification oracles**, each proving via a machine-checkable differential with a control that must fail on a safe target (so a non-vulnerable app abstains rather than earning a badge): trusted-header bypass, JWT `alg:none`, host-header poisoning, XXE, type confusion, stored XSS, sequential IDOR, mass-assignment, non-blind SSRF, and SQLi login-bypass. With the existing oracles, ptai now oracle-verifies 14 distinct classes.\n- **Path-traversal oracle.** A traversal finding re-fetches the payload and confirms the disclosed file content reproduces, with a benign control twin. The bundled `ptai demo` reports 4 findings, all 4 oracle-VERIFIED.\n\n### Fixed\n\n- **Scope safety: never test third parties.** The web scan scraped external links out of a target's content (e.g. SoundCloud/Steam embeds) and fed them to active tools like sqlmap and dalfox — unauthorized active testing of systems outside the engagement. Active-tool targets are now host-locked to the engagement target.\n- **Verification resilience.** An aggressive sweep could knock a fragile single-container target over; the verify phase then ran against a dead target and reported 0 verified despite valid, replayable recipes. The verify phase now waits for the target to answer again before running its oracles. On OWASP Juice Shop this took a scan from 0 to 12 oracle-verified findings.\n\n### Validation\n\n- A scoped scan of OWASP Juice Shop oracle-verifies 12 findings (JWT `alg:none`, BOLA, IDOR, type confusion). A multi-vuln test honeypot verifies 23 across the 14 classes, at 100% precision with zero false positives. These are reproducible runs, not field false-positive rates; the honeypot and clean-app gates are in the repo.\n\n## [1.0.0] - 2026-06-25\n\nThe Trust Spine release. ptai now proves what it finds: a finding earns the VERIFIED badge only when a machine oracle re-runs the exploit and reproduces it N out of N times. No oracle, no badge, and no verdict from an LLM assertion.\n\n### Added\n\n- **Verifier + receipt contract.** A verified verdict must name the machine oracle that earned it, enforced in code.\n- **Recipe-driven oracles:** unescaped reflection, open redirect, IDOR/BOLA differential, error signature, MCP exposure, SQLi boolean and boolean-blind, SSTI (including error-based), sensitive data exposure, and HTTP request smuggling (CL.TE), plus an out-of-band OAST callback oracle for blind SSRF/XXE over a self-hosted loopback collaborator.\n- **Portable proof capsules and `ptai replay`**, plus multi-hop chain capsules, so a finding's proof travels and replays without trusting ptai.\n- **Prove-or-kill gating:** third-party scanner output (nuclei, nikto, zap) is held back until an oracle re-proves it.\n- **Verified-only SARIF export** (`ptai export --sarif`) with a frozen, versioned export-properties contract.\n- **REST path-parameter injection** in the SQLi/XSS/SSTI fuzzers, so injection in `/rest/products/` style routes is caught, not just query parameters.\n- **Experimental CL.TE request-smuggling discovery probe**, oracle-gated.\n- **CI gate** (`--fail-on verified`) with a composite action, and a bundled `ptai demo`.\n\n### Changed\n\n- **Honest verdicts.** An oracle miss now reads as `candidate` (could not re-prove), never `refuted`; `refuted` is reserved for an oracle that can truly disprove a vulnerability.\n- **Impact-honest severity:** a bare out-of-band callback proves existence, not impact, so it is rated medium until impact is reproduced.\n\n### Verified\n\n- 100% precision with zero false positives on the honeypot benchmark, and field-validated against live OWASP Juice Shop: a real broken-object-level-authorization bug verified end to end with a replayable proof capsule.\n\n## [0.17.2] - 2026-06-09\n\nPatch release: tool-installer fixes from user feedback on issue #12 (`lukeswitz`).\n\n### Fixed\n\n- **No more duplicate installs.** Tool detection now also checks `~/go/bin`, `~/.cargo/bin`, and `~/.local/bin`, not just the bare `PATH`. Go- and cargo-installed tools were being seen as missing and reinstalled on every run because those dirs are commonly absent from `PATH`.\n- **Cargo and manual install methods now work.** `feroxbuster`, `findomain`, and `rustscan` (cargo) and `evil-winrm` (gem) previously fell through to \"Unsupported install method\" and never installed.\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "0241da53b983e50f1c608a9f90f6709bf9a50c36": {"t": 1785763250.6995223, "n": 73570, "h": "f7267e8fea57f4e2", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [1.1.0] - 2026-06-29\n\nVerification coverage and resilience. This release roughly doubles the set of vulnerability classes ptai can oracle-verify, and fixes the bug that produced 0 verified findings on a live, otherwise-vulnerable app.\n\n### Added\n\n- **Ten new verification oracles**, each proving via a machine-checkable differential with a control that must fail on a safe target (so a non-vulnerable app abstains rather than earning a badge): trusted-header bypass, JWT `alg:none`, host-header poisoning, XXE, type confusion, stored XSS, sequential IDOR, mass-assignment, non-blind SSRF, and SQLi login-bypass. With the existing oracles, ptai now oracle-verifies 14 distinct classes.\n- **Path-traversal oracle.** A traversal finding re-fetches the payload and confirms the disclosed file content reproduces, with a benign control twin. The bundled `ptai demo` reports 4 findings, all 4 oracle-VERIFIED.\n\n### Fixed\n\n- **Scope safety: never test third parties.** The web scan scraped external links out of a target's content (e.g. SoundCloud/Steam embeds) and fed them to active tools like sqlmap and dalfox — unauthorized active testing of systems outside the engagement. Active-tool targets are now host-locked to the engagement target.\n- **Verification resilience.** An aggressive sweep could knock a fragile single-container target over; the verify phase then ran against a dead target and reported 0 verified despite valid, replayable recipes. The verify phase now waits for the target to answer again before running its oracles. On OWASP Juice Shop this took a scan from 0 to 12 oracle-verified findings.\n\n### Validation\n\n- A scoped scan of OWASP Juice Shop oracle-verifies 12 findings (JWT `alg:none`, BOLA, IDOR, type confusion). A multi-vuln test honeypot verifies 23 across the 14 classes, at 100% precision with zero false positives. These are reproducible runs, not field false-positive rates; the honeypot and clean-app gates are in the repo.\n\n## [1.0.0] - 2026-06-25\n\nThe Trust Spine release. ptai now proves what it finds: a finding earns the VERIFIED badge only when a machine oracle re-runs the exploit and reproduces it N out of N times. No oracle, no badge, and no verdict from an LLM assertion.\n\n### Added\n\n- **Verifier + receipt contract.** A verified verdict must name the machine oracle that earned it, enforced in code.\n- **Recipe-driven oracles:** unescaped reflection, open redirect, IDOR/BOLA differential, error signature, MCP exposure, SQLi boolean and boolean-blind, SSTI (including error-based), sensitive data exposure, and HTTP request smuggling (CL.TE), plus an out-of-band OAST callback oracle for blind SSRF/XXE over a self-hosted loopback collaborator.\n- **Portable proof capsules and `ptai replay`**, plus multi-hop chain capsules, so a finding's proof travels and replays without trusting ptai.\n- **Prove-or-kill gating:** third-party scanner output (nuclei, nikto, zap) is held back until an oracle re-proves it.\n- **Verified-only SARIF export** (`ptai export --sarif`) with a frozen, versioned export-properties contract.\n- **REST path-parameter injection** in the SQLi/XSS/SSTI fuzzers, so injection in `/rest/products/` style routes is caught, not just query parameters.\n- **Experimental CL.TE request-smuggling discovery probe**, oracle-gated.\n- **CI gate** (`--fail-on verified`) with a composite action, and a bundled `ptai demo`.\n\n### Changed\n\n- **Honest verdicts.** An oracle miss now reads as `candidate` (could not re-prove), never `refuted`; `refuted` is reserved for an oracle that can truly disprove a vulnerability.\n- **Impact-honest severity:** a bare out-of-band callback proves existence, not impact, so it is rated medium until impact is reproduced.\n\n### Verified\n\n- 100% precision with zero false positives on the honeypot benchmark, and field-validated against live OWASP Juice Shop: a real broken-object-level-authorization bug verified end to end with a replayable proof capsule.\n\n## [0.17.2] - 2026-06-09\n\nPatch release: tool-installer fixes from user feedback on issue #12 (`lukeswitz`).\n\n### Fixed\n\n- **No more duplicate installs.** Tool detection now also checks `~/go/bin`, `~/.cargo/bin`, and `~/.local/bin`, not just the bare `PATH`. Go- and cargo-installed tools were being seen as missing and reinstalled on every run because those dirs are commonly absent from `PATH`.\n- **Cargo and manual install methods now work.** `feroxbuster`, `findomain`, and `rustscan` (cargo) and `evil-winrm` (gem) previously fell through to \"Unsupported install method\" and never installed.\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "be82cc872804963f3e91a629f5a310c605e0e6e0": {"t": 1785763252.2512784, "n": 72498, "h": "56c8f8a484b51e23", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [1.0.1] - 2026-06-26\n\nA safety patch and a verification-coverage fix.\n\n### Fixed\n\n- **Scope safety: never test third parties.** The web scan scraped external links out of a target's content (e.g. SoundCloud/Steam embeds) and fed them to active tools like sqlmap and dalfox — unauthorized active testing of systems outside the engagement. Active-tool targets are now host-locked to the engagement target. This also stopped the scan from overloading and crashing the target before the verify phase could run, which is what produced 0 verified findings on otherwise-vulnerable apps.\n\n### Added\n\n- **Path-traversal oracle.** A path-traversal finding is now oracle-verified — it re-fetches the traversal payload and confirms the disclosed file content reproduces, with a benign control twin to avoid false positives. The bundled `ptai demo` now reports 4 findings, all 4 oracle-VERIFIED.\n\n## [1.0.0] - 2026-06-25\n\nThe Trust Spine release. ptai now proves what it finds: a finding earns the VERIFIED badge only when a machine oracle re-runs the exploit and reproduces it N out of N times. No oracle, no badge, and no verdict from an LLM assertion.\n\n### Added\n\n- **Verifier + receipt contract.** A verified verdict must name the machine oracle that earned it, enforced in code.\n- **Recipe-driven oracles:** unescaped reflection, open redirect, IDOR/BOLA differential, error signature, MCP exposure, SQLi boolean and boolean-blind, SSTI (including error-based), sensitive data exposure, and HTTP request smuggling (CL.TE), plus an out-of-band OAST callback oracle for blind SSRF/XXE over a self-hosted loopback collaborator.\n- **Portable proof capsules and `ptai replay`**, plus multi-hop chain capsules, so a finding's proof travels and replays without trusting ptai.\n- **Prove-or-kill gating:** third-party scanner output (nuclei, nikto, zap) is held back until an oracle re-proves it.\n- **Verified-only SARIF export** (`ptai export --sarif`) with a frozen, versioned export-properties contract.\n- **REST path-parameter injection** in the SQLi/XSS/SSTI fuzzers, so injection in `/rest/products/` style routes is caught, not just query parameters.\n- **Experimental CL.TE request-smuggling discovery probe**, oracle-gated.\n- **CI gate** (`--fail-on verified`) with a composite action, and a bundled `ptai demo`.\n\n### Changed\n\n- **Honest verdicts.** An oracle miss now reads as `candidate` (could not re-prove), never `refuted`; `refuted` is reserved for an oracle that can truly disprove a vulnerability.\n- **Impact-honest severity:** a bare out-of-band callback proves existence, not impact, so it is rated medium until impact is reproduced.\n\n### Verified\n\n- 100% precision with zero false positives on the honeypot benchmark, and field-validated against live OWASP Juice Shop: a real broken-object-level-authorization bug verified end to end with a replayable proof capsule.\n\n## [0.17.2] - 2026-06-09\n\nPatch release: tool-installer fixes from user feedback on issue #12 (`lukeswitz`).\n\n### Fixed\n\n- **No more duplicate installs.** Tool detection now also checks `~/go/bin`, `~/.cargo/bin`, and `~/.local/bin`, not just the bare `PATH`. Go- and cargo-installed tools were being seen as missing and reinstalled on every run because those dirs are commonly absent from `PATH`.\n- **Cargo and manual install methods now work.** `feroxbuster`, `findomain`, and `rustscan` (cargo) and `evil-winrm` (gem) previously fell through to \"Unsupported install method\" and never installed.\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "30d4662c6ec48189b27b12a45896b5416487b0be": {"t": 1785763254.0057473, "n": 71611, "h": "bfec8701024576eb", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [1.0.0] - 2026-06-25\n\nThe Trust Spine release. ptai now proves what it finds: a finding earns the VERIFIED badge only when a machine oracle re-runs the exploit and reproduces it N out of N times. No oracle, no badge, and no verdict from an LLM assertion.\n\n### Added\n\n- **Verifier + receipt contract.** A verified verdict must name the machine oracle that earned it, enforced in code.\n- **Recipe-driven oracles:** unescaped reflection, open redirect, IDOR/BOLA differential, error signature, MCP exposure, SQLi boolean and boolean-blind, SSTI (including error-based), sensitive data exposure, and HTTP request smuggling (CL.TE), plus an out-of-band OAST callback oracle for blind SSRF/XXE over a self-hosted loopback collaborator.\n- **Portable proof capsules and `ptai replay`**, plus multi-hop chain capsules, so a finding's proof travels and replays without trusting ptai.\n- **Prove-or-kill gating:** third-party scanner output (nuclei, nikto, zap) is held back until an oracle re-proves it.\n- **Verified-only SARIF export** (`ptai export --sarif`) with a frozen, versioned export-properties contract.\n- **REST path-parameter injection** in the SQLi/XSS/SSTI fuzzers, so injection in `/rest/products/` style routes is caught, not just query parameters.\n- **Experimental CL.TE request-smuggling discovery probe**, oracle-gated.\n- **CI gate** (`--fail-on verified`) with a composite action, and a bundled `ptai demo`.\n\n### Changed\n\n- **Honest verdicts.** An oracle miss now reads as `candidate` (could not re-prove), never `refuted`; `refuted` is reserved for an oracle that can truly disprove a vulnerability.\n- **Impact-honest severity:** a bare out-of-band callback proves existence, not impact, so it is rated medium until impact is reproduced.\n\n### Verified\n\n- 100% precision with zero false positives on the honeypot benchmark, and field-validated against live OWASP Juice Shop: a real broken-object-level-authorization bug verified end to end with a replayable proof capsule.\n\n## [0.17.2] - 2026-06-09\n\nPatch release: tool-installer fixes from user feedback on issue #12 (`lukeswitz`).\n\n### Fixed\n\n- **No more duplicate installs.** Tool detection now also checks `~/go/bin`, `~/.cargo/bin`, and `~/.local/bin`, not just the bare `PATH`. Go- and cargo-installed tools were being seen as missing and reinstalled on every run because those dirs are commonly absent from `PATH`.\n- **Cargo and manual install methods now work.** `feroxbuster`, `findomain`, and `rustscan` (cargo) and `evil-winrm` (gem) previously fell through to \"Unsupported install method\" and never installed.\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "62159c627e7c6755d9e9141a4eb1f1c226c2ed67": {"t": 1785763255.7301934, "n": 69621, "h": "9f37ca3c7ea8b61c", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.17.2] - 2026-06-09\n\nPatch release: tool-installer fixes from user feedback on issue #12 (`lukeswitz`).\n\n### Fixed\n\n- **No more duplicate installs.** Tool detection now also checks `~/go/bin`, `~/.cargo/bin`, and `~/.local/bin`, not just the bare `PATH`. Go- and cargo-installed tools were being seen as missing and reinstalled on every run because those dirs are commonly absent from `PATH`.\n- **Cargo and manual install methods now work.** `feroxbuster`, `findomain`, and `rustscan` (cargo) and `evil-winrm` (gem) previously fell through to \"Unsupported install method\" and never installed.\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "df6eab8de5a8c3e4b5f980c50297ae12499254ca": {"t": 1785763257.5307019, "n": 69022, "h": "71f6b01ee58b126c", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.17.1] - 2026-05-27\n\nPatch release: v0.17.0's release-e2e matrix test was over-aggressive on first CI run. v0.17.0 was tagged but never reached PyPI because release-e2e blocked publish; this is the same 0.17.0 content + the matrix test made shippable.\n\n### Fixed\n\n- **Matrix CI test correctly handles both cells**. The deterministic cell uses `--no-llm` (legacy `AgentOrchestrator` path) which doesn't set `exit_reason`; the test now accepts `None` for that cell. The ollama cell uses agent-mode and may have a `+orchestrator_escalation` suffix on exit_reason from Change 1d; the test now strips the suffix before checking the allowlist.\n- **Matrix test caps `--agent-max-iter` at 3** for the ollama cell. qwen2.5-coder:7b on GitHub-hosted runners (no GPU) is slow; the per-iteration cost would otherwise blow past the per-test timeout. Most of the value comes from the orchestrator escalation kicking in anyway, so a low iteration cap is fine.\n- **Per-test timeout bumped to 1800 s** on the matrix test specifically (the file-wide 900 s default stays for the other lifecycle tests).\n\nNo production code changes - this is a test-only fix.\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "3c90fc81f17b0857288f5293ff9a45d3da85a7a2": {"t": 1785763259.2640045, "n": 67877, "h": "1a69ecaba3bb3175", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.17.0] - 2026-05-27\n\nCloses the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). The headline guarantee: **`ptai start` now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly**. Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings emerged via the safety-net escalation path, clean exit in 193 s.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop**. `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. After N consecutive non-VALID responses (default 3, env `PTAI_AGENT_FALLBACK_THRESHOLD`), the loop trips the deterministic fallback with `exit_reason=\"llm_non_cooperative\"`. Separate trigger for early-finish-with-zero-findings (`exit_reason=\"llm_finished_too_early\"`). Env knob: `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **Cooperative-but-unproductive safety net**. When the agent loop ends cleanly (max_iter or coverage) with zero findings AND `fallback_to_deterministic=True`, the in-loop fallback runs and `exit_reason` becomes `llm_unproductive`. Catches the case where the LLM emits valid JSON, picks real handlers, but the chosen actions don't find anything (qwen2.5-coder:7b picking `meta.set_auth` against a vulnerable target was the live example).\n- **Orchestrator escalation on zero findings**. The load-bearing buyer-grade fix: after the agent loop returns, if the engagement still has zero findings, `cli/main.py` escalates to the legacy `AgentOrchestrator` pipeline on the same engagement_id. The orchestrator does proper recon → discovery → probes-with-real-args → chain → validate, producing real findings. The user always gets findings when the target has them, regardless of LLM. `exit_reason` tagged `+orchestrator_escalation` for diagnosability.\n- **`ptai doctor` subcommand**. Prints resolved provider / model / endpoint, env-var surface (API keys masked), storage paths, and a live reachability + auth preflight. Exits 0 on success, 1 on failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI / compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud. Gated by `PTAI_FACTORY_VALIDATE=1`.\n- **Ollama in the factory's auto-detect chain**. When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` and picks the first pulled model from `/api/tags` (not the static `llama3.1` default). Closes `as8ASd3`'s scenario where only `OLLAMA_HOST` was set and the factory previously fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py`. `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field. `python scripts/diag_post_engagement_hang.py --exec \"ptai start ... --no-llm --no-oast\"` then `kill -USR1 ` dumps every thread's stack.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer`. Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Belt-and-braces backstop for future regressions; the root-cause fix is the LLM-client `close()` below.\n- **Startup version-check nag** at `cli/_version_check.py`. PyPI JSON lookup with 1 s hard timeout, 24 h TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (issue #12, poeylizn). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once requests had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback poeylizn posted. `cli/main.py:_run_engagement` now captures the client at function scope and the existing `finally` block calls `await llm_client.close()`. Live-verified clean exit in 24 s even with `PTAI_FORCE_EXIT_DISABLE=1` (proving the close is the actual fix, not the watchdog masking it).\n- **Silent zero-findings against vulnerable targets** (issue #12, poeylizn). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The `ResponseQuality`-driven fallback catches garbage / give-up cases, and the orchestrator escalation catches cooperative-but-unhelpful cases.\n- **Factory silently routing to Anthropic with no key when only `OLLAMA_HOST` is set** (issue #12, as8ASd3). Now the auto-detect path probes Ollama first; failing that, `validate()` at factory time surfaces \"ANTHROPIC_API_KEY not set\" with a concrete next-step block instead of swallowing the error mid-loop.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix**: `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if either cell hits `max_iterations` or returns an unexpected exit_reason; the Ollama cell additionally requires ≥1 finding. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`** so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "30fd0fdd6fa90d2f440d1f020227a5f08644c0a5": {"t": 1785763260.8428266, "n": 66364, "h": "5547a9b4fcd3e753", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\nv0.17.0 in flight. Closes the silent-exit + coverage gap that issue #12 surfaced across two users (`poeylizn` on DeepSeek-via-OpenAI-compat, `as8ASd3` on Ollama with no `PENTEST_AI_LLM_PROVIDER`). Five Changes, sequenced so each can be reverted independently.\n\n### Added\n\n- **Garbage + give-up LLM detection in the agent loop** (Change 1). `ResponseQuality` enum classifies each `_parse_action` outcome as `VALID` / `UNPARSEABLE` / `UNKNOWN_HANDLER`. The loop trips the existing `_run_deterministic_fallback` at `engine/agent_loop.py:319` after N consecutive non-VALID responses, OR when the agent emits `finish` below `min_iterations_before_finish` with zero findings. Two new `ExitReason` values surface the difference: `llm_non_cooperative` (garbage path) and `llm_finished_too_early` (give-up path). Env knobs: `PTAI_AGENT_FALLBACK_THRESHOLD` (default 3), `PTAI_MIN_ITERATIONS_BEFORE_FINISH` (default 5).\n- **`ptai doctor` subcommand** (Change 2). Prints resolved provider/model/endpoint, env-var surface (keys masked), storage paths, and a live preflight via the new `validate()` Protocol method. Exits 0 on success, 1 on auth failure with a concrete next-step block. Distinct from `cli/menu.py:_doctor` (install-audit shell wrapper).\n- **Provider-side `LLMClient.validate()`** for Anthropic (`GET /v1/models`), OpenAI/compat (`GET /models` with `/chat/completions` fallback on 404), Ollama (`GET /api/tags` + configured-model-pulled check with `ollama pull` hint), and LiteLLM (1-token completion). Called from a new `validate_client()` helper in `engine/llm/factory.py` with a hard timeout from `PTAI_FACTORY_VALIDATE_TIMEOUT_MS` (default 2000). Timeout = warn-and-continue, 401/403 = fail loud with the next-step block. Gated by `PTAI_FACTORY_VALIDATE=1` (default on) for one release.\n- **Ollama in the factory's auto-detect chain** (Change 2). When no cloud API key is set but `OLLAMA_HOST` (or default `localhost:11434`) responds to `/api/tags` within 500 ms, the factory resolves to `ollama` with the first available model. Closes `as8ASd3`'s report where setting only `OLLAMA_HOST` fell through to Anthropic with no key.\n- **Hang-investigation harness** at `scripts/diag_post_engagement_hang.py` (Change 4a). `SIGUSR1`-driven `faulthandler` wrapper for diagnosing post-engagement hangs in the field.\n- **Watchdog backstop** in `cli/main.py:_arm_force_exit_timer` (Change 4b). Daemon thread that sleeps for `PTAI_FORCE_EXIT_SECS` (default 5) then calls `os._exit(0)` after logging any still-alive non-daemon threads. Skippable via `PTAI_FORCE_EXIT_DISABLE=1`. Catches future regressions; the root-cause fix is the LLM-client close below.\n- **Startup version-check nag** at `cli/_version_check.py` (Change 5). PyPI JSON lookup with a 1s hard timeout, 24-hour TTL cache at `~/.pentest-ai/version-check.json`, daemon-thread worker so it never blocks startup. Skippable via `PTAI_SKIP_VERSION_CHECK=1`. Test-only `PTAI_VERSION_OVERRIDE` for manual triggers without rebuilding. Modelled on Claude Code's update-available banner.\n\n### Fixed\n\n- **Post-engagement hang requiring Ctrl+C** (Change 4b). The LLM client (`httpx.AsyncClient` inside each provider) was created at engagement start but never `aclose()`d. Once any request had gone through it, the connection pool's keepalive blocked `threading._shutdown` on `lock.acquire()` - exactly the traceback `poeylizn` posted. `cli/main.py:_run_engagement` now captures the client out at function scope and the existing `finally` block calls `await llm_client.close()` next to the established `db.close()` / `cache.close()` pattern.\n- **Silent zero-findings on non-Anthropic providers** (Change 1, root cause). DeepSeek and similar models emit free-form text or hallucinated handler names against ptai's strict-JSON action contract; `_parse_action` was falling through to `Action(name=\"finish\")` and the loop accepted it without warning. The new `ResponseQuality`-driven fallback runs the deterministic probe sweep instead, so the user gets real findings AND a NOTE in the post-engagement summary explaining the LLM was the bottleneck.\n\n### Changed\n\n- **CI release-gate `release-e2e` is now a 2-cell matrix** (Change 3): `ollama` (qwen2.5-coder:7b sidecar) and `deterministic` (`PTAI_NO_LLM=1`). The build can't go green if the ollama cell returns 0 findings or either cell hits `max_iterations`. The 4.7 GB model layer is cached via `actions/cache` keyed `ollama-models-qwen2.5-coder-7b-v1`. `timeout-minutes` up to 40 from 30 to absorb a first cold pull.\n- **`_ci_print` `engagement_complete` JSON payload now carries `exit_reason`**, so the matrix assertion can gate on it without parsing the human-readable summary.\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "327684976143c3f744c0fece4d28a7e784aa03c8": {"t": 1785763262.2682786, "n": 61673, "h": "1566d16e07a5a59a", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.4] - 2026-05-26\n\nRe-cuts the 0.16.3 LLM-provider-error-body fix on a clean commit. The original v0.16.3 tag pointed at a commit whose test job failed (existing Anthropic provider tests mocked `response` with bare `MagicMock`s that didn't carry a `status_code` int, so the new `response.status_code >= 400` check raised `TypeError` against the mock). 0.16.3 never published to PyPI. This release contains the same fix plus the test-fixture updates and the lint/typography cleanup that landed in between.\n\n### Fixed\n\n- **Anthropic provider test mocks now set `status_code = 200`** (`tests/test_misc_coverage.py`, `tests/test_providers_and_small_agents.py`). Followed from the `response.status_code >= 400` check added in v0.16.3; the test fixtures were comparing a `MagicMock` against an int.\n- **Ruff/lint cleanup across the codebase** (34 errors -> 0). Unused imports, sorting, suppressible-exception sites tagged with `# noqa: SIM105` where the broad catch is intentional in probe-edge code, two OSError suppressors converted to `contextlib.suppress`, one shadow-import bug in `mcp_server/server.py` renamed.\n- **AI-shaped typography removed from user-facing docs**. 74 em-dashes across CHANGELOG, README, `docs/llm-providers.md` replaced with hyphens; Unicode arrows and ellipses normalized in this session's additions.\n\n### Included (originally from 0.16.3, never reached PyPI)\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). New `LLMHTTPError` with endpoint URL, model name, status code, and the upstream's actual error message (truncated to 2 KB). Lets users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) see exactly why their endpoint rejected a request, instead of a one-line `httpx` error with no body.\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "b498cf9af5564dad975663518a0ab701b7391c3c": {"t": 1785763263.8847759, "n": 59816, "h": "67ca2506e187743b", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up - surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` - that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** - `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model -> `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model -> completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** - concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit - because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set -> anthropic, `OPENAI_API_KEY` set -> openai, neither set -> openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer ...`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes - `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` - delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred - `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 - blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** - async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) - parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction -> probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) - per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** - the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** - SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred - no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** - `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use - encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe -> mock-/register -> pending_oob -> mock-/poll-with-real-encryption -> poll_oob -> materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape - closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250-1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity - `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it - closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path - the recommended way to drive ptai per `docs/ARCHITECTURE.md` - is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) - MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety - skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% -> ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST->JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** - skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, ...). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** - when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** - refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime - only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** - POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** - new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** - parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only - authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** - bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them - minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** - two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` - returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` - audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** - new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** - orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** - 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** - helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking - not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 -> 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** - new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 -> 4/10 -> 15/20 -> 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** - 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** - `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile -> AuthCredentials -> captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 -> 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3-7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=...` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT ... WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching -> URL/path pattern -> file extension -> cloud service indicators -> AD/Kerberos keywords -> IP/CIDR/domain regex -> fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` - GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` - liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) - shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) - GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) - vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) - cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) - public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` -> `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 -> 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` - interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` - full CRUD\n- `pentest-ai auth profile import-from-flags` - one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` - replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` - sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "0450e630412828db8423604f700b74fa1e21f8c7": {"t": 1785763265.4228199, "n": 59780, "h": "9e97ffd2b774a717", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.3] - 2026-05-26\n\nIssue #12 follow-up — surface upstream LLM-provider errors so users with custom `OPENAI_BASE_URL` endpoints (DeepSeek cloud, Groq, Together AI, local vLLM, etc.) can actually diagnose 400 / 404 / 422 responses instead of seeing a one-line `httpx` error with no body.\n\n### Fixed\n\n- **HTTP errors from any LLM provider now include the response body** (`engine/llm/providers/openai.py`, `anthropic.py`, `ollama.py`). Before: `Client error '400 Bad Request' for url '...'` — that's the entire diagnostic. After: a new `LLMHTTPError` with the endpoint, model name, status code, AND the upstream's actual error message (truncated to 2 KB). Lets users see exactly why DeepSeek / Groq / their vLLM rejected the request. Same machinery covers 401 (wrong key), 404 (wrong model), 422 (incompatible field), and 5xx (upstream down).\n\n### Added\n\n- **Troubleshooting + log-viewing section in [`docs/llm-providers.md`](./docs/llm-providers.md)** — `PYTHONLOGLEVEL=DEBUG` for full request/response, common 4xx causes, and a one-line factory-config sanity check (`python -c \"from engine.llm.factory import _create_inner_client as c; ...\"`) that prints which provider + model + base URL the user's env resolves to without running a scan.\n\nEnd-to-end verified against an OpenAI-compatible non-OpenAI endpoint (Ollama's `/v1/`): wrong model → `LLMHTTPError: OpenAI-compatible endpoint at http://localhost:11434/v1 returned HTTP 404 for model='nonexistent': {\"error\":...}`. Correct model → completes normally.\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** — concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit — because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set → anthropic, `OPENAI_API_KEY` set → openai, neither set → openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer …`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes — `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` — delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred — `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** — async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) — per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** — SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** — `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "fdf6e30b9b2b11a9309bc88a6581e6e8c9d835e3": {"t": 1785763267.4328444, "n": 58264, "h": "30d50bd5bee7b630", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.2] - 2026-05-26\n\nIssue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded `gpt-4o` regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.\n\n### Fixed\n\n- **`PENTEST_AI_MODEL` env var now honoured across all four providers** (`engine/llm/factory.py`). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with `OPENAI_BASE_URL=https://api.deepseek.com/v1` and a DeepSeek key got a 404 because the factory still asked for `gpt-4o`. Now setting `PENTEST_AI_MODEL=deepseek-chat` (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.\n\n### Added\n\n- **[`docs/llm-providers.md`](./docs/llm-providers.md)** — concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (`gpt-4o` 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the `--no-llm` escape hatch. Linked from the README's Path 3 section.\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit — because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set → anthropic, `OPENAI_API_KEY` set → openai, neither set → openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer …`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes — `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` — delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred — `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** — async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) — per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** — SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** — `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "fc99d00e410aa4fdbb524940439252e34ed13295": {"t": 1785763269.277881, "n": 56840, "h": "a22e5bfb65e3fa82", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.1] - 2026-05-26\n\nPatch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical `OLLAMA_HOST` env var instead of `OLLAMA_BASE_URL`); that change was correct but solved an adjacent bug, not the one users were experiencing.\n\n### Fixed\n\n- **CLI agent-mode now actually honors `PENTEST_AI_LLM_PROVIDER`** (`cli/main.py`, `engine/agents/anthropic_agent.py`, `engine/llm/factory.py`). The previous `ptai start` flow hardcoded `AsyncAnthropic()` regardless of which provider the user configured. Users who set `OPENAI_API_KEY` or `OLLAMA_HOST` (or both) saw `agent_mode: NNN action handlers registered`, then a spinner, then a silent exit — because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean `finish` action. The CLI now constructs the LLM client via `engine.llm.factory.create_llm_client()`, which routes correctly through every supported provider:\n\n ```bash\n # OpenAI\n export OPENAI_API_KEY=sk-...\n ptai start http://target\n\n # Local Ollama (configurable via OLLAMA_HOST)\n export PENTEST_AI_LLM_PROVIDER=ollama\n ptai start http://target\n\n # No LLM, deterministic tools only\n ptai start http://target --no-llm\n ```\n\n Three sub-changes:\n - `cli/main.py` replaces `AsyncAnthropic()` with `create_llm_client()`.\n - `engine/agents/anthropic_agent.py` duck-types the client. If it exposes `.complete()` (the unified `LLMClient` from the factory), use that; otherwise fall back to the legacy `client.messages.create()` path so existing test fixtures keep working.\n - `engine/llm/factory.py` auto-detects provider from whichever API key is set when `PENTEST_AI_LLM_PROVIDER` isn't explicit. `ANTHROPIC_API_KEY` set → anthropic, `OPENAI_API_KEY` set → openai, neither set → openai default. Closes the \"I set OPENAI_API_KEY in good faith, why is it routing to Anthropic\" foot-gun.\n\n Verified end-to-end against a live local Ollama running `qwen2.5-coder:7b`: factory routes to `OllamaProvider`, the unified `LLMClient.complete()` round-trips successfully, `AnthropicAgent.decide_next_action` returns a real `Action` instead of the silent `finish`-on-failure. Closes the silent-exit class for the issue #12 reporters.\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer …`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes — `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` — delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred — `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** — async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) — per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** — SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** — `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "407647146e5bd42519f938b92a84d25a19ec7dfe": {"t": 1785763270.9855015, "n": 54545, "h": "4257847053532112", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.16.0] - 2026-05-26\n\nHeadline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. **Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and `intensity=stealth` actually changes traffic shape.** Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.\n\n39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, **8/8 carry `evidence_artifacts`**, 406/406 on-disk artifacts produce valid `curl` reproducers, SARIF webRequest/webResponse populated, REST `/v1/health` + `/v1/findings` return 200, CLI agent-mode parity confirmed.\n\n### Added: plugin-client surfaces (REST adapter + auth hardening)\n\nFoundation for the [pentest-ai-extensions](https://github.com/0xSteph/pentest-ai-extensions) repo's Caido / Burp / ZAP plugins.\n\n- **MCP auth: per-install token file + Host: allowlist** (`mcp_server/auth_local.py`). `~/.pentest-ai/mcp-token` (0600, fresh `secrets.token_urlsafe(32)` on first start) gates every SSE/HTTP request via `Authorization: Bearer …`. Host header allowlist (`127.0.0.1` / `localhost` / `::1` plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). `ptai mcp --no-auth` opts out on trusted hosts; `--token-file ` overrides the location.\n- **REST adapter at `/v1/*`** (`mcp_server/rest.py`). Four routes — `GET /v1/health`, `GET /v1/findings`, `POST /v1/http_request`, `GET /v1/evidence` — delegate to the existing `@mcp.tool()` functions via FastMCP's `custom_route()` decorator. Reuses `LocalAuthMiddleware` for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.\n- **`get_findings` gains `url=` + `since=` filters** (`engine/findings_db.py`, `mcp_server/server.py`). `url=` is a case-insensitive `LIKE` match against the `target` column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. `since=` lets the tab poll incrementally without re-downloading. Both default to `None` and combine cleanly with existing severity/status filters.\n- **`health()` MCP tool** (`mcp_server/server.py`). Liveness probe for plugin status indicators. Returns `{status, version, timestamp, uptime_seconds, active_engagements}` with zero side effects; never raises (degrades to `active_engagements=0` when the DB is unreachable).\n\n### Added: CLI agent-mode parity for evidence + proxy passthrough\n\n- **`ptai start` agent-mode path** (`engine/agents/handlers/registry_bridge.py`) now populates `session._ptai_extras` with `engagement_id` + an `EvidenceCollector` (rooted at `$PENTEST_EVIDENCE_DIR`) + an optional `proxy` from `PTAI_UPSTREAM_PROXY`, then drains the pending-evidence buffer onto every emitted finding via `_attach_pending_evidence_to_findings(session, findings)`. Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred — `WorkingMemory` doesn't carry intensity today; small refactor follows when needed.\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** — async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) — per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** — SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** — `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "00eff5fbf70cf20ec0826e5a020e5d1557164384": {"t": 1785763273.3034916, "n": 51224, "h": "1925fd71b1bf02ad", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: out-of-band collaborator (Interactsh / OAST) integration\n\nPhase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.\n\n- **`engine/oob/` package** — async Interactsh client. Generates RSA-2048 keypair per engagement, `POST /register` to the configured server, polls `GET /poll`, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against `github.com/projectdiscovery/interactsh`. Defaults to `https://oast.fun`, accepts any self-hosted server via `--oast-server`.\n- **`pending_oob` table** (`engine/findings_db.py`) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.\n- **Curated payload library** (`engine/oob/payloads.py`) — per-vuln-class payload templates with `{OAST}` placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + `${lower:}` bypass).\n- **`poll_oob` MCP tool** — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to `timeout=60` seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via `find_pending_oob_by_full_id`, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to `matched`. Stale rows past their `expires_at` get bulk-moved to `expired`.\n- **Probe wirings** — SSRF (`web.ssrf_cloud_metadata`), blind SQLi (`web.sqli_fuzz`), XXE (`web.xxe_upload`), stored XSS (`web.stored_xss`) all fire OAST payloads when `engagement_id` is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.\n- **CLI flags** — `ptai start --oast-server URL` / `--oast-token T` / `--no-oast`. Sets `PTAI_OAST_SERVER` / `PTAI_OAST_TOKEN` / `PTAI_NO_OAST=1` for the engagement.\n- **Privacy disclosure in README** under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.\n\nEnd-to-end verified by a test (`tests/test_oob_end_to_end.py`) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "cfc408e80f2f149cf4e8db96640f7f7a129761ac": {"t": 1785763275.3628588, "n": 48136, "h": "370690314a9f39d4", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: real `intensity=stealth` implementation\n\nThe `intensity=stealth` knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.\n\n- **Curated UA pool with per-call rotation** (`engine/probes/primitives.py`). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.\n- **Per-request jitter** (`engine/probes/primitives.py`). `asyncio.sleep` of `random.uniform(min_ms, max_ms) / 1000` before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.\n- **Upstream proxy passthrough** (`engine/probes/primitives.py` + `mcp_server/probes.py`). Every HTTP call honors `extras[\"proxy\"]`, populated from the `PTAI_UPSTREAM_PROXY` env var or the new `--upstream-proxy` CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — `ptai start http://target --upstream-proxy http://127.0.0.1:8080`.\n- **Stealth knobs wired into MCP run_probe** (`mcp_server/probes.py`). When the engagement intensity is `stealth`, run_probe populates session extras with `ua_rotation=True`, `jitter_ms=(250, 1500)`. http_request always honors `PTAI_UPSTREAM_PROXY`.\n\nActive on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "5319263b5370c5777145b8f6575e82a230015ba0": {"t": 1785763276.7835393, "n": 46608, "h": "cd7013167fdfcd99", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: per-finding evidence bundle\n\nThe headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the \"AI slop\" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.\n\n- **`evidence_artifacts` column on `findings` table** (`engine/findings_db.py`). JSON list of `{artifact_id, type, sha256, filename, method, url, status_code}` summaries pointing at the on-disk exchanges. Migration via the existing `_add_column_if_missing()` pattern with a `'[]'` default so old rows read as an empty list.\n- **HTTP capture in the primitives chokepoint** (`engine/probes/primitives.py`). Every `http_get` / `http_post_json` / `http_post_form` / `http_put_json` call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an `EvidenceCollector` is attached via `_ptai_extras`. Existing probes get evidence for free with zero probe-side changes.\n- **MCP `run_probe` collector wiring + orchestrator auto-attach** (`mcp_server/probes.py`). Constructs an `EvidenceCollector` rooted at `$PENTEST_EVIDENCE_DIR//`, attaches it plus `engagement_id` to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own `evidence_artifacts`.\n- **MCP `http_request` capture parity** (`mcp_server/probes.py`). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under `evidence_artifacts`.\n- **SARIF v2.1.0 DAST `webRequest` / `webResponse`** (`engine/sarif.py`). When a finding carries an `http_request` artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.\n- **`request_to_curl()` utility** (`engine/evidence.py`). POSIX-shell-safe curl one-liner builder; used by the report renderer and by `get_evidence(as_curl=True)`.\n- **Enhanced MCP `get_evidence`** (`mcp_server/server.py`). Adds `include_content` (returns raw bytes), `as_curl` (parses http_request artifacts and emits a copy-pasteable curl), and always returns the on-disk SHA-256 so callers can detect tampering by comparing against the hash stored in the finding's `evidence_artifacts` field.\n- **Per-finding \"Evidence bundle\" section in HTML / PDF reports** (`agents/report/templates/report.html.j2`, `agents/report/renderer.py`). Collapsible `
` block per finding listing each captured exchange's method/URL/status, SHA-256, and on-disk filename. The renderer tolerates both already-parsed lists and JSON-encoded strings from DB rows.\n\nSmoke verified against the local TaskFlow honeypot (port 4000): `web.api_path_discovery` emitted 8 findings, 8/8 carried `evidence_artifacts`, 405/405 on-disk artifacts produced valid curl repros via `get_evidence(as_curl=True)`. End-to-end integration test in `tests/test_evidence_integration_e2e.py` walks the same loop against a synthetic loopback target on every CI run.\n\nWiring still TODO for the standalone CLI agent-mode + legacy orchestrator paths (`cli/main.py`); the MCP path — the recommended way to drive ptai per `docs/ARCHITECTURE.md` — is fully wired.\n\n### Fixed\n\n- **Filename collision in `EvidenceCollector`** (`engine/evidence.py`). `_safe_timestamp()` had second-granularity, so concurrent probe HTTP calls within the same second collided on filename and overwrote each other's captured exchanges. Now includes 8 hex chars of `uuid4` entropy in the suffix. Surfaced by the new end-to-end test under `web.api_path_discovery` (~100 requests in <1 s).\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "91cbb6545f56ab08603a36fc51c240c15196e7b0": {"t": 1785763278.3834815, "n": 42858, "h": "10bc1711e8978a41", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Fixed\n\n- **Ollama provider now honors `OLLAMA_HOST`** (`engine/llm/factory.py`). The factory previously read only `OLLAMA_BASE_URL`, so users following the official Ollama docs (which use `OLLAMA_HOST` as the canonical env var) had their setting silently ignored, fell back to `http://localhost:11434`, and the agent loop hung on the 300 s HTTP timeout before exiting. Now reads `OLLAMA_HOST` first, then `OLLAMA_BASE_URL` as a back-compat alias. Closes #12.\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "3896923d087ff616fd68190735f2d18ad2cfb190": {"t": 1785763280.3498507, "n": 42377, "h": "93c8833b4864df75", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.15.3] - 2026-05-20\n\nDefault-budget bump and documentation. The standalone agent loop's spending cap defaults to **$10 USD** (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.\n\n### Changed\n\n- **`DEFAULT_PRICE_LIMIT_USD` raised from $2 to $10** (`engine/llm/cost.py`). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via `PTAI_PRICE_LIMIT=N`; `=0` is still unlimited; resume after a cap-abort with `ptai resume ` (engagement state is preserved at `aborted_cost_limit` status). Applies only to the standalone agent loop (`ptai start`) — MCP-driven engagements continue to use the AI client's own billing.\n\n### Docs\n\n- **New \"Spending cap (Path 3 only)\" section in README** under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "6ae8658419cd6a31127e09492cb7ca1fb794841e": {"t": 1785763283.420761, "n": 41254, "h": "4a3064acd29aa368", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.15.2] - 2026-05-18\n\nPre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes `http_request` and `run_probe` inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread `auth_profile` on every tool call.\n\n### Fixed\n\n- **MCP `http_request` / `run_probe` inherit engagement auth automatically** (`mcp_server/auth_cache.py`, `mcp_server/server.py`). A process-local cache keyed by `engagement_id` stores the auth captured during `start_engagement`; subsequent tool calls without an explicit `auth_profile` argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: `start_engagement(auth_profile=\"juice_shop\")` populates the cache, then `http_request(engagement_id=..., url=\"/rest/basket/1\")` returns the admin basket with no further auth wiring.\n\n### Added\n\n- **`ptai start --auth-profile ` accepts bearer-flow profiles** (`cli/main.py`). Previously the CLI rejected anything other than `form_post`; now `bearer_static` and `bearer_dynamic` both work and propagate the token through every specialist agent.\n- **`ptai reap --older-than [--force] [--dry-run]`** (`cli/main.py`, `engine/findings_db.py:FindingsDB.reap_engagements`). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless `--force` is passed.\n\n### Internal\n\n- **Post-publish PyPI smoke test** (`.github/workflows/release.yml`). New `smoke` job waits for the wheel to propagate, installs it in a fresh venv, verifies `ptai --version` matches the tag, imports key modules, and runs an MCP `initialize` handshake. Catches missing-file wheels and broken entry points before users hit them.\n- Test hygiene: silenced async-mock warnings in file-upload tests, registered the `pytest-timeout` marker.\n- Reverted a brief regression in the scorer's title-match branch (`73e0bd4` reverts `25d041c`) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "095cbafcbb3e64cabf9059270c49d70d46edb1e0": {"t": 1785763284.9927568, "n": 39153, "h": "895aaba101e9898c", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.15.1] - 2026-05-16\n\nPatch follow-up to 0.15.0 closing a silent auth failure on the headline `flow: bearer` profiles.\n\n### Fixed\n\n- **MCP bearer-flow auth profile resolution** (`mcp_server/auth.py`). Profiles declaring `flow: bearer` with `password_source: env` silently fell back to form-post: `resolve_auth_profile_to_dict` ignored `prof.flow` and returned a `form_post` dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop `/rest/user/login` and the login silently failed with `auth_profile '' could not be resolved or login failed`. Adds a `bearer_dynamic` branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "df04ec7b3c76d6f9d431f3df86e9c17fefe5ce54": {"t": 1785763286.6532075, "n": 38260, "h": "eefcc83fbcf7c289", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.15.0] - 2026-05-16\n\nBug-bounty / production-target readiness release. Four opt-in safety flags so ptai can be pointed at hardened or rate-limited targets without violating program rules, plus a real `bearer` authentication flow that unlocks JWT-style API targets (Juice Shop, crAPI, and the typical modern bug-bounty stack).\n\n### Added: opt-in safety flags\n\nAll three default `false` to preserve existing behavior. Pass on `start_engagement` (MCP) or via the corresponding CLI flag.\n\n- **`intensity=\"safe\"`** — skip destructive-low probes (mass-assignment user creation, coupon forging, stored XSS, race conditions, …). Catches read-only bug classes without mutating server state. Useful for first-pass production-target scans where you don't yet have write authorization.\n- **`respect_rate_limits=true`** — when a target returns HTTP 429, honor `Retry-After` (or fall back to exponential backoff capped at 30s, 3 retries). Recommended for WAF-protected bug-bounty targets; off by default to preserve today's scan-as-fast-as-the-target-allows behavior.\n- **`strict_scope=true`** — refuse any HTTP request whose host doesn't match the engagement target's host. Also disables redirect-following in primitives so a 302 to `attacker.com` can't pull the scan off-target. Bug-bounty programs care a lot about scope discipline; default off preserves today's wide-open behavior.\n\n### Added: `bearer` auth flow with JSON token extraction\n\nThe `bearer` flow was declared in `cli.auth_profiles.VALID_FLOWS` since v0.10 but raised `AuthError` at runtime — only `form_post` and `bearer_static` were actually implemented. The executor now exists.\n\n- **`engine/auth_session.py:WebAuthenticator._login_bearer`** — POSTs credentials (JSON or form body, configurable via `body_shape`) to a `login_url`, extracts a token from a configurable JSON response path (e.g. `authentication.token`), returns an `AuthSession` with `bearer_token` populated. The orchestrator's `set_auth()` propagates it through every specialist agent, so all 30+ auth-gated probes pick it up.\n- **`AuthProfile.body_shape` / `AuthProfile.token_path`** — new fields on the profile dataclass. Profile YAML schema gains the two keys; `from_dict` / `to_dict` updated.\n- **`tests/test_auth_profiles_smoke.py`** — parametrized \"paper-tiger\" guard: enumerates every flow in `VALID_FLOWS` and asserts each either has a real executor or correctly raises the `unsupported auth flow` error. Future declared-but-unimplemented flows fail loudly at test time instead of mid-engagement.\n\n### Generalization validation\n\nptai 0.15.0 ships with a measured authenticated catch rate against OWASP Juice Shop v19.2.1: **63.24% (43 of 68 in-scope challenges)** under aggressive intensity + `strict_scope=true`. Methodology and per-challenge breakdown in `benchmarks/scoring/` (local-only — authenticated benchmark outputs are gitignored).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "3131d5586e88f6c385c85a9b5e074c4f1ff9d635": {"t": 1785763288.3915455, "n": 35383, "h": "ace82f32811a6a1c", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.14.0] - 2026-05-13\n\nLLM-callable tool surface release. The 200+ `SecurityTool` wrappers in `tools/registry.py` are now reachable from both paths: the agent-loop (for `ptai start --agent-mode` standalone CLI) and the MCP server (for Claude Code / Cursor / Codex driving ptai). Companion smart-install flow lets the outer LLM batch-install missing tools with one prompt instead of interrupting the engagement repeatedly.\n\n### Added: agent-loop tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n\n### Added: MCP tool surface (no API key required)\n\n- **`mcp_server/security_tools.py`** — two new MCP tools usable from Claude Code / Cursor / Codex without an Anthropic API key:\n - `mcp__pentest-ai__plan_tools(scope, intensity, target)` — returns the canonical static tool list for an engagement profile.\n - `mcp__pentest-ai__ensure_tools_installed(tools, auto_install)` — audits which are installed; with `auto_install=True` runs the batched install.\n- These complement the long-standing `mcp__pentest-ai__list_tools` and `mcp__pentest-ai__run_tool` that ship since 0.10.0.\n\n### Added: install UX\n\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via `~/.pentest-ai/install-preferences.json::auto_install`.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "e6fe5512cbcc42f88c794183169d1ac889fc564f": {"t": 1785763290.8239307, "n": 34339, "h": "fbe76e3d0a70ef16", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: LLM-callable tool surface\n\n- **`engine/agents/handlers/tool_bridge.py`** — bridges every `SecurityTool` in `tools/registry.py` into the agent loop as a `tool.` handler. The LLM agent loop previously saw only probes; now 200+ wrappers (wpscan, dalfox, hydra, hashcat, paramspider, ffuf, gobuster, ...) are LLM-callable. Plus `tool.run` (generic dispatcher), `tool.list` (category-filtered discovery), `tool.plan` (predict tools for an engagement), and `tool.ensure_installed` (batched install with one prompt).\n- **Upfront install prompt in `cli/main.py` agent-mode branch.** At engagement start, the planner predicts a tool list from the (scope, intensity, target) profile, audits which are missing, and asks once to install them — minimizing mid-engagement popups. Decline persists to `~/.pentest-ai/install-preferences.json::denied_tools` for future engagements.\n- **`ptai setup --per-tool `** for ad-hoc single-tool installs (no need to pick a tier).\n- **Optional install metadata on `SecurityTool`**: `install_method`, `install_cmd`, `manual_url`. Defaults `None` so every existing wrapper compiles unchanged. The installer's resolution order is now (1) `engine/tool_installer.py::TOOL_CATALOG`, (2) optional registry fields, (3) \"manual install required\" hint surfaced to the LLM instead of crashing.\n- **Non-interactive safety:** `PTAI_NON_INTERACTIVE=1` and stdin-not-a-TTY both short-circuit prompts. Auto-install only fires when the user opts in via the preferences file.\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "c788b0744e25527897ebaddc0ab7c8addc9b83bc": {"t": 1785763292.4751375, "n": 32833, "h": "b6de58f5f92155aa", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Four-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, HexStrike v6.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP, Nuclei, HexStrike each 0 critical+high in 1 OWASP bucket.\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "b97f37c094d95f48f242189ecaf100967a05bc9a": {"t": 1785763294.205586, "n": 32822, "h": "80df258a02d49db9", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added: cross-tool benchmark\n\n- **Published Juice Shop benchmark v1** at `docs/benchmarks/juice-shop.md`. Three-tool matrix (ZAP 2.17.0, Nuclei 3.8.0, ptai 0.13.0) against OWASP Juice Shop v20.0.0 on `127.0.0.1:3001`, both unauthenticated and authenticated runs, 60-min cap per cell. Raw artifacts at `benchmarks/results/2026-05-12/juice-shop/`. ptai found 46 critical+high across 5 OWASP Top 10 buckets; ZAP 0 critical+high (1 bucket); Nuclei 0 critical+high (1 bucket).\n- **`tools/registry.py::parse_zap`** — new parser that normalises ZAP `core/alerts` JSON to ptai's `Finding` schema. Mirrors `parse_nuclei`.\n- **`scripts/benchmark_juice_shop.py`** — orchestrator for the deterministic-tool slice of the benchmark (ZAP, Nuclei, ptai-sweep). Resets target between cells, captures wall-clock and Juice Shop's `solved_challenges` side metric. Includes `PENTEST_AI_AUP_ACCEPTED=1` in env for non-interactive ptai runs.\n- **`scripts/triage_sample.py`** and **`scripts/apply_triage_labels.py`** — 20% random sampler (seed `20260512`) plus class-based labeller for FP-rate triage.\n- **`scripts/reset_juice_shop.sh`** — helper to stop/start/warmup a native juice-shop on `127.0.0.1:3001` between benchmark runs.\n\n### Removed: internal-only docs from public repo\n\n- Moved `docs/launch-playbook.md`, `docs/status-page-runbook.md`, and `docs/phase7.md` out of the public repo. These were internal go-to-market planning, SaaS ops runbooks, and internal phase tracking — not user-facing documentation.\n- Deleted `docs/index.md`. It duplicated README content and pointed at the marketing site for the real docs.\n\n### Changed: README\n\n- Humanizer pass on README and CHANGELOG. Removed mid-clause em-dashes, copula-avoidance constructions (\"lives in\" -> \"is in\"), and rule-of-three taglines. MCP tool count corrected 44 -> 47. Promotional phrasing in the \"vs the field\" section and the \"Who uses it for what\" section rewritten in concrete terms.\n\n## [0.13.0] - 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed: MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for (race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR) were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added: MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile. Returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added: safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved: probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity, one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] - 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added: new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added: primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added: bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added: optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed: probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace. Previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST. First echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored. Swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. Iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured. Iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed: decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] - 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added: SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added: CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added: Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added: earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] - 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] - 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] - 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added: 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added: 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] - 2026-04-27\n\n### Added: auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] - 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] - Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "c914d053734ec90596ecac6ed0fb3a48f26414e4": {"t": 1785763295.5539837, "n": 30894, "h": "4908152390f77fac", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.13.0] — 2026-05-11\n\nMCP launch-blocker release. Fixes a structural defect in the MCP server's auth wiring that capped catch rate at 6/10 on the in-process honeypot, and exposes ptai's probe registry as fine-grained MCP tools so an outer LLM (Claude Code, Cursor, etc) can drive engagements iteratively instead of fire-and-forget. Adds a hard-scoped `http_request` tool so the LLM can synthesize novel attack chains (stored SSTI, PATCH-based mass assignment) that no canned probe covers.\n\n**End-to-end honeypot catch rate via MCP went from 6/10 → 10/10 with the single auth-plumbing fix.**\n\n### Fixed — MCP auth plumbing (the launch blocker)\n\n- **`mcp_server.test_web_app` now calls `WebAgent.set_auth(creds)` before `agent.run_assessment(...)`.** The previous code path constructed `WebAgent(registry, db)` and passed `auth_credentials` straight to `run_assessment`, but the deterministic registry sweep reads `self._captured_auth` (not the dict argument). Result: every probe marked `requires_auth=True` skipped silently at `agents/web/web_agent.py:354`. Four probe families that the 0.12.0 release notes claimed coverage for — race conditions, mass assignment, type confusion, authenticated SQLi/XXE/IDOR — were unreachable through the MCP path. The fix logs in via `engine.auth_session.WebAuthenticator`, captures the full Set-Cookie response header (preserved on `AuthCredentials.set_cookie_raw`), converts to the captured-auth shape via `AuthCredentials.to_credentials()`, and calls `agent.set_auth(...)` before kicking off the scan. The orchestrator path was unaffected because `engine.orchestrator._configure_agent` already did this correctly.\n\n- **`mcp_server.start_engagement` gains an `auth_profile` parameter.** Previously the high-level start tool had no schema field for credentials, so the full pipeline could never reach auth-gated probes via MCP. Pass any profile from `pentest-ai auth profile add`; ptai resolves and logs in server-side. Credential values never enter the MCP/LLM request payload.\n\n- **`agents.web.web_agent._run_deterministic` accepts `auth_credentials` as a defense-in-depth fallback.** If a future caller passes credentials to `run_assessment` without going through `set_auth`, the deterministic path now bridges them onto `self._captured_auth` instead of dropping the credentials silently.\n\n### Added — MCP action surface for iterative driving\n\nThree new MCP tools turn ptai into a true action surface for an outer LLM, rather than a fire-and-forget pipeline. Claude Code can now drive engagements probe-by-probe, reading findings between calls and steering the next action.\n\n- **`list_probes(bug_class=\"\", requires_auth_only=False)`**. Enumerates every registered web probe (60 in 0.13.0) with `name`, `bug_class`, `severity_max`, `requires_auth`, `runtime_budget_s`, `owasp`, `tags`. Filters let the LLM zero in on relevant bug classes for the current engagement context.\n\n- **`run_probe(probe_name, target, engagement_id=\"\", auth_profile=\"\", candidate_endpoints=[], js_urls=[])`**. Dispatches a single probe synchronously. Findings persist to the engagement when `engagement_id` is set. Refuses to silently skip `requires_auth=True` probes without an auth_profile — returns a clear error instead, so the LLM knows what's missing. `candidate_endpoints` lets the LLM seed paths discovered in prior turns into probes that consume them (`sqli_fuzz`, `mass_assignment`, `idor_sequential`).\n\n- **`http_request(method, url, engagement_id, headers={}, body=\"\", json_body=None, auth_profile=\"\", allow_destructive=False)`**. Raw-HTTP escape hatch for attack patterns no canned probe covers: stored SSTI chains (POST `{{7*7}}`, then GET to verify), novel PATCH-based privilege escalation on custom endpoints, race-condition orchestration. **Hard guardrails enforced server-side:**\n - URL host:port MUST equal the engagement target host:port. Off-scope requests return `out_of_scope` and never fire.\n - `DELETE`, `PURGE`, `TRACE` require `allow_destructive=True` explicitly.\n - Request body capped at 64 KB; response body truncated to 4 KB (full length reported).\n - Optional `auth_profile` resolves to attached Cookie/Bearer headers.\n\n### Added — safety hardening\n\n- **`PTAI_PRICE_LIMIT` default changed from `0` (unlimited) to `$2`** in `engine/llm/cost.py`. Previously a fresh user's first LLM-driven engagement could run unbounded. Explicit `PTAI_PRICE_LIMIT=0` still means unlimited but logs a loud warning. Invalid values fall back to the safe default.\n\n- **Scope context plumbing in `engine/agent_loop.py`.** New `LoopConfig.scope` field and `engine.scope._current_scope` contextvar so future handlers (and `meta.http_request` in the agent_loop path) can hard-block off-scope URLs without threading the `ScopeEnforcer` through every handler signature.\n\n### Improved — probe coverage\n\n- **`web.idor_sequential` now retries authenticated when an unauthenticated walk gets uniform 401/403s.** Many APIs require any logged-in user to access `/api/users/` but don't gate by ownership; the unauth probe was bailing on `401` and missing the cross-user data leak. With `ctx.captured_auth` set, the probe replays the same ID walk with auth headers and flags divergent identities as \"Authenticated IDOR\" (high severity — one tier below canonical zero-auth IDOR).\n\n### Tests\n\n- **`tests/honeypot/test_mcp_honeypot_e2e.py`** — new end-to-end test boots the in-process honeypot, drives it through `mcp_server.test_web_app` via the FastMCP client, and scores findings against the 10-vuln planted matrix. Hardcoded threshold ≥10/10 so any future drop in the auth wiring or registry sweep fails CI loudly. This is the test that would have caught the 6/10 → 4/10 → 15/20 → 10/20 regression history.\n\n- **`tests/test_mcp_action_surface.py`** — 11 new tests covering `list_probes`, `run_probe`, and `http_request`: enumeration, filtering, unknown-probe errors, target validation, requires_auth gating, scope guard, destructive-method guard, unsupported-method guard, happy-path round-trip against an aiohttp echo server on a free localhost port.\n\n- **`tests/honeypot/server.py`** — `/login` now accepts both JSON and form-encoded bodies and emits a session cookie alongside the JSON token, matching real-world Flask/Rails endpoints. `_user_from_request` falls back to the session cookie when no `Authorization` header is present so cookie-based auth flows reach protected endpoints.\n\n### Internal\n\n- `mcp_server.server` gained three private helpers: `_resolve_auth_profile_to_dict`, `_resolve_auth_to_credentials`, `_credentials_to_captured_auth`. These centralize the auth-profile → AuthCredentials → captured_auth conversion so the MCP tools don't duplicate the bridge logic.\n\n## [0.12.0] — 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added — new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added — primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added — bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added — optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed — probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace — previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST — first echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored — swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. — iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured — iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed — decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] — 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added — SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added — CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added — Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added — earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] — 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] — 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] — 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added — 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added — 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] — 2026-04-27\n\n### Added — auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] — 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] — Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "35ff0d3a71b8b293fe85ef03f35315cc2096ea5d": {"t": 1785763297.4183476, "n": 24177, "h": "d8ad2e5917751ee2", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.12.0] — 2026-05-11\n\nHoneypot catch-rate release. Six existing probes hardened, three new probes added, one new HTTP primitive. End-to-end against a 20-vuln self-hosted Flask honeypot the catch rate moved from 9/20 (45%) to 20/20 (100%) inside a single deterministic three-pass scan.\n\n### Added — new probes\n\n- **`web.sqli_login_bypass`** (`engine/probes/web/sqli_login_bypass.py`). Classic `' OR 1=1 --`-family auth bypass against POST login endpoints. Tries 6 payloads × 4 username field names × 2 body shapes (form-urlencoded + JSON) across 8 common login paths. Detects bypass via three signals on the attack response that the wrong-password baseline didn't show: a new session-shaped `Set-Cookie`, a 3xx redirect to a non-login path, or a status flip (401/403 → 200) with logged-in markers in the body. Critical severity.\n\n- **`web.path_traversal`** (`engine/probes/web/path_traversal.py`). Filename-style query-param LFI. 13 payload depths covering raw `../etc/passwd` at depths 3–7, URL-encoded `%2e%2e/` variants, and Windows `..\\..\\..\\windows\\win.ini`. 6 param names (`name`, `file`, `filename`, `path`, `doc`, `page`) across 12 default paths plus `ctx.candidate_endpoints`. Detects via `root:x:0:`, `[boot loader]`, `[Mail]` body markers. Critical.\n\n- **`web.response_headers`** (`engine/probes/web/response_headers.py`). Single GET on the base URL captures three otherwise-uncovered finding classes: missing security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, plus HSTS on HTTPS); insecure session cookie attributes (Set-Cookie minting a session-shaped name without HttpOnly / Secure / SameSite); Server / X-Powered-By information disclosure (versioned or app-named values). Severities: low / medium / info respectively.\n\n### Added — primitives\n\n- **`http_post_form`** (`engine/probes/primitives.py`). `application/x-www-form-urlencoded` POST helper with the same return shape as `http_post_json`. `allow_redirects` defaults to True because form-POST endpoints commonly 302 to a render route. Used by `stored_xss` and `sqli_login_bypass` to attack Flask/Rails-style form handlers without re-rolling Content-Type each time.\n\n### Added — bug classes\n\nThree new entries in `engine.working_memory.BUG_CLASSES`: `security_misconfig`, `insecure_cookie`, `info_disclosure`.\n\n### Added — optional captured_auth field\n\n`captured_auth[\"set_cookie_raw\"]` now carries the full `Set-Cookie` header from the login response so probes (specifically `response_headers`) can inspect the cookie attributes that only ever appear on the login response itself. Apps that mint session cookies only on successful login (Flask sessions, Rails) get cookie-flag findings via this channel.\n\n### Fixed — probe gaps that were silently no-oping\n\n- **`web.api_path_discovery`** (`engine/probes/web/api_path_discovery.py`). Predicate now accepts HTTP 400/405/422 as \"endpoint exists, wants different args/method\" in addition to 200/401/403. Real endpoints frequently respond 400 to a bare GET when they expect a payload (TaskFlow's `/webhook` requires `callback`; `/api/import` is POST-only and 405s). Random missing paths still 404, so false-positive risk is minimal. This unblocks SSRF / XXE / path-traversal probes that depend on `ctx.candidate_endpoints` for seeding.\n\n- **`web.xxe_upload`** (`engine/probes/web/xxe_upload.py`). Now sends BOTH `multipart/form-data` (Juice-Shop-style upload) AND raw `application/xml` (modern REST endpoints that read `request.get_data()` and feed it straight to an XML parser) per candidate path. Propagates `ctx.captured_auth` via `auth_headers()` so cookie/bearer-gated import endpoints get reached before the 401 short-circuit. Candidates are the UNION of `ALWAYS_PROBED` and `ctx.candidate_endpoints`, not OR/replace — previously a single discovery hit blanked the Juice-Shop defaults.\n\n- **`web.sqli_fuzz`** (`engine/probes/web/sqli_fuzz.py`). Forwards `captured_auth` into both the crawler (so authenticated routes get crawled) and `fuzz()` (so attack requests actually authenticate). `_candidate_query_endpoints` expands `ctx.candidate_endpoints` into `{path}?{param}={payload}` templates so paths the discovery probe surfaced get fuzzed against every common query param. `ALWAYS_PROBED_TEMPLATES` extended with `/api/search`, `/api/lookup`, `/api/find`.\n\n- **`web.stored_xss`** (`engine/probes/web/stored_xss.py`). Now tries `(json, form)` shapes per `(path, field-name)` with read-back after every accepted POST — first echo wins. `captured_auth` flows through both POST and GET. Candidates union `DEFAULT_CANDIDATES` with `ctx.candidate_endpoints`; defaults extended with `/tasks/1..3`, `/posts/1`, `/topics/1` for same-URL POST+GET sinks scanners commonly miss.\n\n- **`web.race_condition`** (`engine/probes/web/race_condition.py`). Three independent fixes: (1) local `_auth_headers` helper only emitted Bearer headers; cookie-kind `captured_auth` produced `Authorization: Bearer session=…` which the server ignored — swapped to canonical `engine.probes.auth.runner.auth_headers`. (2) Baseline POST was awaited before the burst; on apps with a real check-then-sleep-then-update gate the baseline committed its UPDATE before any racer arrived. Fold baseline into the same `asyncio.gather` as the racers (16 concurrent). (3) Hardcoded `WELCOME10` coupon code matched the in-process test honeypot but skipped real apps that seed `SAVE10`/`SAVE25`/`DISCOUNT10`/etc. — iterate `COMMON_COUPON_CODES`, each candidate gets its own burst (the burst itself validates the code AND tests the gate atomicity).\n\n- **`probe_jwt_alg_none`** (`agents/web/spa_probes.py`). Iterates `COMMON_ADMIN_SUBJECTS` (`admin`, `administrator`, `root`, `sysadmin`, `superuser`, `ptai-canary`) in the forged token. Apps that DB-look-up the `sub` claim (Flask `SELECT … WHERE username = ?`) reject the fictitious canary with 401 even when `alg=none` is otherwise honoured — iterating surfaces the bug on real apps without relying on a guessable canary value.\n\n### Fixed — decorator misattachment regression\n\n`web.race_condition` and `web.stored_xss` had `@registry.probe(...)` accidentally attached to internal helper functions (`_race_burst`, `_post_payload`) after a refactor inserted those helpers between the decorator and the probe entry point. Unit tests still passed because they import the probe function directly, bypassing the registry. The break surfaced only under the three-pass orchestrator. Fixed by moving decorators to the right function and adding `spec.fn.__name__ == \"probe_\"` regression asserts to both registration tests.\n\n### Tests\n\nIn-process aiohttp honeypot in `tests/honeypot/server.py` grew five new vulnerable sinks mirroring TaskFlow: `/api/import` (lxml `resolve_entities=True`), `/api/search` (real SQLite with f-string SELECT), `/tasks/{id}` (unescaped HTML render with form+JSON POST), `/auth/login` (string-interp SELECT with cookie-minting bypass), `/files?name=` (`pathlib` UPLOAD_DIR with no anchoring). Five new e2e tests, six new unit-test files. Discovery + injection sweep grew from 40 passing to 174 passing.\n\n## [0.11.0] — 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added — SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added — CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added — Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added — earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] — 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] — 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] — 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added — 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added — 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] — 2026-04-27\n\n### Added — auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] — 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] — Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "c3bc9175e4d7ad7d04004202f900590da799af22": {"t": 1785763299.8009806, "n": 17071, "h": "9eaad2ef776ae0ee", "c": "# Changelog\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.11.0] — 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added — SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added — CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added — Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added — earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\n## [0.10.5] — 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] — 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] — 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added — 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added — 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] — 2026-04-27\n\n### Added — auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] — 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] — Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "1d0cc8e16d004c8cc0effa8e6809cba03f32b2eb": {"t": 1785763305.2483587, "n": 17072, "h": "5e1f79aea67084cf", "c": "# Changelog\n\n\n## [0.11.0] — 2026-05-06\n\nMajor release. New offensive-security probes, CVE intelligence, agent-loop scaffold, plus the launch-prep work staged on May 2.\n\n### Added — SPA-aware probes (14 deterministic checks)\n\n- **JWT alg=none acceptance** (`agents/web/spa_probes.py:probe_jwt_alg_none`). Two-step: anonymous GET, then GET with an unsigned JWT. If the second succeeds, the validator is honouring `alg=none`. On Juice Shop 19.2.1 this catches 8 separate critical bypasses across `/api/Users`, `/api/SecurityAnswers`, `/api/PrivacyRequests`, `/api/BasketItems`, `/api/Complaints`, `/rest/admin/application-configuration`, `/rest/user/whoami`, `/api/Challenges`.\n\n- **JWT alg variants** (`probe_jwt_alg_variants`). Tests case + whitespace + array + missing-alg variants. Reports only on endpoints where canonical lowercase `'none'` was rejected, so the count is not inflated.\n\n- **SQLi auth bypass on login** (`probe_login_sqli_bypass`). POSTs a tautology payload in the email field. Detects the canonical Sequelize-bug shape that returns 200 + a token-shaped response.\n\n- **GET-method SQLi on search** (`probe_search_sqli_get`). Two-step response-size diff: baseline `?q=apple` vs UNION payload. 3.0x size ratio with HTTP 200 = critical.\n\n- **Path-filter bypass via poison null byte** (`probe_ftp_leak`). Two-stage: direct GET to `/ftp/` vs URL-encoded NUL bypass `/ftp/%2500.md`. Catches the canonical Express `serve-index` filter defect. Pulls dependency manifests (`package.json.bak`) and other backup files.\n\n- **Authenticated pivot** (`probe_authenticated_pivot`). Captures an admin JWT from the SQLi login bypass, then re-tests every 401-anon endpoint with the captured token. The autonomous-agent move: a finding becomes the input to the next probe. On Juice Shop unlocks 9 protected endpoints.\n\n- **Open redirect** (`probe_open_redirect`). 10 redirect paths × 12 parameter names. Sentinel host with substring match on the `Location` header.\n\n- **REST resource enumeration** (`probe_user_enum`, `probe_unauth_rest_leak`). User-shape detection plus generic 200+JSON-without-auth detection across 19 Sequelize/Express scaffold paths.\n\n- **Source-map disclosure**, **directory listing**, **dev/admin interface leak** (probes 4 to 6).\n\n### Added — CVE intelligence\n\n- **`engine/cve_db.py`**: osv.dev batch API wrapper. Parses npm `package.json` and `package-lock.json` (v1, v2, v3 lockfiles) into `(name, version)` tuples and looks them up against the live osv.dev advisory database. When a leaked manifest comes through `probe_ftp_leak`, the engine auto-correlates and emits per-package `Vulnerable dependency` findings tagged with their GHSA identifiers.\n\n### Added — Agent-loop foundation (v0.11 autonomous-agent rebuild, phase 1 + 2)\n\n- **`engine/working_memory.py`**: per-engagement state object. Coverage matrix tracks `(endpoint, bug-class)` pairs the loop has tried. Captured-auth store for in-memory tokens. Pattern export refuses without explicit `cross_engagement_share` opt-in. Privacy-by-design enforced by tests.\n\n- **`engine/agent_loop.py`**: Action / Observation / LLMAgent protocol, handler registry, `run_agent_loop` driver. LoopConfig pins safety: max_iterations, coverage_threshold, min_iterations_before_finish.\n\n- **`engine/agents/anthropic_agent.py`**: LLMAgent backed by the Anthropic Messages API. Renders state + handler list, parses Claude's JSON action response, falls back to `finish` on malformed output.\n\n- **`engine/agents/handlers/web_probes.py`**: three SPA probes wrapped as agent-loop handlers (`probe.jwt_alg_none`, `probe.sqli_login`, `probe.ftp_leak`). Imported on agent-mode dispatch, registers automatically.\n\n- **`ptai start --agent-mode`** (`cli/main.py`). New flag routes the engagement through the agent loop instead of the legacy fixed pipeline. Default behaviour unchanged.\n\n### Added — earlier launch-prep work (May 2 staging)\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n- **Chain noise filter.** The exploit chainer used to fabricate chains from any list of findings, including nikto error strings. Fixed: `agents/exploit_chain/chain_agent.py` now drops scanner-noise findings before chain generation and refuses to fire on fewer than 2 medium-or-higher findings.\n\n- **Evidence-gated chain validation.** `agents/poc_validator/poc_agent.py` now updates `findings.poc_status` and `attack_chains.status` based on whether each chain step has corroborating evidence on the finding row.\n\n### Changed\n\n- **`--no-llm` is now an explicit opt-in.** `ptai start` used to silently fall back to deterministic mode when no LLM provider was configured. Now non-interactive runs without `--no-llm` AND without an LLM provider exit with code 3 and a clear message. ptai is LLM-driven by default; the deterministic path is opt-in.\n\n- **Tagline corrected** on `cli/main.py`, `docs/index.html`, `docs/index.md`. The previous \"Most autonomous pentesting AI on the market\" claim was not defensible against CAI's HackTheBox CTF wins or HexStrike's adoption numbers. Replaced with concrete capability copy.\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.10.5] — 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] — 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] — 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added — 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added — 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] — Unreleased\n\n### Added — auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] — 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] — Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}, "c37c0da35eb52374ef01c6f80c059a7cd26cda55": {"t": 1785763306.7835603, "n": 12220, "h": "d8a286f4e4210c53", "c": "# Changelog\n\n\n## [0.11.0] — 2026-05-02\n\nThree new runtime capabilities, one security fix, and a public legal documentation suite for launch.\n\n### Added\n\n- **`PTAI_PRICE_LIMIT` hard budget cap** (`engine/llm/cost.py`, `engine/orchestrator.py`). Set `PTAI_PRICE_LIMIT=5.00` (or any float in USD) and the orchestrator raises `CostLimitError` mid-run as soon as accumulated LLM spend crosses the threshold. The cap is enforced inside `CostTracker.add()` so it fires even on cached responses. If no limit is set the engine runs uncapped as before.\n\n- **Process registry with `ptai ps` and `ptai kill`** (`engine/process_registry.py`, `cli/main.py`, `mcp_server/server.py`). Every tool process spawned through `ToolRunner` is registered in an in-memory table keyed on PID with command, start time, and engagement context. `ptai ps` prints a formatted table. `ptai kill ` terminates the process and removes it from the registry. Also exposed as `list_processes` and `kill_process` MCP tools so MCP clients can manage long-running scans from the tool-use layer.\n\n- **`SelectionAgent` heuristic router** (`agents/selection/selection_agent.py`, `cli/main.py`, `mcp_server/server.py`). Routes a target string to the best-fit agent without an LLM call. Heuristics run in order: intent keyword matching → URL/path pattern → file extension → cloud service indicators → AD/Kerberos keywords → IP/CIDR/domain regex → fallback to WebAgent. Returns `{\"target\", \"agent\", \"reason\", \"confidence\"}`. Exposed as `ptai route [--intent ] [--json]` and as the `select_agent` MCP tool.\n\n- **Public legal documentation suite** (`docs/legal/`). New or strengthened markdown that mirrors the live pages at `pentestai.xyz/{privacy,terms,aup,cookies,subprocessors,security}`:\n - `PRIVACY.md` — GDPR Article 6 legal bases, 72-hour breach notification SLA, CCPA/CPRA disclosures, 2021 EU SCCs Module 2 with UK Addendum and Swiss FDPIC supplements.\n - `TERMS.md` — liability cap, indemnification, dispute resolution options, sanctions and export controls, beta features clause.\n - `AUP.md` (new) — shifts authorization-to-test responsibility to the user; defines prohibited targets and conduct; abuse-handling SLA and indemnification position.\n - `DPA.md` (new) — GDPR Article 28 template with Annex I, II, III.\n - `SUBPROCESSORS.md` (new) — vendor table with 30-day change-notice mechanism.\n - `COOKIES.md` (new) — cookie inventory, banner consent flow, and GPC signal compliance.\n - `RESPONSIBLE_DISCLOSURE.md` (new) — public security policy with safe harbor.\n\n### Security\n\n- **`ProcessRegistry.kill()` refuses unregistered PIDs.** The original implementation fell through to `os.kill(pid, signal.SIGTERM)` for any live PID, even one not spawned by ptai. An attacker who could write a valid integer to the registry endpoint could have sent SIGTERM to arbitrary processes. Fixed by returning `False` immediately when `registry.get(pid)` is `None`. Test `test_kill_refuses_unregistered_live_pid` spawns a real `sleep 30` without registering it and asserts both that `kill()` returns `False` and the process is still alive after the call.\n\n### Changed\n\n- `CostLimitExceeded` → `CostLimitError` to comply with `ruff` rule N818 (exception class names must end in `Error`). Renamed across `engine/llm/cost.py`, `engine/orchestrator.py`, and `tests/test_cost_tracker.py`. Any code that caught `CostLimitExceeded` by name must be updated to `CostLimitError`.\n\nAll notable changes to ptai are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).\n\n## [0.10.5] — 2026-04-29\n\n### Security\n\nPre-launch security sweep with bandit, pip-audit, gitleaks, and ruff S-codes. Three real fixes landed:\n\n- **`engine/tool_installer.py`**: `install_tool` and `install_tier` previously ran `subprocess.run(f\"echo {sudo_password} | sudo -S {' '.join(tool.install_cmd)}\", shell=True, ...)`. CWE-78 command-injection if a sudo password ever contained a shell metacharacter. Switched to argv form with the password piped via stdin.\n- **`cli/menu.py`**: `os.system(candidate + \" --quiet\")` replaced with `subprocess.run([candidate, \"--quiet\"], check=False)`. The candidate is currently a fixed path; this is defense in depth for future contributors.\n- **`engine/sarif.py`**: tool version was hardcoded `\"0.8.0\"` in every generated SARIF, lying to GitHub Code Scanning about which ptai produced the report. Now resolved dynamically via `importlib.metadata`.\n\nThe remaining 5 bandit B501 findings (`verify=False` on httpx.AsyncClient inside built-in scanners) are intentional: built-in scanners scan targets which may have invalid/self-signed certs, and cert validity is part of what we report on. Each is now annotated with `# nosec B501` plus a comment explaining why.\n\n`.gitleaks.toml` allowlists `tests/` and `engine/scanners.py` so the JWT regex test fixtures don't fail a launch-blocking CI scan.\n\n### Verified\n\n- `bandit -r ... --severity-level high`: 0 HIGH (was 8)\n- `pip-audit`: 0 CVE-vulnerable deps\n- `gitleaks detect`: 0 real leaks across 94 commits\n- 658 tests pass\n\n## [0.10.4] — 2026-04-29\n\n### Performance\n\n- **Deterministic mode is fast again.** WebAgent, ReconAgent, ADAgent, APISecurityAgent, and WirelessAgent all ran tools sequentially within each phase, which made `test_web_app` against a real target take >20s and a full deterministic engagement push past 5 minutes. Tools within a phase now run concurrently with a per-tool 30s ceiling. Verified by `tests/test_agents_parallel.py`: 5 mock tools that each sleep 0.5s now complete in ~0.5s wall-clock (was 2.5s).\n\n## [0.10.3] — 2026-04-29\n\n### Fixed (E2E test sweep, 16 bugs)\n\n- **HTML and PDF reports broken on PyPI install.** `agents/report/templates/report.html.j2`, `agents/llm_redteam/corpus.yaml`, `playbooks/builtin/*.yaml`, and `config/pentest-ai.yaml` were not packaged in the wheel. Added `[tool.setuptools] include-package-data` and explicit `package-data` entries. PyPI users can now generate HTML and PDF reports.\n- **Hard exit on missing LLM key.** `ptai start` used to exit with error if no API key was set, breaking the flagship Claude Code MCP usage path (where Claude Code provides the AI). Replaced with a friendly first-run prompt that lets users choose Anthropic key, OpenAI key, Ollama, or skip (for MCP / deterministic mode). Choice persists to `~/.pentest-ai/config.yaml`.\n- **Silent zero-finding results when LLM is unreachable.** `BaseAgent.think()` swallowed exceptions on the first call, returning an empty response. Tool loop exited immediately with zero findings. Now raises `LLMUnavailableError`; agents catch and dispatch to their own deterministic methods.\n- **No deterministic fallback in `api_security`, `credential_tester`, `vuln_scanner`, `privesc` agents.** Added tool-driven `_run_deterministic_*` methods to each.\n- **Input validation added to `ptai start`:** rejects `--scope` outside `recon|web|ad|cloud|mobile|full`, `--intensity` outside `stealth|normal|aggressive`, and targets containing shell metacharacters or oversized inputs.\n- **MCP `run_tool` accepts shell metacharacters in `target`.** Now rejects with a clear error before invoking the tool.\n- **Built-in HTTP scanners hardcoded `https://` for bare hostnames.** `_resolve_http_url` helper probes https, falls back to http, returns a clear \"target unreachable\" finding if both fail.\n- **Phase failures hidden in engagement summary.** CLI now surfaces `phase_errors` from the orchestrator. The summary panel turns yellow with \"Engagement complete with phase failures\" when any phase failed; CI mode emits `engagement_partial`.\n- **`set_intensity` for nonexistent engagement returned `applied_live=False` instead of an error.** Now checks the engagement exists first and returns a clear error.\n- **Attack chain discovery wove info-level meta findings into \"Web to Shell\" chains.** Added severity gates: only medium+ findings can seed an entry, only low+ can extend. `_build_severity_chain` filters info entirely.\n- **`query_compliance` returned empty controls for real findings.** Extended `CATEGORY_COMPLIANCE` to map `vulnerability`, `discovery`, `injection`, `xss`, `ssrf`, `authz`, `authorization`, `secrets`, `headers`, `ssl` (the categories findings actually use). Added OWASP control mappings to all entries.\n- **Recon could produce 27,680 findings against popular targets.** `subfinder` against `example.com` harvested cert-transparency log noise unbounded. Added per-phase finding caps (subdomain_enum=200, port_scan=500, etc.). When a cap is hit, an info-severity note is recorded so the user knows enumeration was bounded.\n- **Engagements stuck at `status='running'` forever after ptai crashed.** Added `FindingsDB.reconcile_stale_engagements`: anything `running` older than 30 minutes flips to `interrupted`. Called at the top of every `ptai start`.\n\n### Added — 8 new MCP tools\n\nSpecialist agents that previously had no MCP exposure are now reachable via Claude Code:\n`test_api_security`, `test_credentials`, `test_vulnerabilities`, `test_privesc`, `test_mobile`, `test_wireless`, `test_social_engineering`, `browser_inspect`. Total MCP tools: 33 → 41.\n\n### Added — 4 new specialist agent classes\n\n`agents/api_security/`, `agents/credential_tester/`, `agents/vuln_scanner/`, `agents/privesc/`. Each follows the BaseAgent pattern with both LLM-driven and deterministic modes. Marketing claim of \"12 specialist agents\" is now backed by real Python implementations.\n\n## [0.10.2] — Unreleased\n\n### Added — auth profile system\n\nA complete redesign of how ptai handles target credentials. See [docs/credentialed-scans.md](docs/credentialed-scans.md).\n\n- `pentest-ai auth profile add ` — interactive wizard to create an auth profile\n- `pentest-ai auth profile list / show / remove / use / current` — full CRUD\n- `pentest-ai auth profile import-from-flags` — one-line migration from legacy `--login-*` flags\n- `--auth-profile ` flag on `ptai start` — replaces 6+ legacy flags\n- `pentest-ai chain prof1=target1 prof2=target2 ...` — sequential multi-target scans, each with its own auth context\n- Playbooks accept `auth_profile: ` key\n- Four credential sources: env vars, 1Password CLI (`op://`), HashiCorp Vault, AWS Secrets Manager\n\n### Security\n\n- **MCP credential safety:** `authenticated_scan`, `test_web_app`, `test_active_directory`, `test_cloud` now accept `auth_profile` parameter. Credentials are resolved server-side and never enter the LLM context window. Previously, raw passwords flowed through the MCP request payload to Claude / Cursor / Desktop. **Recommended security update for all MCP users.**\n- Profile file at `~/.pentest-ai/auth-profiles.yaml` enforced at mode `0600` on every write; refuses to load if perms are wider.\n- New `SecureCredential` in-memory class redacts on `repr/str/format/json/pickle`.\n- Resolvers fail closed: missing dependency or unreachable backend raises `SecurityError` instead of falling back to a plaintext prompt.\n- Playbooks refuse to load if they contain bare `password:`, `token:`, or `secret:` keys (defense in depth against committing creds to YAML).\n\n### Deprecated (will be removed in 0.11)\n\n- MCP `password=` / `bearer_token=` / `credentials=` parameters. Use `auth_profile=`. Old params still work but emit a deprecation warning.\n- `--basic-auth user:pass` CLI flag (passes credential on argv). Use a profile.\n\n### Backward compatibility\n\nAll existing CLI flags (`--login-url`, `--login-user`, `--login-password-env`, etc.) continue to work unchanged. Migration is optional; the new system is purely additive until 0.11.\n\n### Tests\n\n- 94 new tests across credential resolvers, profile manager, MCP integration, chain command, playbook integration, and migration tooling\n- 5 sentinel-leak tests verifying credential values never appear in profile files, logs, MCP payloads, or process argv\n- All 586 existing tests still pass\n\n---\n\n## [0.10.1] — 2026-04-22\n\n### Fixed\n\n- Authenticated web scanner now finds real bugs on DVWA reliably\n- `pentest-ai start` correctly invokes the orchestrator (not the MCP server)\n\n## [0.10.0] — Earlier\n\n- Initial 0.10 line, 10 agents, 200+ tool wrappers, MCP server with 35+ tools\n"}}}}