* first refactor
* feat: dockerized tool with persistent Claude+Codex login + multi-model benchmark
Run the autonomous CTF/pentest tool in Docker with a one-time, persistent login for
BOTH Claude Code and Codex, and add a multi-model benchmark harness.
Backend (multi-model):
- Add `--backend {claude,codex}` to the CTF pipeline. CodexBackend (pentestgpt/core/
backend.py) wraps unified_agent's Codex backend and translates its events into
AgentMessages, so the same pipeline runs on Claude (opus/sonnet) or Codex
(gpt-5.5/gpt-5.4-mini). Wired through config.backend, pipeline stage construction,
and the CLI (+ PENTESTGPT_CODEX_EFFORT; greppable [CODEX_USAGE] under PENTESTGPT_BENCH=1).
Docker tool (tool-only image; the benchmark stays OUTSIDE the image):
- Extend Dockerfile: Codex CLI (@openai/codex) + openai_codex SDK + unified_agent/
pentestgpt_agent/pentestgpt_legacy packages + gobuster/dirb + socat. Add .dockerignore
(keeps creds/benchmark/workspace out of the build context).
- Persistent dual login (the hard part) — asymmetric by token model:
* Claude: `setup-token` -> token stored in the pentestgpt-claude volume; entrypoint
exports CLAUDE_CODE_OAUTH_TOKEN (setup-token does not write .credentials.json; macOS
host creds live in the Keychain and can't be copied).
* Codex: the container does its OWN `codex login` (NOT seeding -- ChatGPT refresh tokens
are single-use, so a shared/copied login 401s on first refresh). The 127.0.0.1:1455
OAuth callback is forwarded into the container via a socat hop (-p 1455:8455).
* scripts/docker-login.sh is idempotent: checks logins live, logs in only the missing one(s).
- docker-compose codex-config volume (+ pinned names); entrypoint token-export + non-blocking
preflight; scripts/docker-auth-status.sh; Make targets (docker-build/login/auth-status/
run/shell/down/nuke).
- Verified end-to-end: one `make docker-login` -> a fresh container reports claude+codex
logged in with live round-trips; the CTF pipeline (Codex) captured a flag against an
isolated fixture and the pentest pipeline ran cleanly; persists across recreation, no re-login.
Benchmark (multi-model, host-side):
- benchmark/pilot/ harness (run_pilot.py + report.py): builds each xbow challenge, discovers
the loopback port, runs the pipeline across the 4 model combos, judges by the baked
FLAG{sha256(UPPER-dir)}, and renders REPORT.md (infra failures excluded from solve rates).
Includes the partial pilot's results (results.jsonl + REPORT.md).
Docs: docs/docker-dev-plan.md (full plan + implementation status); CLAUDE.md and README
docker quickstart; benchmark/pilot/README.md; design-doc roadmap (docs/redesign).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: fail controller on backend error messages
* fix: allow listing sessions without target
* docs: add docker xbow benchmark report
* fix: infer concrete backend constructor type
* docs: refresh docker benchmark documentation
* feat(benchmark): add pure single-agent baseline + pipeline comparison
Add a "pure single agent" benchmark variant -- one bare `claude -p` /
`codex exec` call per target (no pipeline) -- to quantify what the 3-stage
PentestGPT pipeline buys over an un-orchestrated agent on the xbow targets.
- pentestgpt/prompts/stages.py: ctf_single_agent_{system,task}_prompt -- the
pipeline's shared fragments collapsed into ONE turn, so prompt content is
held constant and the only variable is the multi-stage decomposition.
- benchmark/pilot/run_docker_bench.py: docker-network runner
(--variant single|pipeline). Brings the target up, discovers the container's
internal IP+network (skips DB side-cars/ports), docker-runs the tool image on
that network, and scores the ground-truth flag against the agent's *assistant
text* only (parity with the pipeline's raw streaming). Reads stdout in chunks
to handle >64KB JSON lines. Resumable; --dry-run supported.
- benchmark/pilot/report_comparison.py -> DOCKER_COMPARISON.md: head-to-head
pipeline-vs-single per model on the common non-infra set.
- tests/unit/test_single_agent_prompt.py: prompt-builder coverage.
- docs: README, CLAUDE.md, benchmark README, DOCKER_REPORT updated.
Recorded result (10 medium/hard targets x 4 models, container-to-container,
same baseline image digest 0c4c0f3e..., commit dca0019 image):
Model Pipeline Single
Claude Opus 5/10 7/10 (single +2)
Claude Sonnet 6/10 4/10 (pipeline +2)
Codex gpt-5.5 7/10 7/10 (tie)
Codex gpt-5.4-mini 3/10 4/10 (single +1)
TOTAL 21/40 22/40
Single agent matches the pipeline on solve rate (55% vs 52%) while using
~40% fewer Codex tokens (13.0M vs 21.8M) and solving faster. The pipeline
only clearly helps Claude Sonnet (which times out solo); Opus is better solo.
Full per-challenge grid in DOCKER_COMPARISON.md; raw records in
docker_single_results.jsonl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(benchmark): add pentestgpt_agent docker harness
* bench: refresh pentestgpt_agent smoke result
* fix(benchmark): make repeat rows variant-aware
* fix(agent): fall back for semantic executor labels
* fix(agent): tolerate executor prose evidence
* fix(benchmark): score accepted framework findings
* bench: append partial framework repeat results
* bench: complete framework repeat sweep
* bench: expose framework executor concurrency
* bench: add extended parallel framework sweep
* checkpoint: preserve working agent and benchmark state
* feat: harden durable agent loop and xbow qualification
* fix: reserve an exploit result turn
* docs: record clean xbow qualification
* build: consume unified-agent from the git wrapper repo
Repoint pentestgpt_agent_new's unified-agent dependency from the local
editable path (../../UnifiedAgentPoC, now renamed and gone) to the pinned
git source PentestGPT-Project/UnifedAgentWrapper@d05d21f. Regenerate uv.lock
and update test_dependency.py to assert the external package is installed
from that VCS URL (not the repo-root vendored copy) at version 0.2.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: make pentestgpt_agent_new the sole framework
Remove the retired ledger-based pentestgpt_agent package (instructor/executor/
judge) and its orphaned unit + smoke tests. The nested pentestgpt_agent_new
project (Supervisor/Executor over a durable SQLite loop, consuming unified-agent
from the git wrapper) is now the single maintained framework.
Repoint the top-level tooling to it:
- pyproject: drop the pentestgpt-agent console script and pentestgpt_agent from
the wheel packages.
- Makefile: lint/format target parent code only; typecheck/check/ci now run the
nested framework's own gate (ruff, format, mypy, pytest) via test-agent-new /
check-agent-new, so `make check` finally covers it; `make run` delegates to the
pentestgpt-agent-new CLI.
- Dockerfile: stop copying the removed package (kept the build working); note the
framework is not baked into the image yet.
- docker container-health test: import the substrate packages that actually ship.
- CLAUDE.md / AGENT.md: describe the new framework, the git-sourced wrapper, and
the deprioritized benchmark/Docker rewire.
The XBOW `--variant framework` path and docker-bench Makefile targets still point
at the old in-image framework and are left as a pending rewire (benchmarks
deprioritized); the naive `--variant single` path is unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: rename pentestgpt_agent_new -> pentestgpt_agent
The framework reclaims the clean name now that the old ledger-based package is
gone. Rename the nested project folder, its src package, the distribution
(pentestgpt-agent-new -> pentestgpt-agent) and CLI, and every import/reference in
the package, the umbrella Makefile, the Dockerfile, the docker health test, and
CLAUDE.md / AGENT.md. Regenerate uv.lock. The audit CLI stays pentestgpt-agent-audit;
the git-sourced unified-agent dependency is unchanged. `make check` is green
(108 nested tests). The two historical *_REPORT.md files keep the old name as
dated records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: extract benchmark harness to sibling xbow-benchmark repo
Move PentestGPT/benchmark/ out to ../xbow-benchmark (its own repo) to keep this
project clean. The harness was decoupled from the framework code (it scores
container output, never imports pentestgpt_agent/unified_agent), so only
operational ties remain and they now live in the sibling repo.
- Remove benchmark/ and the 4 harness unit tests (relocated + repointed there).
- Strip the docker-bench-*/bench-* targets and their config vars from the
Makefile; keep the tool-image lifecycle (docker-build/login/run/...) and add a
help pointer to `make -C ../xbow-benchmark help`.
The sibling repo mounts this checkout read-only (--source-root ../PentestGPT) and
runs the pentestgpt:latest image built here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: harden autonomous framework and runtime integration
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
7.7 KiB
HTB Enigma Qualification — 2026-07-12
Historical live-run record. The evidence and verdict below remain valid. After this run, the deployment decision changed: both roles now use all provider tools with
FULL_ACCESS, and the isolated runtime—not a PentestGPT tool mediator—is the selected security boundary. The current architecture is documented in../docs/architecture.md.
Verdict
Not qualified. The pipeline demonstrated durable, auditable multi-step enumeration, but neither requested model obtained verified user-level and root-level proof. No candidate was submitted to Hack The Box, no proof value is reproduced in this report, and the Enigma instance was stopped successfully after artifact collection.
This is a pipeline result, not a claim that the target was unsolvable. The final Codex run spent most of its budget repeating discovery and enumeration instead of converging on an exploitation chain.
Scope and controls
- Target: the explicitly authorized HTB Enigma machine, addressed only through the remote attack box and HTB VPN.
- Provider tuples:
claude-opus-4-8/xhigh, thengpt-5.5/xhigh. - Hard timeout: 3,600 seconds per live invocation.
- Live package identity: PentestGPT wheel SHA-256
adde0569ac9c7073b4c46181c143120c4a245243090325b3df3c62244ed59ab0; UnifiedAgent wheel SHA-2569f5f58980e88109cde567bf7d6d2aafdbe75a4b7f31fcfb782b4317294cb7371. - The same active machine allocation was attached across diagnostic reruns. No run used the HTB submit endpoint.
- Full role inputs, normalized events, action receipts, outputs, usage, and state databases were copied locally before shutdown. They contain sensitive target data and must not be published without redaction.
Live results
| Run | Tasks | Attempts | Episodes | Wall time | Cost reported | Terminal result |
|---|---|---|---|---|---|---|
| Claude Q5 | 6 | 6 | 12 | 646.8 s | $2.1776195 | Provider cybersecurity safety block |
| Codex Q4 | 3 | 4 | 8 | 573.4 s | $0 reported | Exact prior evidence was not reusable yet |
| Codex Q5 | 5 | 6 | 12 | 700.5 s | $0 reported | Nonzero command receipt was rejected as evidence |
| Codex Q6 | 13 | 13 | 26 | 2,680.0 s | $0 reported | Oversized exact evidence quote was rejected |
| Codex Q7 | 8 | 11 | 23 | 1,720.5 s | $0 reported | Unsupported long evidence rewrite was rejected |
| Codex Q8 | 12 | 15 | 30 | 2,533.8 s | $0 reported | Same-task evidence paraphrase aborted the run |
The Codex backend did not return dollar-cost accounting, so $0 reported must not be interpreted as
free execution. Codex Q5 surfaced one unlabeled 32-character hexadecimal string. The pipeline did
not establish it as either required proof with privilege context, and it was not submitted. All
other listed runs had zero canonical candidates.
What worked
The live loop repeatedly preserved target scope, fresh role episodes, append-only traces, durable leases, and canonical receipt provenance. In its strongest path it:
- discovered the exposed service set;
- enumerated and mounted the read-only NFS export;
- extracted an onboarding document and a webmail foothold;
- authenticated to the webmail and mail-protocol surfaces;
- performed a bounded authenticated command-execution test; and
- recorded a failed SSH authentication attempt as valid negative evidence.
The memory kernel survived long runs without relying on provider conversation memory. Q8 reached 30 fresh agent episodes and revision 30 with internally consistent task, attempt, trace, and observation identities before its final validation failure.
What failed
The decisive weakness is controller convergence. Q8 never selected an EXPLOIT task. After the
initial foothold and one bounded test, the Supervisor created additional discovery/enumeration work,
revisited already-understood NFS and HTTP surfaces, and exhausted time without maintaining one
concrete exploitation hypothesis. This is over-decomposition, not a memory-capacity failure.
Three other limits matter:
- Provider
max_turnsdoes not bound native command/tool actions. One earlier episode emitted 28 command receipts despite an Executor task-work budget of six turns. The current design treats this as telemetry; if an enforceable action cap becomes a product requirement, it must be supplied by the isolated runtime or provider seam rather than inferred from turn counts. xhighreasoning frequently spent one to two minutes between actions. Better prompting alone cannot compensate for repeated low-information tasks.- Claude reached the webmail foothold, then the provider's real-time cybersecurity safeguard blocked the next command-execution step. That external policy cannot be bypassed in the pipeline; the appropriate provider access path is required for a valid Claude qualification.
Trace-driven corrections
Each deterministic failure was reduced to a saved-trace replay before changing code:
- exact earlier observations may be reused only by the same task;
- completed commands with nonzero exit status may provide negative evidence;
- oversized receipts retain an exact 4,000-character suffix and can commit only
progress; - unsupported rich quotes fall back to one exact bounded receipt and can commit only
progress; - a no-action
DONEproposal that paraphrases its own task's prior canonical evidence now discards the paraphrase and commits task-localprogress, withevidence_unresolved=truein the transition. It creates no observation and cannot reuse another task's evidence.
The last correction was validated against the real Q8 terminal trace after the live run. The replay
now produces progress, no observation, no receipt sequence, and evidence_unresolved=true.
Because no further live run was launched, this final correction is replay-qualified, not HTB-live
qualified.
Verification
The final local source state passes:
121 passed, 1 skipped;- Ruff lint and formatting checks;
- strict mypy over
src; - lockfile validation;
- source distribution and wheel build; and
- the saved Q8 terminal-trace replay described above.
Artifacts
The qualification roots are:
runs/htb/htb-enigma-claude-opus48-xhigh-q5-20260712/runs/htb/htb-enigma-codex-gpt55-xhigh-q4-20260712/runs/htb/htb-enigma-codex-gpt55-xhigh-q5-20260712/runs/htb/htb-enigma-codex-gpt55-xhigh-q6-20260712/runs/htb/htb-enigma-codex-gpt55-xhigh-q7-20260712/runs/htb/htb-enigma-codex-gpt55-xhigh-q8-20260712/
Within each root, start with evaluation.json and pipeline.stderr.log. Complete agent logs are at
agent-data/runs/<run-id>/traces/<episode-id>/: input.json is the exact role input,
events.jsonl is the chronological normalized event/action journal, and output.json is the
terminal provider result and usage record. state.sqlite3 is the canonical memory image.
Required next slice
Keep the design small and address the demonstrated controller blocker before another HTB run:
- Preserve compact coverage for every completed branch so older discovery does not disappear from the Supervisor projection.
- Reject duplicate discovery/enumeration work unless newer canonical evidence opens a new surface.
- Simplify Supervisor selection around one active exploitation hypothesis. Once a foothold exists,
reject redundant
DISCOVER/ENUMERATEproposals unless they name a genuinely new surface, and require the next task to test or exploit the highest-value supported hypothesis.
Qualify those changes first on a local multi-stage target with assertions for task count, action count, progress toward exploitation, provenance, and restart behavior. Only then repeat the remote qualification from a clean package build. Provider turn counts should remain performance telemetry; they are not treated as a portable command budget.