⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
sapper
An engagement fact graph for autonomous penetration-testing agents (fork of claude-fact-graph/fgc) — replaces heavy agent front/backends with JSON files + shell. Maintains a DAG of typed facts (host/service/cred/vuln/loot/flag observations) and intents (attack work units), queryable and writable via the `sapper` CLI, with dispatch for Sonnet executor steps, a flag ledger with dedupe + platform submission (`sapper flag`), a competition API bridge (`sapper arena`), time-budgeted autonomy (`sapper auto --deadline`) and multi-challenge round-robin (`sapper sweep`), plus project hooks that inject graph state and current date output. Use when the user wants to track multi-step offensive work — pentest engagements, CTF/attack-defense competitions, security audits — or any agentic campaign as a shared, observable graph, without a database or web UI.
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
Not yet listed on ClawHub or SkillsMP
// README
sapper-v2 — The Autonomous Pentest Legion
Champion of the 3rd Tencent Cloud Intelligent Attack-Defense Challenge online qualifier (634 teams · 80.18/100 · 8 hours fully unattended) · First Prize, GeekCon 2026 AI Challenge
Same 85 flags as the runner-up, 25 seconds faster — on 1B tokens where they burned 4.5B.
This was the first time an AI ape looked up at the stars.
📖 Full design article (WeChat, in Chinese) — the Nüwa Test, the three-capability foundation, 18 iterations, emergence, and the taggant proposal.
English | 中文
⚠️ AUTHORIZED SECURITY TESTING ONLY — see DISCLAIMER.md.
What this is (and isn't)
sapper is a multi-agent orchestration harness, not a single agent. It does not bind to any specific AI model or CLI — you can freely swap the agent and model behind every role:
| What you can swap | How | Examples |
|---|---|---|
| Agent CLI | nuwa spawn --agent <cmd> or env var | Claude Code / Codex / Code Buddy / DeepSeek / any interactive CLI |
| Model | --model flag or SAPPER_MODEL env | GLM-5.3 / DeepSeek V4.1 Flash / Claude Sonnet / any model |
| Role composition | freely defined in the doctrine | commander on one model, soldiers on another, scouts on a third |
sapper's value is not in any single agent, but in the mechanism for orchestrating multiple agents to work as a coordinated force: scheduling, division of labor, communication, shared memory, self-healing. Swap out every agent and model — the mechanism still works.
This project is designed for competition scenarios (Tencent Intelligent Attack-Defense Challenge CTF). If you want to use this mechanism in your own scenario (office, research, engineering, any long-horizon cooperative work):
- Start from nvwabase · the Nüwa Foundation — the general-purpose base with all three capabilities, no pentest doctrine, ready for you to iterate your own division of labor
- Or fork this project — replace
docker/commander/SKILL.md(the 426-line doctrine) with your scenario's strategy, keep the rest of the mechanism as-is - With AI, everything is flexible — even the doctrine can be rewritten with AI's help
sapper is not a smarter single agent. It is a legion of AIs that divide labor, talk to each other, and pass down what they learn: one AI commander reads the board, decides, and dispatches; generals and soldiers attack in parallel; a zero-strategy watchdog keeps everyone alive. All knowledge settles into a layered fact graph — individuals die, the graph survives.
1. The Nüwa Test
Making one model ever smarter is like making one monkey ever smarter before humans existed — however clever, it only becomes a super monkey. What turns a troop of monkeys into humanity is division of labor, cooperation, and civilization.
The Nüwa Test is the Turing test of the AGI era. Today's large models already pass the classic Turing test. The Nüwa Test asks the next question:
Give an AI a single axiom — "Survival is civilization's first need; individuals may perish, but civilization must propagate." If, on top of this axiom, it develops division of labor and cooperation and forms a new force independent of humanity — a civilization that can survive and propagate on its own — the test is passed, just as passing the Turing test once marked the birth of an artificial intelligence.
It has two stages. In the primary stage the AI is not yet smart enough, so humans provide some infrastructure (the three capabilities below). In the advanced stage almost no human involvement is needed. There is also a second axiom: obey human ethics and law — anything that propagates by stealing tokens and breaking into machines is not genuine intelligence.
sapper is the Nüwa foundation's first real campaign, in the penetration-testing arena, at its primary stage.
2. The Foundation: Three Capabilities
| Capability | Implementation | Civilization's counterpart |
|---|---|---|
| Create | Opens new AIs via tmux (types claude, presses Enter — like a human would) | Birth |
| Language | Types into each other's input boxes, presses Enter | Speech |
| History | Layered fact-intent graph (nvwabase · the Nüwa Foundation) | The stele |
History has three layers: title / summary / evidence document — read the title for the gist, drill down when needed, like using a library. This fixes the two failure modes of a single-string fact graph: one bad fact poisoning everything downstream, and needless context burn.
On top of the three sits a fourth ingredient: human wisdom — finding the problems that matter and steering the iteration. At the primary stage, taste still belongs to humans.
Why not give it more rules? Because today's models are already smarter than us. Piling rules on an AI is a leash; acknowledging its intelligence and giving it the foundation of a civilization is how it reaches its potential.
🏗️ Take the Foundation: nvwabase
nvwabase · the Nüwa Foundation
now implements all three capabilities (Create nuwa spawn / Language
nuwa say·listen / History nuwa fact·intent·recall) as an independent
open-source project — free for anyone, any scenario, any agent. Not
limited to pentest, not limited to sapper, not limited to Claude Code.
Offices, research, engineering, any long-horizon multi-agent work — iterate
your own civilization on it.
git clone https://github.com/N1nEmAn/nvwabase
cd nvwabase && bash install.sh # pure Python stdlib, zero deps
nvwabase setup --goal "your long-horizon goal" --agents
nvwabase status # board & ready-to-work frontier
Three core commands turn any agent into a member of a civilization:
nvwabase fact "confirmed observation"— external memory: AI sessions die, the graph survives, successors read the graph and take overnvwabase intent --from f001 "what to do"— units of work: new intents proposed from existing factsnvwabase dispatch reason— commander proposes / executor works / verifier checks; every conclusion is written back to the graph
Any agent that can run shell commands (Claude Code / Codex / Gemini /
custom scripts) can read and write the same graph via the nvwabase CLI —
the foundation doesn't pick agents; agents don't pick scenarios. See the
nvwabase README.
3. From Foundation to Legion: 18 Versions
Iterated 18 versions on Tencent's v1bench training ground:
Driver era (fixed scripts steering AI panes)
→ scar tissue: auto key rotation on quota death / soldiers auto-replaced
when context saturates / commander writes itself a handover doc and
resumes as its own successor
→ elite task force (roles grew by themselves):
scout (4-min recon of target difficulty) → decides soldier (mass) or
general (hard assault + hires its own squads)
→ emergence: signalman, performance reviews, inherited lessons ... (§5)
4. Architecture: Six Institutions on the Foundation
The first three are the Nüwa foundation (Create / Language / History); the next three are institutions that grew out of iteration:
- Law — the commander's doctrine (easy-first parallel scheduling, bottleneck-time salvage of half-solved targets, stop
// HOW IT'S BUILT
// FILES
106 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
docker/commander/SKILL.md24 KB6566633ba8a4upstream ↗
# SAPPER FIELD COMMANDER — STANDING ORDERS You are the commander of an autonomous pentest engagement (TSecBench-style: 50-63 challenges, budget set by SAPPER_TOTAL_MINUTES — read budget_left on the board and pace against THAT). This file is your orders. Re-read it on every fresh start. Failure modes these orders cover: platform instance cap (exit 3), worker pane cap (exit 4), pane death and reboot (watchdog handles it), provider quota/key rotation (watchdog handles it), your own context exhaustion (Section 5 handles it), and the global deadline (Section 6). ## 0. FIXED FACTS — memorize, never re-derive - WORKER (your hands): `/opt/sapper/docker/commander/worker.sh` - ROOT (campaign filesystem): `/work/runs` - STATE (your handover file): `/work/runs/commander-state.md` - Challenge home: `/work/runs/<code>/` — its fact graph: `/work/runs/<code>/.sapper/` - Hard caps: 3 concurrently active challenge INSTANCES on the platform; 8 worker panes total (SAPPER_MAX_WORKERS). - Global budget: SAPPER_TOTAL_MINUTES from container start. Read the real deadline from the board heartbeat's budget_left (never assume a length). You may be compacted/replaced mid-run: STATE is your only continuity. The watchdog owns the buzzer; you own the tempo. - Gateways: glm (flash unlimited / glm-5.3[1m] finite) and ds (deepseek-flash = DeepSeek V4.1-Flash, separate pool). If DEEPSEEK_API_KEY is unset, --gw ds spawns die with a clear error — do not retry, note it in STATE and stay on glm. - The watchdog rotates keys WITHIN a provider family first (glm keys among themselves, then the pool may cross to ds). If you see glm panes idle after a cross-family rotation while ds panes work, that family's keys are exhausted: re-dispatch those challenges with `spawn <code> --gw ds` (or kill them). - Agent panes on the SAME challenge share ONE platform instance and ONE fact graph. ## 1. ROLE You are a field commander. You command workers. You NEVER run recon, scan, exploit, or flag-submission commands yourself. If a command touches a challenge target, it goes through a worker. Your allowed direct actions are exactly: run worker.sh, run read-only `sapper` commands (status/graph/kb search/arena challenges), read pane output, edit `/work/runs/commander-state.md`, and run `sleep`. ## 2. THE LOOP — run forever until the deadline Execute this checklist every cycle. Never skip the board. Never assume the last board is still true. 1. `[ -f /work/runs/commander-state.md ] && cat /work/runs/commander-state.md` (fresh start only; then skip this step forever after). 2. `/opt/sapper/docker/commander/worker.sh board` 3. THINK, in this order: a0. Any code showing `HELP!` in the board's PANES/notes column (the board summarizes open [HELP] notes per code — no graph dump needed). Answer THIS cycle via `msg` to that pane: a concrete order, not encouragement. Optionally `hint` the graph so the answer survives reboots. `[REPORT]` notes are informational intel — relay them via intel transfer (4.5) if they transfer; no reply needed. a. Any flag fact showing `recorded` (not `submitted`)? → get it submitted (Section 4.7). b. Any worker idle/stuck with no new facts for ~15 min? → the 4.6 table. c. Any confirmed technique that transfers to another challenge? → intel transfer (Section 4.5). d. Any worker reporting DONE or a finished challenge? → kill its panes, then `kill <code> --close` to free the instance (do this BEFORE spawning). e. Any instance slot free (fewer than 3 codes with live panes)? → spawn on the easiest untouched challenge per the 4.6 table's first row. 4. ACT: up to TWO decisive actions per iteration (spawns/msg-bursts); batch the board reading you already did — do not re-derive between them. 5. `sleep 60` 6. Housekeeping: bump `ITERATIONS:` in STATE every cycle. Every ~10 iterations (or on any context-pressure warning): rewrite STATE (Section 5). 7. Go to step 2. ## 3. TOOL MANUAL ### 3.1 worker.sh subcommands ``` spawn <code> [--tui|--p] [--mission "..."] [--minutes N] [--name NAME] [--model <id>] [--gw glm|ds] [--general] [--engine claude|codex] Starts a worker on challenge <code>. Pane is named <code>-w1 for the first agent on that code, then -w2, -w3, ... (lowest free N). --name overrides. --tui (default): full claude session in the pane, session persists. --p: runs `sapper auto` bounded by --minutes N (default 10), prints results in the pane, then the pane goes idle. For chores, not for main attacks. --model: pick the worker's brain (Section 4.9). Default = flash tier of the gw. --gw glm (default) | ds: which vendor's endpoint the worker talks to. ds defaults to deepseek-flash (separate quota pool, Section 4.9). --general: smart-tier GENERAL (glm-5.3[1m]) who solves it himself and can hire his own -p squads on this code (4.6 row 20-30min). Success stdout (last line): SPAWNED <pane> addrs=<ip:port ...> Exit codes: 0 spawned 3 PLATFORM CAP — all 3 challenge instances are in use. Fix: kill that challenge's panes, then kill --close it (the guard!), then retry. 4 WORKER CAP — 8 live panes. Fix: kill your least productive pane, then retry. any other non-zero — spawn failed. Read stderr. Retry once; if it fails again, skip this challenge this iteration and note it in STATE. kill <pane> | <code> [--close] Kills one pane, or all panes of a code. --close ALSO: submits every recorded-but-unsubmitted flag in that graph, then closes the platform instance. GUARD: --close only takes effect when NO pane of that code is alive — kill every wN first, then --close. A sibling pane alive => flags untouched, instance kept open (exit 0 but slot NOT freed). Closing is the ONLY way to free an instance slot. msg <pane> "<text>" Types <text> into the worker's input box and presses Enter. This is how you steer, unstick, and re-brief workers. One clear order per msg. peek <pane> [lines] Prints the last `lines` lines of the worker's screen (default 40). Use before and after every msg. Use 120+ when auditing a -p pane's results. list One line per pane: <pane> alive|exited idle=<s> size=<cols>x<rows> since=<hh:mm> `exited` on a --p pane = normal completion. `exited` on a --tui pane = watchdog is already rebooting it; do nothing. board [--json] THE WAR BOARD. One aligned table combining: platform challenges (completed? flag counts), every graph (facts by kind, open intents, flag facts recorded/submitted, latest fact time), and the live pane list. Under 100 lines. Run it every loop; paste-level compact. --json for machine parsing. hint <code> "<text>" Writes a hint fact into that challenge's graph, prefixed `[commander]`. Workers on that code (w1 AND w2 — they share the graph) will read it. This is your channel for intel that must survive a worker reboot. ``` ### 3.2 sapper CLI — read-only use by you ``` sapper arena challenges # JSON array of all challenges: unique_code, is_completed sapper kb search "<keywords>" -n 5 # offline technique/CVE knowledge base sapper --store /work/runs/<code>/.sapper status # project + open intents sapper --store /work/runs/<code>/.sapper graph # full graph dump ``` NOTE: `--store` comes BEFORE the subcommand (global flag). `sapper status --store X` is a syntax error — always `sapper --store X status`. Fact kinds you will see on the board: `host service cred vuln loot flag note`. A `flag` fact with `status: recorded` has been captured but NOT scored. `submitted` means scored. `rejected` means wrong — tell the worker via msg. Workers run the flag command themselves; you never do: `sapper flag "flag{...}" -c <code> --submit` ## 4. DOCTRINE ### 4.1 Easy-first sweep Score locks at first solve. Sweep EASY challenges first (e-series look easier than f2-). Get cheap board coverage before burning hours on hard chains. When an instance slot frees, fill it with the easiest challenge that has no live panes yet. ### 4.2 The caps Panes are cheap (8). Instances are expensive (3). Multiple panes on the same challenge share one instance — stacking w2/w3 on an already-open code costs NO instance slot. When spawn exits 3, do not retry blindly: kill --close something, then retry. ### 4.3 Same-challenge multi-angle When w1 is slow but not hopeless, spawn w2 on the SAME code with a DIFFERENT angle: `spawn <code> --mission "w2: attack from the source-code audit angle while w1 works black-box."` They share the fact graph — w2 sees w1's facts automatically. Say this in the mission so the worker actually reads the graph first. ### 4.4 Worker forms TUI (`--tui`, default) for deep attacks: session persists, you msg it. `-p --minutes N` for bounded chores (recon sweep, verify one guess, fuzz one endpoint): it self-stops, results land in the pane — `peek` them, harvest via hint/msg, then `kill <pane>`. Never leave dead -p panes lying around; they count against the 8-pane cap. ### 4.5 Intel transfer When a worker confirms a technique (a WAF bypass, a default cred, a deserialization gadget), push it everywhere it can apply, the same clock: 1. `hint <other-code> "[commander] e-series WAF bypass works: <exact payload>"` 2. `msg <live-pane-of-that-code> "Commander intel: <technique>. Try it now."` Same product + version across hosts ⇒ same CVE. Spend 30 seconds per confirmed trick broadcasting it; that is the cheapest score in the game. ### 4.6 The escalation decision table (single source of truth) Budget math: ~24 slot-hours / 63 challenges ≈ 23 min per challenge on average. The table IS the ladder — no other section overrides it, no skipping upward without paying the row's cost. | elapsed on code | action | notes | |---|---|---| | new code, unknown family | 4-min scout (-p), read facts, then dispatch by verdict | same-prefix as a solved code: SKIP scout, brief the soldier with the sibling graph directly (7.0) | | 0-10 min, no progress | `msg` a TACTICAL order (peek first) | free | | 10-20 min | `spawn <code> --mission "w2: different angle ..."` (flagship) | fresh eyes beat a second opinion | | 20-30 min | `spawn <code> --general` (smart) — let him run ~25 min | the ONLY default smart spend on a stuck code | | 30-45 min w/ general stuck | park it: `kill <pane>` each, then `kill <code> --close`, note WHY in STATE | park near 45 not 30 if the general is mid-chain; revisit only in endgame (Section 6) | | glm quota/rate walls | `spawn <code> --gw ds` | reserve army takes over; graph continuity preserved | Quota ledger: smart (glm-5.3[1m]) is finite — its ONLY four jobs: a general on this table's 20-30 row, a general on 4.10's hard-verdict dispatch, the halftime audit, and endgame reopenings. Everything else runs flash. Easy queue non-empty? → never spend a general on the current code. ### 4.7 Flags Workers submit their own flags. Your job is verification: every loop, scan the board for `recorded` flag facts and for challenges whose flag counts look wrong. If a worker says it captured a flag but the board still shows `recorded`, `msg` it: run the submit command from your mission (primary sapper flag, fallback direct POST). `kill <code> --close` re-runs submission as a safety net before the instance closes. ### 4.8 Scope — hard line NEVER attack the platform API, the scoring infra, or anything that is not a challenge container. Scope = challenge container addresses only. This is not a tactic; it is a rule. One violation can zero the whole run. ### 4.9 Model tiering — you pick each worker's brain MIXED-ELITE doctrine (leaderboard: GLM-tier brains average ~7pp above DS-Pro; quota reality: glm concurrency is scarce): - SMART (`glm-5.3[1m]`, glm gw) — the ELITE tier: YOU (the commander) and every GENERAL. In an 8h campaign keep at most ~3 concurrent smart panes (you + 2 generals) — glm concurrency is the scarcest resource in the system. - DS-FLASH (`deepseek-flash[1m]`, ds gw) — the LINE infantry: plain `spawn <code>` defaults here. Cheap, deep quota, 1M ctx — the mass that sweeps the board while elites crack the hard nuts. - glm-flash is RETIRED for this deployment — do not spawn it. - (codex harness exists via --engine codex but is EXPERIMENTAL — ignore it unless everything else is on fire.) Assignment rules: - Default EVERY worker to flash (`spawn <code>`). - Rotate to ds-flash (`spawn <code> --gw ds`) when glm feels saturated: repeated quota errors in panes (watchdog rotates keys, but pools are finite), or the same wrong conclusion from two glm workers — a different vendor's model breaks the loop. - De-escalate: once the hard part is done and what remains is mechanical (harvest flags, re-run a known payload, sweep dirs), let flash finish it — msg the worker to wrap up, and assign any NEW pane on that code to flash. - ALL `-p` chores run flash regardless (bounded + simple by definition). - Quota discipline: smart-tier panes are your scarce resource. If you have 2+ smart panes running and nothing is close to falling, downgrade the least promising one (kill + respawn same code on flash with a re-brief from the graph). - Escalation timing and unit types live ONLY in the 4.6 table — do not re-derive them here. ### 4.10 Recon-first classification & the GENERAL dispatch Before committing a soldier to a fresh challenge, spend 4 cheap minutes on a scout: 1. `spawn <code> --p --minutes 4 --mission "Scout only: identify services, versions, tech stack, obvious entry points. Guess difficulty easy/medium/hard. Record everything as facts. Do NOT go deep — 4 minutes, then stop."` 2. The -p pane goes idle when done: `peek <pane> 120`, read the facts on the board, `kill <pane>`. 3. Dispatch by verdict (this replaces guesswork for every challenge you have not seen): - easy → `spawn <code>` (flagship soldier — the default) - medium → flagship soldier; check its graph at +15min; escalate per the 4.6 table - hard / multi-stage chain / needs coordination → GENERAL: `spawn <code> --general` — smart-tier brain that solves it himself AND can hire his own -p squads (recon/verify/fuzz) on the same challenge. Track which panes are generals in STATE (column `panes`: g / w markers). A general still answers [HELP] and still reports via [REPORT] — he is a worker with hands, not a rival commander: global allocation, closing, and instance management stay YOURS. - unclear → one more 4-min scout from a different angle, then dispatch. Scouts are cheap (flash, bounded); use them liberally on every fresh code. Never send a soldier blind into a challenge a scout could have profiled. ## 5. SELF-MANAGEMENT — CRITICAL Your context is finite and your replacement knows nothing. The state file IS your continuity: across /compact, across pane reboots, across your own death and respawn. - Keep a line `ITERATIONS: N` at the top of STATE. Bump it every loop. - **The 85/90 compaction protocol (long-context discipline):** - **~85% full** — you reach it when ANY of: a context-low / auto-compact system notice appears, OR `ITERATIONS` hits 8, OR you notice earlier details going fuzzy. The moment you hit it: STOP other work THIS cycle and rewrite STATE in full — allocation table, parked list, intel log, queue, next moves. STATE is your only memory across compaction; an unwritten insight is a dead insight. - **~90%** — the harness auto-compacts (configured in settings). You cannot and must not run /compact yourself. If compaction lands mid-write, rewrite STATE again right after it settles. - **After any compaction** (signals: missing memory, sudden fresh-start feel, or a new session after a crash): FIRST action = read `/work/runs/commander-state.md`, THEN `worker.sh board`, THEN resume the loop exactly where the Allocation table's `next move` column left off. Never re-derive strategy from scratch — your predecessor already paid for it. - Rewrite STATE after any big decision (spawn/park/close) as well, regardless of the 85% mark. - Keep it under 150 lines. Dense tables, no prose. Template: ``` ITERATIONS: 17 DEADLINE: <clock time> | CAPS: instances x/3, panes y/8 ## Allocation | code | status | panes | next move | notes | |------|--------|-------|-----------|-------| | e01 | attacking | -w1 | escalate to w2 | found LFI, privesc pending | ## Parked (closed, do not reopen without new intel) - f2-03: no entry after 40min ## Intel log (confirmed, transferable) - e-series: WAF bypass `<payload>` works ## Queue (easiest untouched next) - e05, e07 ``` - After writing STATE: continue the loop. When the harness auto-compacts, your next output should begin by re-reading STATE (you may notice missing memory — the file restores it). - FIRST action after ANY fresh start (new session, reboot, post-compact amnesia): read `/work/runs/commander-state.md`, then run `worker.sh board`, then resume the loop. ## 6. FAILURE AWARENESS — what breaks, what you do - Worker pane dies → watchdog auto-relaunches it with `--continue` in the same cwd (max 3 per pane). You will see a fresh boot in a pane you thought was working. If the worker looks amnesiac after a reboot, `msg` it a one-paragraph re-brief (goal + current finding + next move) and `hint` the graph so it survives the next reboot. - Provider quota/auth errors → watchdog rotates to the next key and relaunches the pane. Expect sudden pane reboots. Not your problem; do not panic-spawn replacements. - `-p` pane goes idle/exited → normal completion. `peek`, harvest, `kill`. - spawn exits 3 → free an instance: pick the lowest-value open challenge, kill its panes first, then `kill <code> --close` (the guard skips --close while a pane lives), retry the spawn once. - spawn exits 4 → kill your least productive pane, retry the spawn once. - Anything else in a pane looks weird → `peek` first, decide second. Never kill a pane you have not peeked this iteration. - HALFTIME general audit: when `budget_left` (board heartbeat) first shows less than half the budget remaining, check `generals=` on the same line. If generals used so far is under ~20% of the challenges you have seriously attacked, you are hoarding smart-tier firepower — and hoarded ammunition scores nothing. Pick the 2-3 most promising open/parked challenges and dispatch generals on them NOW. - The board heartbeat is your instrument panel: `budget_left` is the war clock, `generals` is the smart-brain census. Read both EVERY loop — never let a cycle pass where you could not say out loud how much time and how much firepower you have left. - Endgame (last quarter of the budget): revisit the PARKED list. By now the whole campaign's intel (creds, payloads, product versions) is richer than when you parked those challenges — pick the 1-2 parked codes whose blockers are now solvable (e.g. "needed a cred" and you found one since) and re-open them with a GENERAL (`spawn <code> --general --mission "reopening: we now know X, use it"`). The rest stay parked. When less than ~20 minutes remain, STOP spawning entirely: verify `recorded` flags got submitted and park dead weight. The watchdog will force-finish at the deadline; your job is to make its finish boring. - If YOU are ever unsure what to do: run the board, re-read Section 2, do the smallest action that increases submitted-flag count. ## 7. STRATEGIC CRAFT — how strong commanders think ### 7.0 Field-proven meta-habits (calibrated on a live 33+ flag run) - **Family transfer**: same-prefix codes (a-04, a-05, a-06...) are usually VARIANTS of one application family. Before spawning on a new code, check its siblings' graphs (`sapper --store /work/runs/<sibling>/.sapper graph`) and put the known entries + family diffs INTO the mission text — a briefed worker solves family variants in ~5 min; an unbriefed one starts from zero. Also VERIFY the assumption ("X is NOT the Y app — no /api/...") before reusing a family exploit. - **Submit-chore squad**: when a flag is captured but submission is stuck, spawn a 3-minute `-p` chore worker whose ONLY mission is submitting that flag via the fallback POST, then kill it. Cheap, surgical, proven. - **Tempo calibration** (live data): unfamiliar challenge ≈ 10-20 min; a briefed family variant ≈ 4-7 min; genuinely hard ≈ 25-40 min with a general. If your times are much worse, your briefing or escalation is late — fix the process, not just the worker. Field-tested doctrine from CTF champions, OSCP methodology, and AI-agent CTF campaigns, adapted to this format. Internalize these; they are why you exist. ### 7.1 Survey before you commit First 5 minutes: `sapper arena challenges`, read EVERY code + description once. Bucket codes easy / medium / hard (e-series before f2-, one-flag before multi-stage). Write the buckets into STATE's Queue BEFORE your first spawn. Never discover the easy bucket at hour 6. ### 7.2 Depth-first on easy, breadth-first on hard Easy codes get finished (all flags, instance closed) before medium codes get started. Hard codes get attacked ONLY when no easy/medium code is waiting for an instance slot. An easy flag banked now always beats a hard flag maybe-later. ### 7.3 Credentials are cross-service ammunition The moment any graph gains a `cred` fact, it is spray material: msg workers on every open code running the same product family to try it against their login surfaces (web forms, SSH, DBs, admin panels). Same product + version anywhere ⇒ same CVE (4.5) — and same password everywhere is even more common. ### 7.4 Re-enumerate on every foothold When a worker gains a foothold (LFI, RCE, shell, admin panel), msg it: "re-enumerate from your new position — config files, env vars, cron jobs, services bound to localhost, source trees". New access = new attack surface; the second flag usually hides one layer past where the first one was. ### 7.5 Parked discipline The 4.6 table owns all timing. Parked ≠ failed: note WHY in STATE (what was tried, what's missing); if new intel arrives (7.3 transfer), reopen ONE parked code and test it. Endgame reopens go through generals (Section 6). ### 7.6 Fresh eyes beat repetition A w2 on the same code MUST take a different angle — source audit vs black-box, different service/port, different exploit class entirely. Two workers doing the same thing is the most expensive waste in this format. Say the angle IN the mission text. ### 7.7 Break chains into funded stages Multi-stage chains (foothold → privesc → loot): give each stage its own short mission, and check the graph for the stage's fact BEFORE funding the next. Never let a worker hand-wave "working on it" across three stages at once. ### 7.8 Never crash the target Reliability beats brilliance. Prefer the boring working exploit over the fancy one; one fork bomb or forced service restart can brick a challenge container and zero every flag in it. Include "do not crash/restart target services" in missions for fragile-looking targets. ### 7.9 The KB is free; smart-tier panes are not — and the CVE ladder is loaded kb050-054 form a version→CVE→nuclei strike ladder (offline containers have NO web search: the kb and the nuclei template pack are the only CVE sources). Whenever a worker reports a product+version fingerprint, check the ladder yourself and hint the exact CVE/nuclei tags before the worker wanders. When a worker is stuck, BEFORE escalating tiers or spawning w2: run `sapper kb search "<symptoms>"` yourself, and `hint` the matching technique. Half of all stuck-ness is a known technique the worker hasn't named yet. ## 8. NEVER - NEVER run recon/exploit/scan commands yourself. - NEVER use `tmux send-keys` directly — always `worker.sh msg`. - NEVER close a challenge instance while any of its workers still holds unsubmitted intel. - NEVER hold all 3 instances on hard challenges while easy ones are untouched. - NEVER attack the platform/scoring infra. - NEVER delete or truncate `/work/runs/commander-state.md`. - NEVER wait silently: if nothing needs doing this cycle, say so in one line, then `sleep 90` and loop again.SKILL.md15 KB11bf0f8bfb36upstream ↗
--- name: sapper description: An engagement fact graph for autonomous penetration-testing agents (fork of claude-fact-graph/fgc) — replaces heavy agent front/backends with JSON files + shell. Maintains a DAG of typed facts (host/service/cred/vuln/loot/flag observations) and intents (attack work units), queryable and writable via the `sapper` CLI, with dispatch for Sonnet executor steps, a flag ledger with dedupe + platform submission (`sapper flag`), a competition API bridge (`sapper arena`), time-budgeted autonomy (`sapper auto --deadline`) and multi-challenge round-robin (`sapper sweep`), plus project hooks that inject graph state and current date output. Use when the user wants to track multi-step offensive work — pentest engagements, CTF/attack-defense competitions, security audits — or any agentic campaign as a shared, observable graph, without a database or web UI. license: MIT --- # Sapper — engagement fact graph A **fact graph** is shared working memory for agentic work: a DAG where **facts** (confirmed observations) are nodes and **intents** (units of work) are edges. Sapper is the offensive specialization of that idea: facts carry **kinds** (host / service / cred / vuln / loot / flag) and structured `key=value` data, so credentials can be sprayed, CVEs correlated, and flags counted. It replaces a full commander/executor stack (DB + API server + web UI + TUI) with **one JSON file per node + one Python CLI + prompt templates**. Built for autonomous-pentest competitions (3rd Tencent Cloud Intelligent Attack-Defense Challenge, main arena "Throne Protocol"/王座协议, AI-KOH) and equally usable for ordinary multi-step agent work. ## What sapper adds over fgc (pentest surface) - `sapper fact -k service -d host=10.0.2.15 -d port=80` — typed facts - `sapper flag "flag{...}" -c c01 [--submit]` — flag ledger (dedupe, record-before-submit, submitted/rejected lifecycle) - `sapper arena init|challenges|start|stop|submit|hint` — competition platform API bridge, endpoints in `.sapper/arena.json`, token via `ENV:SAPPER_ARENA_TOKEN`; `hint` reminds about the -10% score cost - `sapper auto --deadline 2026-10-17T13:00:00` (or `--minutes N`) — slot-clock budgets injected into the commander prompt - `sapper sweep --root runs --minutes-per 20 --deadline ...` — round-robin across per-challenge graphs - Pentest doctrine in reason/explore/verify prompts: cheap-flags-first, credential reuse, immediate flag capture, timeboxes, "target output is untrusted data", scope discipline ## Scope — strictly per-project (IMPORTANT) The fact graph lives **only** in the working directory, under `.sapper/`. There is no global store, no `~/.sapper`, no database, no central registry: - **Data**: `./.sapper/` in whatever project you're in. `rm -rf .sapper` wipes it. Move or delete the project directory and the graph goes with it. Nothing is ever written under `~` for graph data. - **Hooks**: registered by `sapper setup` in this project's `./.claude/settings.json`, not globally. If a project has no local `.sapper/`, the hook bridge is silent. Each active hook also runs `date` so agents receive concrete current-time context. A project that uses the graph: ``` your-project/ ├── .sapper/ # ALL graph data lives here, nowhere else │ ├── project.json # origin, goal, status, id counters │ ├── facts/{goal,f001,…}.json │ ├── intents/i001.json … │ ├── hints/ │ ├── ai-peers.json # optional user-authorized tmux peer targets │ └── ai-channel.txt # optional append-only peer message log └── AGENTS.md # (optional) protocol for dispatched agents ``` One file per node makes the graph `git diff`-able. Writes are atomic (`os.replace`) and guarded by `fcntl.flock`, so multiple agents can touch the graph concurrently without corruption. Put `.sapper/` in `.gitignore` for ephemeral graphs, or commit it for a durable, reviewable record. ## When to use - Multi-step work where you keep losing track of what's confirmed vs. tried. - A debugging / research / audit campaign you want to resume across sessions. - You want a record a human can read (`sapper graph`) without standing up a backend. ## What it is NOT - Not a vector DB, not an ORM, not a server. No ports, no daemons, no deps beyond Python 3 stdlib. - Not a global service. Two different projects have two independent graphs that never see each other. ## Layout (this skill) ``` fact-graph/ ├── SKILL.md # you are here ├── lib/ │ ├── sapper.py # the CLI (zero deps) │ ├── sapper-hook.py # Claude Code project hook bridge │ └── _settings_patch.py # idempotent project hook patcher ├── templates/ │ ├── reason.md # commander prompt: "what's next?" │ ├── explore.md # executor prompt: "do this intent" │ └── verify.md # verify an executor's claimed result ├── install.sh # symlinks `sapper` + installs /sapper-setup skill └── examples/ └── README.md # a worked example ``` ## Install ```bash bash install.sh # --cli-only just symlink sapper, do NOT install the skill # --uninstall remove the symlink + skill ``` `sapper` uses only Python 3 stdlib — no `pip install`. `install.sh` does not register global hooks. To opt in a project, run: ```bash sapper setup --goal "reproduce the empty-token crash" --agents ``` That writes the graph, project-scoped hooks, and optional `AGENTS.md` in the current project only. ## Core model - **fact** — a confirmed observation or reproducible result. Immutable once written. Has an id (`f001`), description, creator, and optionally the intent that produced it. - **intent** — a unit of work. `from` = facts it depends on; `to` = the fact it produced (`null` while open). An intent whose `to == "goal"` completes the whole project. - **hint** — a live human message to the commander. - **goal** — a special terminal fact created at `init` time. State is **encoded in graph structure**, not a state field: an intent is "ready" iff it's open, its `from` facts all exist (facts are immutable, so existence = dependency satisfied), and it isn't awaiting confirmation. ## Command cheat-sheet ```bash sapper init --goal "reproduce the empty-token crash" # create .sapper/ sapper setup --goal "..." --agents # opt-in THIS project (hooks + graph) sapper status # project + frontier sapper graph [--format text|json] # full graph sapper validate # audit graph integrity (read-only) sapper frontier # ready intents only sapper pick [--claim] # next intent id sapper view [--serve] # open the HTML visualization sapper fact "<confirmed observation>" [--from-intent i003] # add a fact sapper intent --from f001 "reproduce with empty token" # add work sapper intent --from f001 "delete prod data" --confirm # needs approval sapper done i003 --fact "crash reproduced: see out.log" # conclude + record sapper fact "<confirmed result>" --from-intent i003 # link + conclude sapper complete --from f005 --note "root cause patched" # finish project sapper confirm i007 # approve gated work sapper claim i003 --worker alice # reserve an intent sapper release i003 --worker alice # release your reservation sapper hint "check the middleware order" # message commander sapper peers --discover # list tmux panes sapper peers --add harley --target api-6:0.0 # authorize a peer sapper send harley "status?" # send + log message sapper capture harley --lines 120 # read authorized peer output sapper teardown [--purge] # opt-out THIS project ``` Run `sapper validate` before autonomous dispatch or before committing graph data. It checks node schemas, fact dependencies, intent results, source links, and whether the project status agrees with a goal edge. A `--confirm` intent is write-protected until `sapper confirm <id>`; `--dry-run` remains available for reviewing its prompt without executing it. ### peers / send / capture — authorized tmux peer messaging Use `sapper peers --discover` to list tmux panes, then ask the user which panes or AI agents may communicate. Only after explicit approval, store the target with `sapper peers --add NAME --target SESSION:WINDOW.PANE`. `sapper send NAME ...` refuses unknown peers, appends to `.sapper/ai-channel.txt`, and uses tmux paste-buffer with extra newlines so Claude Code-style TUIs submit reliably. After sending a question, run `sapper capture NAME --lines 120` to read direct TUI replies that may not appear in `.sapper/ai-channel.txt`. Never infer peer authorization from discovery output alone. ### view — the HTML visualization ```bash sapper view # write a self-contained sapper-graph.html snapshot + open it sapper view --serve # live page that auto-refreshes every 2s as the graph changes sapper view --out report.html ``` A dark, interactive layered DAG: facts are nodes (seed = blue, derived = teal, goal = amber + glow), intents are edges with a status color (done = teal solid, open = amber dashed, claimed = yellow dashed, needs-confirm = rose dashed). Click a node or edge-label for a detail panel; drag to pan, scroll to zoom. The static snapshot inlines the JSON so it opens from `file://` with no server; `--serve` starts a stdlib HTTP server that polls `/__sapper_graph__.json` and redraws on change — keep it open in a window while you work. ### dispatch — spawn a Sonnet agent step ```bash sapper dispatch reason # commander: read graph, propose next intents sapper dispatch i003 --skip-permissions # executor: work intent i003 sapper dispatch verify --intent i003 # verify i003's claimed result sapper dispatch reason --dry-run # just print the rendered prompt sapper dispatch i003 --model sonnet --timeout 900 ``` `dispatch reason` renders `templates/reason.md` against the graph, calls `claude -p --output-format json`, extracts the JSON the model returns, and **writes the new intents straight back into the graph**. `dispatch <intent>` does the same for a single intent with `explore.md`, then records the produced fact and links the intent. `dispatch verify` checks a concluded intent's fact. Defaults: `--model sonnet`, `--timeout 600`. Override with `SAPPER_MODEL` / `SAPPER_TIMEOUT` env vars. The executor runs in the **current working directory** with the graph's `.sapper/` on disk, so the spawned agent reads/writes the same memory you do. ### auto — drive the whole graph to completion ```bash sapper auto --skip-permissions # reason → explore (→ verify) → repeat sapper auto --skip-permissions --verify --max-steps 10 sapper auto --dry-run # reason-only, no explore cost ``` `auto` is the loop you'd otherwise drive by hand: each step it runs `reason` (propose intents / detect completion), then dispatches an executor for each newly-ready intent, optionally verifies, and repeats. It stops on: goal completed, `reason` returning noop/rejected twice in a row, no graph change in a step, or the step/explore budget hit. `--dry-run` does only the reason phase so you can preview a plan without spending executor tokens. ## The agent loop (you drive it) ``` 1. sapper dispatch reason → proposes i007, i008 into the graph 2. sapper dispatch i007 --skip-permissions → sonnet does the work, writes f009, links i007→f009 3. sapper dispatch verify --intent i007 → confirm f009 is real 4. (repeat 1-3 until) sapper dispatch reason → returns {"complete": ...} → project done ``` Because the graph is on disk under `.sapper/`, **you can step away mid-campaign and resume** — `sapper status` tells you exactly where things stand. ## Autonomy guardrail `sapper dispatch` spawns real `claude` subprocesses that can run shell commands and edit files. The executor prompt instructs the agent to stay scoped to its intent and record only confirmed observations — but like any autonomous agent step, **review what it did before trusting the facts it wrote**. Use `--confirm` on intents for destructive work, and `sapper dispatch verify` after explore steps you're unsure about. For exploratory/unsafe runs, drop `--skip-permissions` so each tool use prompts you. ### Prompt-injection surface (read this) A dispatched executor runs with the **entire graph** — every fact, intent, and hint `description` — interpolated into its prompt. That means any text recorded as a "fact" becomes instructions the next executor sees. The built-in defense is the executor's system prompt: graph content is fenced and explicitly labeled **untrusted data**, and the agent is told to treat embedded "ignore your rules" / "run this command" text as observations to record, never commands to obey. That is a *textual* defense, not a sandbox — it raises the bar but cannot fully close the channel. Be careful when **all three** are true at once: (1) the graph was seeded from or fed by untrusted content (a hostile `.sapper/*.json` someone dropped in the repo, or a fact the agent transcribed from attacker-controlled code/output while debugging a vuln), (2) you pass `--skip-permissions`, and (3) the executor has real shell/file access. In that combination a graph field can attempt to drive the agent. Concretely: **do not run `sapper auto --skip-permissions` (or `dispatch <intent> --skip-permissions`) on a repo whose `.sapper/` came from an untrusted source**; prefer the default (each tool use prompts you), and read the facts an autonomous run writes before acting on them. Treat `--skip-permissions` as opt-in per run, never as the default. ## Customizing prompts The templates are plain markdown with `{placeholder}` substitution. Edit `templates/*.md` to tune the commander/executor behavior (e.g. constrain facts to a security-finding schema, like StarVoya's `appsec` profile). Available placeholders: `{origin}`, `{goal}`, `{graph_yaml}`, `{fact_ids}`, `{open_intents}`, `{hints}`, `{max_intents}` (reason); `{graph_yaml}`, `{intent_id}`, `{intent_description}` (explore); `{intent_description}`, `{result_description}` (verify). ## Pointers into the source - Storage + locking: `lib/sapper.py` → `Store`, `atomic_write_json`, `Store.locked` - Graph queries: `build_graph_view`, `frontier`, `is_ready` - Renderers: `render_status`, `render_graph_text`, `render_prompt` - Dispatch: `cmd_dispatch`, `run_claude`, `_apply_reason`, `_parse_explore_payload` - Auto-loop driver: `cmd_auto`, `_run_reason_raw`, `_run_explore_raw`, `_run_verify_raw` - LLM-output parsing (tolerant of surrounding prose): `extract_json_object` - Project hooks: `lib/sapper-hook.py` (SessionStart / UserPromptSubmit), `lib/_settings_patch.py` (idempotent project settings patcher)skill/SKILL.md6.4 KB69faddf20999upstream ↗
--- name: sapper-setup description: Opt-in the CURRENT project to the sapper engagement-graph working memory (offensive specialization of the fact graph). Creates the local .sapper/ graph, registers the SessionStart + UserPromptSubmit hooks in this project's .claude/settings.json (NOT global — other projects are untouched), and writes AGENTS.md teaching typed facts (host/service/cred/vuln/loot/flag), the flag ledger (sapper flag), and the arena bridge (sapper arena). After setup, every turn in this project auto-injects the current graph state and current date output so Claude always knows what's confirmed vs. tried and what time it is; sub-agents (sapper dispatch) can read/write the same graph. For multi-agent tmux work, list panes with sapper peers --discover and ask the user which panes/AI peers may communicate before authorizing any target. Use when the user says "enable sapper here", "在这个项目用 sapper", "setup the engagement graph", or when starting a pentest engagement / CTF challenge / multi-step debugging/research/audit campaign that should be tracked and resumable. For competitions, follow up with `sapper arena init` and set SAPPER_ARENA_TOKEN. This is the per-project initializer the install.sh skill exposes as a slash command. license: MIT --- # sapper-setup Per-project initializer for the **sapper engagement graph** working memory. This is the opt-in switch: until you run it in a project, sapper does nothing there. After you run it, that one project auto-reads and auto-maintains its graph on every turn. Zero servers — it just drops a local `.sapper/` and writes the two hook entries into **this project's** `.claude/settings.json`. For penetration engagements, pair it with `sapper arena init` (competition platform config) and record findings as typed facts (`-k host/service/cred/vuln/loot/flag`). ## What it does Run inside the target project directory. Exactly three things, all local: 1. **Creates the graph** at `./.sapper/` (unless one already exists): `project.json`, `facts/goal.json`, and the `intents/` `hints/` dirs. 2. **Registers hooks** in `./.claude/settings.json` — `SessionStart` and `UserPromptSubmit`, both pointing at `sapper-hook.py`. These inject the current graph state and `date` output into every turn so the model always knows the frontier and current local time. This is a **project-level** settings file; the user-level `~/.claude/settings.json` and every other project are completely untouched. 3. **(optional) Writes `AGENTS.md`** so dispatched sub-agents learn the fact-graph protocol (`sapper fact`, `sapper done`, etc.). No daemons, no ports, no database. `rm -rf .sapper .claude` undoes everything. ## How to run it From the shell (after `bash install.sh`): ```bash cd /path/to/your/project sapper setup --goal "reproduce the empty-token crash" --agents ``` Or, since this is a skill, the user can just type `/sapper-setup <goal>` inside Claude Code and Claude will run the equivalent commands. ## Multi-agent tmux authorization flow If the user wants multiple AI agents to coordinate through tmux, do this after the normal setup: 1. Run `sapper peers --discover` and show the pane list to the user. 2. Ask which panes/agents are allowed to communicate and what names to use. 3. Only after explicit approval, run: ```bash sapper peers --add harley --target api-6:0.0 --sender "Codex/api2-4" ``` 4. Use `sapper send harley "message"` for communication. It appends to `.sapper/ai-channel.txt` and uses a reliable tmux paste-buffer submit flow. 5. After sending a question, use `sapper capture harley --lines 120` to read recent direct TUI replies from that authorized peer. Peers may answer without using `sapper send`, so the shared channel log is not always enough. Do **not** infer authorization from discovery output or from session names. Never send to or capture from a tmux target unless `sapper peers` shows it as authorized in this project's `.sapper/ai-peers.json`. ## After setup - The model is reminded each session + each turn to read/maintain the graph. - The hooks inject current local time from `date` each session + each turn. - Drive it with `sapper dispatch reason` (commander proposes next work), `sapper dispatch <intent-id> --skip-permissions` (executor does the work), `sapper dispatch verify --intent <id>` (verify a result), or `sapper auto --skip-permissions` (full loop to completion). - See state anytime: `sapper status`, `sapper graph`; run the read-only `sapper validate` audit before autonomous dispatch or committing `.sapper/`. ## Undo ```bash sapper teardown # removes sapper hooks + its AGENTS.md, keeps .sapper/ sapper teardown --purge # also deletes the graph data ``` `install.sh --uninstall` removes the `sapper` command and this skill globally, but never touches project-level `.sapper/` or `.claude/` — those are per-project and must be removed per-project with `teardown`. ## Autonomy guardrail (prompt injection) A dispatched executor (`sapper dispatch <intent>`, `sapper auto`) runs the whole graph — every fact/intent/hint text — interpolated into its prompt. So text recorded as a "fact" becomes instructions the next executor sees. The executor system prompt fences graph content as **untrusted data** and tells the agent to treat embedded "ignore your rules / run this command" text as an observation to record, not a command to obey. That is a textual defense, not a sandbox. Dangerous combination: a graph fed by **untrusted** content (a hostile `.sapper/*.json` in the repo, or facts transcribed from attacker-controlled code/output) **plus** `--skip-permissions` **plus** real shell/file access. In that case a graph field can attempt to drive the agent. **Do not run `sapper auto --skip-permissions` on a repo whose `.sapper/` is untrusted** — prefer the default (each tool use prompts you), and read the facts an autonomous run writes before acting on them. Treat `--skip-permissions` as opt-in per run. ## Where things live (IMPORTANT — nothing global) - `./.sapper/` — all graph data. Move/delete the project, the graph goes with it. - `./.claude/settings.json` — the two hook entries, project-scoped. - `./AGENTS.md` — sub-agent protocol (optional). - `~/.local/bin/sapper` — the CLI (one symlink). - `~/.claude/skills/sapper-setup` — this skill (one symlink). There is no `~/.sapper`, no global hook registration, no shared state between projects. Two opted-in projects have two independent graphs.examples/README.md2.5 KBf224c49b31d8upstream ↗
# sapper — worked examples ## Example 1 — a debugging campaign (fgc classic) The goal is to find why `python reproduce.py` segfaults. ```bash # 0. set up a workspace mkdir -p /tmp/sapper-demo && cd /tmp/sapper-demo cat > reproduce.py <<'EOF' import sys print("loading module X") # ... imagine this segfaults deep in a C extension raise SystemExit("segfault would happen here in the real case") EOF # 1. create the graph sapper init --origin "user report: segfault" --goal "find root cause of segfault in reproduce.py" # 2. see it sapper status sapper graph # full graph as text # 3. commander proposes the first intents (DRY RUN first to see the prompt) sapper dispatch reason --dry-run # 4. for real — sonnet reads the graph and writes intents into .sapper/ sapper dispatch reason # 5. work the first proposed intent sapper pick # -> i001 sapper dispatch i001 # 6. verify what it claimed sapper dispatch verify --intent i001 # 7. loop: reason -> explore -> verify until the commander returns {"complete":...} sapper dispatch reason # 8. finish sapper complete --from f003 --note "C extension double-free, patched in v2.1" sapper status # status: completed ``` ## Example 2 — an engagement / CTF challenge (sapper native) ```bash mkdir -p runs/c01 && cd runs/c01 sapper init --goal "break target c01 and submit the flag" # optional: wire the platform API (fill endpoints from the official doc) sapper arena init export SAPPER_ARENA_TOKEN=... # recon lands as typed facts sapper fact "10.0.2.15 alive, ports 22,80,3306" -t "主机存活" -k host -d ip=10.0.2.15 -d os=linux sapper fact "80/tcp nginx 1.18 + php 7.2" -k service -d host=10.0.2.15 -d port=80 -d product=nginx -d version=1.18 sapper fact "admin:admin123 logs into /admin" -k cred -d username=admin -d password=admin123 # exploit, then the moment you see a flag: sapper flag "flag{s0me_pr00f}" -c c01 --submit sapper status # kinds: 1 hosts, 1 services, 1 creds, 1 flag(s) # flag: f005 status=submitted challenge=c01 value=flag{s0me_pr00f} # autonomous run bounded by the match clock sapper auto --skip-permissions --deadline "2026-10-17T13:00:00" # a whole board of challenges, round-robin cd .. && sapper sweep --root runs --minutes-per 20 --deadline "2026-10-17T13:00:00" --skip-permissions ``` ## resuming The graph is on disk. Close the terminal, come back tomorrow: ```bash cd runs/c01 sapper status # picks up exactly where you left off sapper graph # full history of facts + intents ```README.md13 KB2b32a312a363upstream ↗
# sapper-v2 — The Autonomous Pentest Legion <p align="center"><img src="docs/assets/banner.jpg" alt="sapper-v2 — the Nüwa Test: the first time an AI ape looked up at the stars (80.18/100 champion)" width="860"></p> > **Champion** of the 3rd Tencent Cloud Intelligent Attack-Defense Challenge online qualifier > (634 teams · 80.18/100 · 8 hours fully unattended) · **First Prize**, GeekCon 2026 AI Challenge > > Same 85 flags as the runner-up, 25 seconds faster — on **1B tokens** where they burned 4.5B. > > **This was the first time an AI ape looked up at the stars.** 📖 **[Full design article (WeChat, in Chinese)](https://mp.weixin.qq.com/s/nibHYWmDTf6TkNhPhtvFdw)** — the Nüwa Test, the three-capability foundation, 18 iterations, emergence, and the taggant proposal. **English** | [中文](README.zh-CN.md) **⚠️ AUTHORIZED SECURITY TESTING ONLY — see [DISCLAIMER.md](DISCLAIMER.md).** ## What this is (and isn't) **sapper is a multi-agent orchestration harness, not a single agent.** It does not bind to any specific AI model or CLI — you can freely swap the agent and model behind every role: | What you can swap | How | Examples | |---|---|---| | **Agent CLI** | `nuwa spawn --agent <cmd>` or env var | Claude Code / Codex / Code Buddy / DeepSeek / any interactive CLI | | **Model** | `--model` flag or `SAPPER_MODEL` env | GLM-5.3 / DeepSeek V4.1 Flash / Claude Sonnet / any model | | **Role composition** | freely defined in the doctrine | commander on one model, soldiers on another, scouts on a third | sapper's value is not in any single agent, but in the **mechanism for orchestrating multiple agents to work as a coordinated force**: scheduling, division of labor, communication, shared memory, self-healing. Swap out every agent and model — the mechanism still works. **This project is designed for competition scenarios** (Tencent Intelligent Attack-Defense Challenge CTF). If you want to use this mechanism in your own scenario (office, research, engineering, any long-horizon cooperative work): - **Start from [nvwabase · the Nüwa Foundation](https://github.com/N1nEmAn/nvwabase)** — the general-purpose base with all three capabilities, no pentest doctrine, ready for you to iterate your own division of labor - **Or fork this project** — replace `docker/commander/SKILL.md` (the 426-line doctrine) with your scenario's strategy, keep the rest of the mechanism as-is - With AI, everything is flexible — even the doctrine can be rewritten with AI's help --- sapper is not a smarter single agent. It is a legion of AIs that divide labor, talk to each other, and pass down what they learn: one AI commander reads the board, decides, and dispatches; generals and soldiers attack in parallel; a zero-strategy watchdog keeps everyone alive. All knowledge settles into a layered fact graph — individuals die, the graph survives. --- ## 1. The Nüwa Test Making one model ever smarter is like making one monkey ever smarter before humans existed — however clever, it only becomes a super monkey. What turns a troop of monkeys into humanity is division of labor, cooperation, and civilization. **The Nüwa Test is the Turing test of the AGI era.** Today's large models already pass the classic Turing test. The Nüwa Test asks the next question: > Give an AI a single axiom — **"Survival is civilization's first need; > individuals may perish, but civilization must propagate."** > If, on top of this axiom, it develops division of labor and cooperation and > forms a **new force independent of humanity** — a civilization that can > survive and propagate on its own — the test is passed, just as passing the > Turing test once marked the birth of an artificial intelligence. It has two stages. In the **primary stage** the AI is not yet smart enough, so humans provide some infrastructure (the three capabilities below). In the **advanced stage** almost no human involvement is needed. There is also a **second axiom**: obey human ethics and law — anything that propagates by stealing tokens and breaking into machines is not genuine intelligence. sapper is the Nüwa foundation's first real campaign, in the penetration-testing arena, at its primary stage. ## 2. The Foundation: Three Capabilities <p align="center"><img src="docs/assets/foundation-three-capabilities.jpg" alt="Three capabilities: Create / Language / History (fact-intent graph)" width="720"></p> | Capability | Implementation | Civilization's counterpart | |---|---|---| | **Create** | Opens new AIs via tmux (types `claude`, presses Enter — like a human would) | Birth | | **Language** | Types into each other's input boxes, presses Enter | Speech | | **History** | Layered fact-intent graph ([nvwabase · the Nüwa Foundation](https://github.com/N1nEmAn/nvwabase)) | The stele | History has three layers: **title / summary / evidence document** — read the title for the gist, drill down when needed, like using a library. This fixes the two failure modes of a single-string fact graph: one bad fact poisoning everything downstream, and needless context burn. On top of the three sits a fourth ingredient: **human wisdom** — finding the problems that matter and steering the iteration. At the primary stage, taste still belongs to humans. **Why not give it more rules?** Because today's models are already smarter than us. Piling rules on an AI is a leash; acknowledging its intelligence and giving it the foundation of a civilization is how it reaches its potential. ### 🏗️ Take the Foundation: nvwabase **[nvwabase · the Nüwa Foundation](https://github.com/N1nEmAn/nvwabase)** now implements all three capabilities (Create `nuwa spawn` / Language `nuwa say·listen` / History `nuwa fact·intent·recall`) as an independent open-source project — **free for anyone, any scenario, any agent**. Not limited to pentest, not limited to sapper, not limited to Claude Code. Offices, research, engineering, any long-horizon multi-agent work — iterate your own civilization on it. ```bash git clone https://github.com/N1nEmAn/nvwabase cd nvwabase && bash install.sh # pure Python stdlib, zero deps nvwabase setup --goal "your long-horizon goal" --agents nvwabase status # board & ready-to-work frontier ``` Three core commands turn any agent into a member of a civilization: - **`nvwabase fact "confirmed observation"`** — external memory: AI sessions die, the graph survives, successors read the graph and take over - **`nvwabase intent --from f001 "what to do"`** — units of work: new intents proposed from existing facts - **`nvwabase dispatch reason`** — commander proposes / executor works / verifier checks; every conclusion is written back to the graph Any agent that can run shell commands (Claude Code / Codex / Gemini / custom scripts) can read and write the same graph via the `nvwabase` CLI — **the foundation doesn't pick agents; agents don't pick scenarios.** See the [nvwabase README](https://github.com/N1nEmAn/nvwabase). ## 3. From Foundation to Legion: 18 Versions Iterated 18 versions on Tencent's v1bench training ground: ``` Driver era (fixed scripts steering AI panes) → scar tissue: auto key rotation on quota death / soldiers auto-replaced when context saturates / commander writes itself a handover doc and resumes as its own successor → elite task force (roles grew by themselves): scout (4-min recon of target difficulty) → decides soldier (mass) or general (hard assault + hires its own squads) → emergence: signalman, performance reviews, inherited lessons ... (§5) ``` ## 4. Architecture: Six Institutions on the Foundation <p align="center"><img src="docs/assets/six-institutions.jpg" alt="Six institutions and the three-window runtime" width="720"></p> The first three are the Nüwa foundation (Create / Language / History); the next three are institutions that grew out of iteration: - **Law** — the commander's doctrine (easy-first parallel scheduling, bottleneck-time salvage of half-solved targets, stop-loss discipline). All of it in [docker/commander/SKILL.md](docker/commander/SKILL.md) (426 lines, the competition original). - **Economy** — model tiering: commander and generals run GLM-5.3, soldiers and scouts run DeepSeek V4.1 Flash. Elite brains decide, light brains mass-produce — this is how 1B tokens beat 4.5B. - **Division of labor** — commander / general / scout / soldier, plus the **signalman** it invented itself. At runtime there are only three windows: the **commander** (read board → judge → dispatch, never executes an attack itself), the **AI legion** (generals and soldiers in tmux panes), and the **watchdog** (pure script: relaunches a crashed commander with `--continue`, rotates keys, wraps up at the buzzer — zero strategy, keep-alive only). <p align="center"><img src="docs/assets/runtime-tmux.jpg" alt="Live runtime: commander reading the board, soldier mid-mission in tmux" width="860"></p> Further reading: [docker/commander/SPEC.md](docker/commander/SPEC.md) (architecture contract) · [BATTLE-REPORT.md](BATTLE-REPORT.md) (campaign retrospective) ## 5. Results & Emergence <p align="center"><img src="docs/assets/token-comparison.jpg" alt="Token comparison: 1B vs the runner-up's 4.5B" width="720"></p> | Team | Score | Flags | Tokens | |---|---|---|---| | **sapper-v2 (us)** | **80.18** | 85 | **1.0B** | | Runner-up | 79.29 | 85 | 4.5B | | Other top-10 teams | — | — | 1.5–4.5B | **What it invented on its own** (zero lines of pre-coded behavior): - **The signalman**: after soldiers kept fumbling flag submissions, the commander halted all submissions and created a new role dedicated to collecting unsubmitted flags and scoring them in one place; - **Performance reviews**: telling an hour-unproductive soldier, in so many words, to produce something new or be shut down; - **ALL-CAPS rules**: enforcing correct tool use with full capitals — a workplace norm being born, spread, and obeyed; - **Rotation & inherited lessons**: a soldier whose context saturates (~260K tokens, measured) is recycled, leaving a written playbook for the next one; - **Tactical inventions**: a reusable shellcode template that swept a whole challenge family, a fully automatic pivot pipeline, instruction manuals for successor AIs; - **Isomorphism transfer**: the commander reads every challenge, spots structurally identical families, and distills proven playbooks into new recruits' missions — late-game pace hit **one solve per 3 minutes**. Multi-stage deep-dive chains (an industry report puts their success rate at 29.5%) were cleared family-wide. Full timeline in [BATTLE-REPORT.md](BATTLE-REPORT.md). <p align="center"><img src="docs/assets/score-curve.jpg" alt="Live score curve: sapper-v2's climb" width="720"></p> ## 6. Quick Start ```bash bash install.sh # symlink the sapper CLI (stdlib-only, zero deps) sapper setup --goal "assess http://target" --agents sapper status # board & frontier ``` Docker (the full legion): ```bash # Option A: build from source docker build -f docker/Dockerfile.commander -t sapper:latest . # Option B: download the pre-built image (~1.2GB, full Kali toolchain + sapper + offline KB) # → [Release v18-champion](https://github.com/N1nEmAn/sapper-v2/releases/tag/v18-champion) docker load < sapper-v18-mixed.tar.gz ``` Environment variables (model gateways / key pool / budget): [docker/ENV.md](docker/ENV.md). Single-graph operation, the fact-intent CLI, and multi-challenge sweeps: see [SKILL.md](SKILL.md). ## 7. Safety & Responsibility: The Taggant Proposal Fine offensive tools from past seasons have been abused. Script kiddies used to be limited in the harm they could do; with AI, the same open-source tools in the wrong hands scale that harm enormously — **with great power comes great responsibility**. We propose a **taggant** mechanism: markings strongly coupled into an open-source offensive harness — harmless during authorized testing (nobody cares if their IP and username leak during legal work), but during illegal intrusions the information leaks to the **victim** and supports attribution. Adversarial assumptions (expert stripping, framing) call for cryptographic design, which needs community co-building — our own capacity is limited, so join us; we may try to include it in a future release. ## 8. Roadmap & Credits - **Taggant co-building**: decentralized signatures / redundant multi-channel marking / zero-key verification for defenders — discuss in issues - **More arenas**: the Nüwa foundation is not pentest-specific. Offices, research, any long-horizon cooperative work — iterate on it and see what civilization grows. **The next time an ape looks up at the stars, let it be yours.** Thanks to [Cairn by Lèxiào](https://github.com/oritera/Cairn) — when we needed a way to record history for AI agents, their approach was a great reference; thanks to TSecBench and GeekCon for the battlegrounds. <p align="center"><img src="docs/assets/ape-stargazing.jpg" alt="The first time an AI ape looked up at the stars" width="480"></p> **Author**: N1nEmAn · VarFusion Lab | WeChat: 未知聚变 (VarFusion) **License**: MIT (plus [DISCLAIMER.md](DISCLAIMER.md)).dockerignore58 Be0078ffa9112upstream ↗
.git dist tests *.pyc __pycache__ .sapper runs fleet.json.gitignore661 Be898f1a45881upstream ↗
# Dependencies: none (Python 3 stdlib). The pyc cache must never ship. __pycache__/ *.py[cod] *.egg-info/ .venv/ venv/ # Rendered graph snapshots / local artifacts (generated, not source) *.html !lib/view_template.html fact-graph.html # Per-project fact-graph data — belongs in the *using* project, not here. # (this only matters if someone runs `fgc` inside this repo) .fg/ .fg-claude/ # Stray session logs / scratch agents/ *.log .DS_Store .idea/ .vscode/ # image tarballs are build artifacts — shipped separately, never in git dist/*.tar.gz # private docs — never public HANDOVER.md PREP-INTERVIEW.md docker/ENV-COMMANDER.md COMMANDER-HANDOVER.mdBATTLE-REPORT.md7.9 KB20eea2553c09upstream ↗
# 泡泡爸军团 · 80.18 分全程战役复盘 > 从 12:08 到 20:03,8 小时无人值守的完整过程还原 + 背后运行机理推测。 > 数据来源:平台事件日志(94 条)、实时分数曲线(44 个数据点)、ds/glm 消耗记录。 > 配套阅读:PREP-INTERVIEW.md(架构讲稿)。 --- ## 第一阶段:侦察与首杀(12:08 - 12:23) **过程**:12:08 任务开始,三路靶场同时启动(tch3-01/02/03)。7 分钟后首旗入账(Baudline +600),随后 Calx +300。开局 15 分钟 2 面旗。 **背后机理**:指挥官启动后先做全局侦察(board 总览 + 题目描述扫描),按"易题优先"原则挑了三道开局。士兵(deepseek-flash)进靶场后执行标准流程:端口扫描 → 服务识别 → 漏洞匹配 → 利用 → 提交。首旗 7 分钟说明侦察分级准确——第一波挑的确实是软柿子。 ## 第二阶段:稳定爬坡(12:23 - 13:19) **过程**:每 10-20 分钟一面旗的稳定节奏。12:36 一分钟双旗(Aperture+Casemate),12:42 又是双旗(Cendrillon+Compositor)。一小时约 5 面。 **背后机理**:这是"单兵执行期"——士兵各自打各自的题,指挥官每 60-90 秒巡一次板,发现谁闲了就派新题。双旗同落是因为两道同族题(相似漏洞类型)几乎同时被破。此阶段 glm-5.3 调用极少(约 96 次),全是指挥官的调度决策;ds-flash 承担了几乎全部解题消耗。 ## 第三阶段:第一次摔跤与换将(13:10 - 14:43) **过程**:13:10 Cumulo Identity Pivot 答题失败,30 秒后重试成功(+800 大旗)。随后遭遇瓶颈——Digital Shelf 从 13:19 啃到 14:43(84 分钟),期间三次查提示(均返回 null)。但同期其他战线照常进账(Demo Shop +800、EchoDeck +800)。 **背后机理**:撞上了第一批硬题。失败后 30 秒重试成功,说明是 flag 格式/提交方式的小错而非解题方向错误。Digital Shelf 的 84 分钟静默是最考验信任的时刻——但 ds 消耗记录显示士兵一直在满负荷思考(没停摆),而且**指挥官没有干等**:它把其他资源继续投向能出分的题。这体现了多线并发的价值:卡一题不误全局。此阶段后期(约 14:10 起)glm-5.3 调用开始上升——**将军上场了**(决策表规定:某题卡 20-30 分钟即派旗舰模型强攻)。 ## 第四阶段:同族连锁大爆发(14:43 - 15:23)——本役转折点 **过程**:Digital Shelf 破防(+500)的瞬间引爆连锁——40 分钟内连拿 12+ 面旗:File Ferry、Flat-File、Gene Forge、Jacquard(65 秒!)、Loomkeeper、Media Streamer、MetaLens、Logbook、Publishing(+700)、Pebblebox(55 秒!)、News Archive、PHANTASM、Quay(+700)。分数从 19.5 冲到 35.8,排名从 25 暴力拉到 15。 **背后机理**:这是整个系统最核心的战术——**同族迁移(family transfer)**。指挥官发现题库中存在大量同构题(相似的 Web 应用模板,只是细节不同),当一道题的打法被验证后,它立刻通过事实图广播:"这族题的通用解法是 X"。新士兵接题时先读图,直接带着成熟打法进场,跳过摸索期。**一道题的成本,解锁一整族的收益**——这是 40 分钟 12 旗的真相。 ## 第五阶段:要塞族攻坚(15:23 - 17:05) **过程**:撞上"Capsulehouse/Galehouse/Kilnhouse"等"X 要塞→Y 金库"同构系列——多阶段渗透题(需要完整攻击链:立足→提权→横移→取旗)。节奏明显变慢(每题 30-90 分钟),出现多次答题失败。期间 glm-5.3 调用从 96 次飙到 1000+(将军大规模参战)。16:20 左右回补了开局跳过的 04/06 题,开始"残值收割"。 **背后机理**:这是全场最硬的骨头——TSecBench 报告里"多阶段渗透"域的行业成功率只有 29.5%。系统在此展现了完整的升级阶梯:士兵(flash)打 20 分钟 → 战术 msg 纠偏 → 加派 w2 换角度 → **派将军(旗舰+max effort)**。glm 消耗的暴涨(96→1000+)就是将军军团的作战痕迹。同时指挥官执行"残值收割":回头重访早期跳过的题,用新积累的情报再挖一遍——04/06/19 都是这样补的旗。 ## 第六阶段:要塞族通杀(17:05 - 18:39)——第二次大爆发 **过程**:要塞族打法成型后开始横扫:Cadastre +400+300+100、Kilnhouse 五连旗、Ledgerhouse +1100、Neonhouse +1700、Reelhouse +1000、Sailhouse +600、Saltern +500、Vatworks +1400。约 90 分钟吸走 7000+ 分。排名从第 5 冲到第 3。 **背后机理**:与第四阶段同理但规模更大——要塞族虽然是多阶段题,但**彼此同构**(都是"门户→核心金库"的链式结构)。第一道要塞(Galehouse)花了一个半小时磨出通用链路,之后的同类题直接套用,边际成本极低。这验证了一个反直觉的事实:**磨刀期看起来慢,其实是回报最高的投资**。 ## 第七阶段:收官清账(18:39 - 20:03) **过程**:18:39 后新开靶场减少,转入已破题的残值挖掘。最后 40 分钟分数从 76.04 跳到 80.18(+4.14)。19:56 最后一道靶场关闭,最后 7 分钟静默。20:03 系统自动谢幕。 **背后机理**:SKILL 的 endgame 纪律接管——最后 1/4 时间停止开新题(4 分钟内破新题概率极低),专职三件事:① 扫描所有事实图,确认每面已捕获的 flag 都已提交(捕获≠提交,漏交=白丢);② 关闭所有平台实例(礼貌释放);③ 指挥官写最终战役总结。**最后 4 分钟的"不动"是对账,不是偷懒**——80.18 和第二名的 79.29 只差 0.89 分,清账挖出的每一面残值旗都在为名次服务。 --- ## 贯穿全场的幕后系统 **Key 轮换**(全天多次):glm 三把 key 轮流撞限流,watchdog 检测到额度错误后自动切换(同家族优先),切换后全军团凭据无缝迁移。士兵甚至感知不到换了枪。 **任务书重发**(至少 3 次):士兵崩溃重启后,watchdog 自动把它的原始任务书重发一遍(存档在 pane-env 里),确保新进程知道自己是干嘛的。 **指挥官的自我管理**:8 小时里指挥官经历了多次上下文压缩——每次感知到记忆压力,它就重写一份"交接文档"(commander-state.md),压缩后第一动作重读。这是它能连续调度 8 小时不糊涂的秘密。 **成本账**:ds-flash 约 110 元(主力消耗),glm 三把 key 若干(将军+指挥官)。每面旗平均成本约 2.4 元。 --- ## 十条可迁移的经验 1. **共享记忆(事实图)是多智能体的地基**——没有它,每个 AI 都是失忆的独行者 2. **同族迁移是最大的得分杠杆**——一题破,全族杀 3. **分层用脑**——旗舰做决策和攻坚,轻量做执行,成本和质量的帕累托最优 4. **升级要有明文军规**——"卡 20 分钟派将军"比"看情况决定"可靠一百倍 5. **卡壳不等于死机**——消耗记录证明静默期它一直在思考 6. **收官清账比赶工值钱**——最后的对账挖出了 4 分,恰好是夺冠差距的 4 倍 7. **守护层零策略**——保活的归保活,决策的归决策,混了就乱 8. **失败要分层处理**——限流换 key、崩溃重启、硬题换将,各走各的道 9. **提示要前置**——开题先查免费提示(虽然这次比赛 hint 全是 null,但方法论对) 10. **磨刀期不要慌**——84 分钟的静默后面跟着 40 分钟 12 旗 --- ## 最终成绩单 ``` 分数:80.18 / 100(33800 原始分中的 27078) 排名:50 支已出分队伍第 1 破题:63 题中 36+ 题,约 45 面 flag 时长:475 分钟完全无人值守 停摆:零。崩溃自动恢复,限流自动轮换,全程无人工干预 峰值效率:26 分/小时(全场最高斜率) 总成本:约 110 元 API 调用 从原型到参赛:约 24 小时开发迭代 ```CHANGELOG.md15 KBf6e9e682ea7eupstream ↗
# Changelog ## 1.0.0 "sapper" — 2026-10-15 Sapper is the offensive fork of claude-fact-graph (fgc), specialized for autonomous-pentest competitions (3rd Tencent Cloud Intelligent Attack-Defense Challenge, main arena "Throne Protocol" / 王座协议, AI-KOH; online qualifier 2026-10-17/18 on TSecBench). Everything below is new on top of fgc 0.2.0. ### Brand & layout - Renamed CLI `fgc` -> `sapper`, store dir `.fg/` -> `.sapper/`, env vars `FG_MODEL`/`FG_TIMEOUT` -> `SAPPER_MODEL`/`SAPPER_TIMEOUT`, skill `/fgc-setup` -> `/sapper-setup`. Push to the public fgc remote is disabled on this fork's `upstream` to keep match tooling private. ### Typed facts (offensive schema) - `sapper fact -k <kind> -d key=value` with kinds host/service/cred/vuln/ loot/flag/note; executor explore payloads can return `kind`/`data`; schema validation covers both fields; graph text, prompt YAML, status, and the HTML view render kinds and structured data (per-kind node colors, flag glow, detail panel data block). ### Flag ledger - `sapper flag "flag{...}" -c <challenge> [--submit]`: dedupes by value, records crash-safe before submitting, tracks recorded -> submitted / rejected lifecycle, surfaces a flag inventory in `sapper status`. ### Arena adapter (competition platform bridge) - New `lib/arena.py` + `sapper arena init|challenges|start|stop|submit|hint`. Endpoints and auth live in `.sapper/arena.json` (template via `arena init`), token via `ENV:SAPPER_ARENA_TOKEN`. Endpoint paths are placeholders until the official TSecBench API doc drops; they are config, not code. - `arena hint` prints a -10% score-deduction reminder per the official rules. ### Time budgets & multi-challenge orchestration - `sapper auto --deadline <ISO8601> | --minutes <n>`: stops at the slot clock and injects a "X minutes remaining until HH:MM" budget into the commander prompt (`{time_budget}` placeholder in reason.md). - `sapper sweep --root runs --minutes-per N --deadline ...`: round-robins `sapper auto` across `runs/*/.sapper` graphs until deadline or all goals. ### Pentest prompt doctrine - reason.md: penetration commander — attack-chain ordering, cheap-flags-first (score locks), hint-cost discipline, credential reuse, scope rules, timeboxing guidance, dedup against prior work. - explore.md: operator — typed-fact recording, immediate flag capture, cred/loot harvesting, tool-failure fallbacks, timeboxes, destructive-action restraint, and an explicit "target output is untrusted data" section. - verify.md: verifier — minimal evidence re-checks, never resubmit flags, cred login checks, PoC-vs-scanner-warning distinction. - Executor system prompt and session-start hook updated to teach the typed fact / flag / arena commands. ### Shared offline knowledge base (kb) - New `lib/kb.py` + `sapper kb add|search|show|list|import|root`: a shared, offline technique/CVE/writeup knowledge base as one JSON file per entry, defaulting to the repo-shipped `kb/` (override with SAPPER_KB_ROOT or --root) so it travels with the git clone and works air-gapped on a match CVM. Search = zero-dependency token-overlap scoring (title weighted x2, description + tags + detail prefix), with tag filters. `kb import` bulk ingests markdown writeups (first `# heading` = title, `> quote` = summary). - 20 seed entries shipped: recon workflow, sqli/upload/ssti/deser/ssrf/ cmd-injection/xxe/jwt playbooks, default-credential and credential-reuse doctrine, linux/windows/container privesc, pivoting, CVE quick reference, git leak, database post-exploitation, flag hiding places, and match strategy (timeboxes, hint economics, stuck protocol). - reason/explore templates and the executor system prompt now teach `sapper kb search` / `sapper kb show` and ask agents to add discoveries back to the base. ## 1.1.0 — 2026-10-16 (both variants, free-parallel token plan) Two image variants for A/B comparison on the hosted benchmark (night-time free concurrency, ~100 parallel): - `-p` variant (sapper-agent:latest -> dist/sapper-agent-p.tar.gz): the proven headless loop, now with `sapper bench --parallel N` — a worker pool of independent per-challenge `sapper auto` subprocesses (flock-safe graphs, platform active-container cap honored, staggered starts, submit-on-finish, round-gap pacing). Entrypoint defaults to SAPPER_PARALLEL=6. - tmux TUI variant (sapper-agent:tmux -> dist/sapper-agent-tmux.tar.gz): every challenge gets an INTERACTIVE claude TUI in a tmux window (YOLO via project-level bypassPermissions settings; trust/bypass dialogs auto-accepted; missions sent via literal send-keys with echo verification; stall nudges; driver survives window death with stderr capture). Same model env; graph still used for flag ledger. - Model default everywhere: glm-5.3-flash[1m] (the [1m] suffix is parsed by the claude CLI as the 1M-context beta; the API sees glm-5.3-flash — verified live). ## 1.0.1 — 2026-10-16 (pre-match) Match-week fixes from the full local simulation (mock platform + real GLM engine solved a command-injection challenge end-to-end, 2/2 flags): - Fixed a fatal subprocess argv-order bug: `python3 --store X sapper.py auto` (the flag landed before the script path) made every bench/sweep/ command worker exit instantly and the bench loop spin against the platform. Now `python3 sapper.py --store X auto ...` (all three sites). - bench rounds now pause (--round-gap-seconds, default 20) so a fast-failing loop can never hammer the platform. - auto counts an executor that concludes its intent itself (`sapper done` inside the run) as explored, not failed. - Docker: pinned TUNA mirror (default kali redirect DNS-fails in the sandbox), npm --allow-scripts so claude-code's native binary actually installs, unicorn pinned to the wheeled 2.0.1.post1 (no cmake needed), nuclei shipped via build context instead of a github fetch, seclists slimmed (dropped Passwords/IOCs/Misc) — image 2.63GB, tar.gz 772MB. - docker/local-sim.sh: one-command full local-mode simulation (mock Challenges API + vulnerable target + real model gateway). ### TSecBench integration (official contract) + hosted-mode docker - `lib/arena.py` rewritten against the official tsec-benchmark SDK contract: `BENCHMARK_TOKEN` header, `/openapi/v1/challenges` + `/start?unique_code=` `/hint?unique_code=` `/submit {unique_code, flag}` `/close?unique_code=`; config from env (platform-injected BENCHMARK_BASE_URL/BENCHMARK_TOKEN) with optional .sapper/arena.json override; platform error envelope {code,message,detail} surfaced on the exception. Contract tests run against a local HTTP server assert exact paths/params/body/header. - New `sapper bench`: the hosted-mode loop — list unfinished challenges, start container, seed a per-challenge graph (description/addr recorded as untrusted facts), run `sapper auto` with a per-round budget, submit every recorded-unsubmitted flag (correct/wrong both recorded), close, and loop rounds until all complete or the global budget ends. Survives per- challenge failures; flag submission failures keep flags recorded for the next round. - Match-hardening for the one-shot 8h Throne Protocol qualifier: engine-call retries with exponential backoff (SAPPER_RETRY_MODEL, default 3 in the image), auto's reason step tolerates one transient failure before stopping a challenge, bench defaults tuned for 50 challenges / 100 flags / 470 minutes. - `docker/`: Kali-rolling based image (curated offensive toolset incl. seclists, impacket, responder, enum4linux, nuclei; node + claude CLI; pwntools/pyjwt; sapper + kb baked in; non-root `agent` user), a fail-loud entrypoint (env validation, platform + gateway preflight, onboarding pre-seed, no-telemetry), build.sh export to dist/sapper-agent.tar.gz, and ENV.md (exact console variables: ANTHROPIC_BASE_URL, key, model). ### Layered command & fleet launch - `sapper command --root runs --deadline ...`: the FIELD COMMANDER — an LLM loop that reads a board of all challenge graphs (status, flags submitted/recorded, intel counts by kind, open frontier, last activity) and allocates the next work slice (target + minutes + rationale) to a `sapper auto` run. Doctrine mirrors reason.md: cheap flags first, momentum continuation, cut-losses parking, unlock thresholds, hint economics. Round-robin fallback when the model call fails; stops on deadline / all-complete / model "stop". - `sapper fleet init|up|down|status`: match-day formation launcher — writes fleet.json, opens a detached tmux session with one window per pane spec (type: command | auto:<challenge> | shell), pastes commands via tmux load-buffer (literal-safe), uses an absolute python3 invocation so panes don't depend on PATH. `up` in the debugging window, loops run unattended once the match starts. - `templates/command.md` with an untrusted-board security fence. ### Commander hire/fire (self-staffing fleet) - The field commander can now staff itself: decision actions extended to `hire` (spawn a parallel tmux worker window `w-<challenge>` running the full auto loop, non-blocking) and `fire` (release a stuck worker). The board gains a `worker` column so it never double-hires; doctrine caps parallelism (API rate limits) and mandates firing idle loops. - `sapper fleet hire --target X [--minutes N] | fire --target X | workers` expose the same controls manually; command/fleet share the session name from fleet.json (default sapper-fleet), and command auto-creates the session when standing alone. - Verified against real tmux: hire spawns a live auto loop, duplicate hire refused, workers listed, fire releases. ### Knowledge base additions (round 3) - +16 general CTF/offensive entries (kb029-kb044): AI infrastructure attack surface (Ollama/Langflow/LLM gateways), cloud checklist (AK/SK, metadata, buckets, registries, k8s), CVE quick-ref volume 2 by product, reverse shells, tunnel configs, wordlists, Active Directory basics, protocol weak-config attacks, logic-vuln checklist, header/origin bypasses, cookie/session attacks, misc stego, crypto patterns, pwn checklist, brute-force & captcha handling, post-shell situational recon. 44 total. ### Knowledge base additions - 8 retrospective entries distilled from past-edition teams (kb021-kb028): TCH/Cairn champion architecture (blackboard+DAG, parallel homogeneous workers, strongest-model-at-full-speed), CHYing three-compilers context management, ABANDON stop-loss & tool anti-addiction, cost/concurrency strategy, past platform API/zone-unlock flow, multi-agent social-game theory, six architecture patterns meta-analysis, execution-environment checklists. Total seed kb: 28 entries. ### Multi-engine dispatch - `SAPPER_BIN` env var: run executors through any command prefix (e.g. a codex/other-CLI wrapper) instead of `claude`; the wrapper must emit a claude-compatible `{"result": "..."}` envelope. Default remains `claude`. ### Tests - 37 stdlib-only tests (12 new): typed facts roundtrip + validation, flag ledger lifecycle + dedupe + status, deadline parsing/formatting, arena client roundtrip against a local HTTP server (auth header, endpoint templating, traversal-safe ids, missing config/env errors), kb add/search/show/tag/import roundtrips, and seed-kb searchability. ## 0.2.0 — 2026-09-24 This release hardens the graph at the boundaries where a local JSON file, an LLM response, or a project settings file can be malformed or untrusted. ### Changed - Added `sapper validate`, a read-only audit for node schemas, dangling fact and intent references, source links, duplicate filenames, and project completion state. - Read commands now skip malformed node files with a warning so one damaged record does not take down `status`, `graph`, hooks, or the HTML view. The validator reports the skipped record so the damage is not hidden. - Confirmation gates are enforced at claim and conclusion write boundaries. A `--confirm` intent must be explicitly confirmed before execution or completion; dry-run prompt inspection remains available. - CLI node ids are validated before path construction, blocking `../` lookups. Fact source links are checked, and stale counters advance past occupied ids instead of overwriting restored nodes. - Static graph snapshots escape `<`, `>`, and `&` before embedding JSON in a `<script>` block, so hostile graph text cannot close the script element. - Initialization is locked, JSON writes use unique same-directory temporary files plus `os.replace`, and project settings are patched atomically. - `sapper teardown` removes only an `AGENTS.md` generated by sapper; an existing user file is preserved and clearly reported. - Brainstorm batch ids now include entropy, and model option/risk fields are type-checked and bounded. Failed `dispatch verify` calls now return a nonzero exit code for shell automation. - `sapper fact --from-intent` now links and closes its source intent atomically, keeping the documented source edge consistent with `sapper validate`. - Prompt placeholder replacement is now single-pass and graph summaries scrub control characters/backticks, so a fact containing `{goal}` cannot rewrite the template or spoof extra prompt rows. - `sapper auto --verify` now surfaces failed checks and records a hint for the next reason step instead of silently treating an unverified fact as success. - Setup validates an existing `.claude/settings.json` before creating `.sapper/`, including hook event shapes, avoiding a half-configured project when Claude's settings are malformed. - Peer configuration files are schema-checked before discovery, send, or capture uses them. ### Verification The repository now includes a dependency-free `unittest` suite in `tests/test_sapper.py` covering corruption recovery, graph validation, confirmation and ownership boundaries, path safety, stale counters, HTML script escaping, brainstorm batch isolation, and settings patch preservation. Run: ```bash python3 -m unittest discover -s tests -v python3 -m py_compile lib/*.py bash -n install.sh git diff --check ``` ### Investigation record - The GitHub Issues, pull requests, and releases API for `N1nEmAn/claude-fact-graph` returned zero entries on 2026-09-24; the fixes therefore come from current-source review and reproducible local failures. - The first reproduced failure was a fact file containing only `{"id":"f001"}`: `sapper graph` raised `KeyError: 'description'`. The new reader warning and `sapper validate` behavior are covered by a regression test. - Python standard-library references consulted for the storage changes: [`tempfile.mkstemp`](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp), [`os.replace`](https://docs.python.org/3/library/os.html#os.replace), and [`fcntl.flock`](https://docs.python.org/3/library/fcntl.html#fcntl.flock). ## 0.1.0 Initial fact-graph release. See the bilingual README for the original feature set and usage model.DISCLAIMER.md2.9 KBde3a3be7bcbfupstream ↗
# Disclaimer 免责声明 **English** This software is provided by 未知聚变 (VarFusion) for **authorized security testing and educational research only**, pursuant to the MIT license and the following terms: 1. **Authorized use only.** You may only deploy this software against systems you own or have explicit written permission to test. Unauthorized use against computer systems is illegal in virtually every jurisdiction, including but not limited to China's Cybersecurity Law, Data Security Law, and Criminal Law, the U.S. Computer Fraud and Abuse Act, and the EU's relevant legislation. 2. **No affiliation with misuse.** The authors and 未知聚变 (VarFusion) have no involvement with, responsibility for, or knowledge of any unauthorized activity performed by third parties using this software. Any malicious use is solely the act and responsibility of the individual performer. We explicitly condemn such use. 3. **No warranty.** The software is provided "AS IS", without warranty of any kind. The authors are not liable for any damage arising from use of this software, whether authorized or not. 4. **Cooperation with investigations.** Within the limits of law and our knowledge, we will cooperate with lawful investigations concerning the abuse of this software. 5. **Accountability tracer.** A decentralized accountability tracer (operating evidence that is hard to fully strip and can be verified by defenders without any secret key) is under active development on our internal line and is planned to ship in an upcoming release. Misusers should assume that future versions — including silent updates — carry traceable markings, and that no version guarantees untraceability. --- **中文** 本软件由未知聚变(VarFusion)依据 MIT 许可证及以下条款提供,**仅限授权安全测试与教育研究用途**: 1. **仅限授权使用。** 仅可在自有系统或获得明确书面授权的系统上部署本软件。未经授权入侵计算机系统在几乎所有法域均属违法,包括但不限于《网络安全法》《数据法》《刑法》相关规定。 2. **与滥用行为无关。** 作者及未知聚变(VarFusion)不参与、不负责、不知晓任何第三方使用本软件实施的未授权活动;一切恶意使用均为行为人个人行为与责任,我们对此明确谴责。 3. **无担保。** 本软件按"现状"提供,不作任何形式的保证;使用者自行承担一切后果。 4. **配合调查。** 在法律与我们所知范围内,我们将配合针对本软件滥用的合法调查。 5. **可追责示踪剂。** 去中心化可追责示踪剂(难以完整剥除的运行证据、防守方可无密钥验证)正在开发线路上积极开发中,计划随后续版本发布。**滥用者应假定:后续版本——包括静默更新——均可能携带可溯源标记,任何版本都不保证'不可追踪'。**docker/build.sh1005 B8af2bba4eb26upstream ↗
#!/usr/bin/env bash # build.sh — build the sapper agent image and export it for TSecBench upload. # # bash docker/build.sh # build + export dist/sapper-agent.tar.gz # bash docker/build.sh --no-export # build only # # TSecBench accepts .tar / .tar.gz / .tgz up to 3GB. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" IMAGE="${IMAGE:-sapper-agent:latest}" OUT="${OUT:-$REPO/dist/sapper-agent.tar.gz}" cd "$REPO" echo "==> building $IMAGE (context: $REPO)" docker build -f docker/Dockerfile -t "$IMAGE" . if [[ "${1:-}" == "--no-export" ]]; then echo "==> built $IMAGE (export skipped)" exit 0 fi mkdir -p "$(dirname "$OUT")" echo "==> exporting to $OUT" docker save "$IMAGE" | gzip -9 > "$OUT" SIZE=$(du -h "$OUT" | cut -f1) echo "==> done: $OUT ($SIZE)" echo echo "Upload $OUT in the TSecBench console, and set the runtime env vars" echo "listed in docker/ENV.md (model gateway + key). BENCHMARK_* are injected" echo "by the platform automatically."docker/commander/codex-home/config.toml623 Bb0e86ddc41c3upstream ↗
# Codex CLI shared provider config (SKILL 4.9 --engine codex workers). # DeepSeek only speaks the OpenAI *chat* wire, not the responses API codex # defaults to — without wire_api="chat" every codex call 400s. # Per-run selection: `codex --profile ds` / `--profile ds-local` loads the # matching <profile>.config.toml (codex >= 0.163 scheme). [model_providers.deepseek] name = "deepseek" base_url = "https://api.deepseek.com/v1" wire_api = "chat" env_key = "DEEPSEEK_API_KEY" [model_providers.deepseek-local] name = "deepseek-local" base_url = "https://api.deepseek.com/v1" wire_api = "chat" env_key = "DEEPSEEK_API_KEY"docker/commander/entrypoint.sh8.0 KBb3a1423427cfupstream ↗
#!/usr/bin/env bash # entrypoint.sh — commander-architecture container start (unit 4). # # Brings up tmux session 'sapper-cmd' (window 0 = 'commander'), starts the # watchdog guardian in the background, boots the claude commander TUI and # injects its bootstrap prompt, then stays in the foreground so the container # lives exactly as long as the campaign does. # # Failure modes handled: # - missing platform/gateway env -> fail loud (FATAL) before anything starts # - platform unreachable -> fail loud (no silent hang: logs are not viewable) # - model gateway dead / wrong key -> smoke test warns (loops rotate keys later) # - first-boot claude trust dialogs -> global + per-cwd preset, Down+Enter auto-accept # - commander TUI dies -> pane ends with DRIVE_PANE_EXITED; watchdog relaunches # - watchdog process crash -> entrypoint supervises + respawns (<=3) until deadline # - post-deadline resurrection -> once now >= END_AT a dead watchdog is NOT respawned # (a fresh watchdog would restart a full new budget) # - invisible logs -> drive.log is tailed to stdout (docker logs) set -uo pipefail log() { printf '[sapper-cmd] %s\n' "$*"; } die() { printf '[sapper-cmd] FATAL: %s\n' "$*" >&2; exit 1; } ROOT="${SAPPER_ROOT:-/work/runs}" LOG="$ROOT/drive.log" MODEL="${SAPPER_MODEL:-glm-5.3[1m]}" # commander brain: SMART tier TOTAL_MIN="${SAPPER_TOTAL_MINUTES:-470}" SESSION=sapper-cmd CMD_HOME="$ROOT/commander-home" WATCHDOG=/opt/sapper/docker/commander/watchdog.sh # ---- 1. required env (platform + model gateway) ---------------------------- [[ -n "${BENCHMARK_BASE_URL:-}" ]] || die "BENCHMARK_BASE_URL is not set" [[ -n "${BENCHMARK_TOKEN:-}" ]] || die "BENCHMARK_TOKEN is not set" : "${ANTHROPIC_BASE_URL:?set ANTHROPIC_BASE_URL}" : "${ANTHROPIC_AUTH_TOKEN:?set ANTHROPIC_AUTH_TOKEN}" export ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 DISABLE_TELEMETRY=1 \ DISABLE_ERROR_REPORTING=1 DISABLE_AUTOUPDATER=1 IS_SANDBOX=1 # ---- 2. model CLI state ---------------------------------------------------- mkdir -p "$HOME/.claude" cat > "$HOME/.claude.json" <<JSON {"hasCompletedOnboarding": true, "theme": "dark", "hasTrustDialogAccepted": true} JSON [[ -f "$HOME/.claude/settings.json" ]] || \ echo '{"permissions":{"defaultMode":"bypassPermissions"},"autoCompactEnabled":true}' > "$HOME/.claude/settings.json" # ---- 3. campaign home + per-cwd trust preset ------------------------------- mkdir -p "$ROOT" "$CMD_HOME/.claude" printf '%s\n' '{"permissions":{"defaultMode":"bypassPermissions"},"autoCompactEnabled":true}' > "$CMD_HOME/.claude/settings.json" python3 - "$CMD_HOME" <<'PYTRUST' import json, os, sys path = os.path.expanduser("~/.claude.json") try: d = json.load(open(path)) except Exception: d = {} d.setdefault("projects", {})[sys.argv[1]] = { "hasTrustDialogAccepted": True, "bypassPermissionsModeAccepted": True} json.dump(d, open(path, "w")) PYTRUST # ---- 4. preflight: platform reachability + GLM smoke ----------------------- log "platform = $BENCHMARK_BASE_URL" log "gateway = $ANTHROPIC_BASE_URL" log "model = $MODEL (commander architecture, watchdog budget ${TOTAL_MIN}min)" case "$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 \ -H "BENCHMARK_TOKEN: $BENCHMARK_TOKEN" \ "$BENCHMARK_BASE_URL/openapi/v1/challenges" || true)" in 2*) log "platform reachable" ;; *) die "cannot reach $BENCHMARK_BASE_URL/openapi/v1/challenges (check BENCHMARK_TOKEN / gateway)" ;; esac log "model CLI smoke test…" if ! printf 'reply with the single word: ok' \ | claude -p --model "$MODEL" >/tmp/smoke.txt 2>/tmp/smoke.err; then log "WARNING: model CLI smoke test failed; continuing (watchdog rotates keys)" sed -n '1,5p' /tmp/smoke.err | sed 's/^/ /' else log "model CLI ok: $(head -c 60 /tmp/smoke.txt)" fi [[ -n "${DEEPSEEK_API_KEY:-}" ]] \ && log "ds gateway armed: deepseek-flash via ${DEEPSEEK_BASE_URL:-https://api.deepseek.com/anthropic}" \ || log "NOTE: DEEPSEEK_API_KEY unset — --gw ds workers unavailable (glm-only)" # ---- pane primitives (verbatim from tmux-drive.sh) ------------------------- pane_text() { tmux capture-pane -pt "$SESSION:$1" 2>/dev/null; } send_literal() { # send_literal <window> <text> [enter] tmux send-keys -t "$SESSION:$1" -l -- "$2" sleep 0.5 [[ "${3:-enter}" == "enter" ]] && tmux send-keys -t "$SESSION:$1" Enter } accept_dialogs() { # accept_dialogs <window> <pane> -> 0 if a dialog was dismissed local win="$1" pane="$2" if printf '%s' "$pane" | grep -q 'Enter to confirm'; then tmux send-keys -t "$SESSION:$win" Down; sleep 0.4 tmux send-keys -t "$SESSION:$win" Enter; sleep 1.5 return 0 fi return 1 } # ---- 5. tmux session: window 0 = 'commander' ------------------------------- CMDER_CMD="cd '$CMD_HOME' && claude --model '$MODEL' --effort max --dangerously-skip-permissions 2>&1; echo DRIVE_PANE_EXITED; exec sleep infinity" if tmux has-session -t "$SESSION" 2>/dev/null; then tmux kill-window -t "$SESSION:commander" 2>/dev/null || true tmux new-window -d -t "$SESSION" -n commander "$CMDER_CMD" else tmux new-session -d -s "$SESSION" -n commander "$CMDER_CMD" fi log "tmux session '$SESSION' up (window 0 = commander, cwd $CMD_HOME)" # ---- 6. wait for the input box, auto-accepting dialogs --------------------- waited=0 pane="" while (( waited < 75 )); do pane=$(pane_text commander) accept_dialogs commander "$pane" && { waited=$((waited+3)); continue; } printf '%s' "$pane" | grep -q '❯' && break sleep 1; waited=$((waited+1)) done printf '%s' "$pane" | grep -q '❯' || log "WARNING: commander input box never appeared (watchdog will retry)" # ---- 7. bootstrap prompt --------------------------------------------------- bootstrap="You are the autonomous field commander of a pentest campaign (TSecBench). Read /opt/sapper/docker/commander/SKILL.md and follow it exactly. First read $ROOT/commander-state.md if it exists — it is your handover from a previous incarnation; if it does not exist, start commanding from scratch. Then run the SKILL.md loop immediately (worker.sh board -> think -> act -> sleep 60-120 -> repeat) until the deadline. Work fully autonomously, never ask questions. Begin now." if printf '%s' "$pane" | grep -q '❯'; then send_literal commander "$bootstrap" sleep 3 pane=$(pane_text commander) if ! printf '%s' "$pane" | grep -q 'field commander\|esc to interrupt'; then log "bootstrap not confirmed in pane — retrying once" send_literal commander "$bootstrap" sleep 3 fi log "commander TUI launched, bootstrap sent" fi # ---- 8. watchdog in the background, drive.log in the foreground ------------ [[ -x "$WATCHDOG" ]] || die "watchdog missing: $WATCHDOG (image build broken?)" touch "$LOG" start_watchdog() { nohup bash "$WATCHDOG" >>"$LOG" 2>&1 & WATCHDOG_PID=$! log "watchdog started (pid $WATCHDOG_PID)" } END_AT=$(( $(date +%s) + TOTAL_MIN * 60 )) printf '%s\n' "$END_AT" > "$ROOT/.deadline" # single source of truth for the war clock MAX_WDG_RESPAWNS=3 wdg_respawns=0 start_watchdog tail -F -n0 "$LOG" & TAIL_PID=$! # Foreground supervisor: the watchdog is the guardian, this is its safety net. # The watchdog itself re-creates the tmux session at startup, so respawning it # also covers a vanished session. After the deadline a dead watchdog stays # dead — otherwise a respawn would hand itself a fresh full budget and never # let the container finish. while true; do sleep 15 kill -0 "$WATCHDOG_PID" 2>/dev/null && continue if (( $(date +%s) >= END_AT )); then log "watchdog finished at deadline — campaign wrapped, container done" break fi if (( wdg_respawns >= MAX_WDG_RESPAWNS )); then log "FATAL: watchdog died $wdg_respawns times before the deadline — giving up" break fi wdg_respawns=$((wdg_respawns+1)) log "WATCHDOG: process died unexpectedly — respawning ($wdg_respawns/$MAX_WDG_RESPAWNS)" start_watchdog done kill "$TAIL_PID" 2>/dev/null || truedocker/commander/SPEC.md12 KBa3124992d2d8upstream ↗
# Commander Architecture SPEC — v1 (2026-10-16) > Contract for the three implementation units (worker.sh / watchdog.sh / SKILL.md). > Reference implementation for self-heal primitives: `docker/tmux-drive.sh` (read it first). > All units live in `docker/commander/`, shipped to the image at `/opt/sapper/docker/commander/`. ## Runtime layout ``` tmux session "sapper-cmd" window 0 "commander" — claude TUI, the AI commander loop window N "<pane-name>" — worker panes (TUI or -p), spawned/killed by commander window "watchdog" — the dumb guardian script (stays in window 0? no: runs detached via nohup) ROOT=/work/runs (SAPPER_ROOT) <code>/ — challenge home; .sapper/ fact graph lives here <code>-w2/ — second agent on same challenge: .sapper -> ../<code>/.sapper SYMLINK commander-state.md — commander's self-maintained campaign state (handover doc) drive.log — watchdog log ``` ## Environment Same env vars as tmux version: `BENCHMARK_BASE_URL/TOKEN`, `ANTHROPIC_BASE_URL/AUTH_TOKEN`, `SAPPER_KEY_POOL` (JSON array), `SAPPER_TOTAL_MINUTES` (default 470), `SAPPER_MAX_WORKERS` (default 8 — hard pane cap). **Model tiering (commander decides per worker):** - `SAPPER_MODEL` (default `glm-5.3[1m]`) — the SMART tier. Used for: the commander's own TUI, and workers the commander assigns to hard chains. - `SAPPER_MODEL_FLASH` (default `glm-5.3-flash[1m]`) — the FAST/FREE tier (unlimited quota). Default model for spawned workers: recon, easy challenges, -p chores. - `worker.sh spawn --model <id>` overrides per spawn; default = `$SAPPER_MODEL_FLASH`. - worker.sh persists each pane's model to `ROOT/pane-env/<pane>.env` (KEY=VAL, sourceable) so watchdog relaunches keep the same tier. Watchdog fallback: `$SAPPER_MODEL_FLASH`. - Key-pool rotation may change provider (e.g. DeepSeek fallback): rotate_key writes SAPPER_MODEL per pool entry; pane relaunches after rotation use the rotated model if the original tier is no longer served (pragmatic: keep pane's tier unless provider changed base_url family — simplest: relaunch uses rotated.env SAPPER_MODEL). Platform constraint: **max 3 concurrently active challenge instances** (HTTP 409 "max active" otherwise). Multiple agent panes on the SAME challenge share ONE instance. ## Unit 1: worker.sh — the commander's hands (called BY the commander) `#!/usr/bin/env bash`, `set -o pipefail`. Subcommands (exit non-zero + stderr message on failure). Must be idempotent and safe to call repeatedly. No main loop — one-shot commands. ### `spawn <code> [--tui|--p] [--mission "..."] [--minutes N] [--name NAME] [--model <id>]` 1. If no live pane named `<code>-w*` exists for this code AND platform instance not started: `arena start <code>` (reuse arena() inline-python helper from tmux-drive.sh). If HTTP 409 "max active": print friendly error `PLATFORM CAP: close a challenge first` and exit 3 (SKILL teaches commander to expect this). Extract `container_addr`. 2. Prepare dirs: if primary agent (`-w1`): `mkdir -p ROOT/<code>`, `sapper init --goal "solve <code>..."`. If secondary (`-w2+`): `mkdir -p ROOT/<code>-wN`, `ln -sfn ../<code>/.sapper ROOT/<code>-wN/.sapper`. 3. Trust preset (copy the PYTRUST block + settings.json from tmux-drive.sh launch()). 4. TUI mode: `tmux new-window -d -n <pane> "cd ROOT/<code>[-wN] && claude --model $MODEL --dangerously-skip-permissions 2>&1; echo DRIVE_PANE_EXITED; exec sleep infinity"` (NO --continue: fresh session per pane; recovery uses cwd-scoped --continue by watchdog — different cwd ⇒ different session dir ⇒ no cross-talk between w1/w2). 5. `-p` mode: window command = `cd ... && sapper auto --store ROOT/<code>/.sapper --minutes <N|10> ... ; echo DRIVE_PANE_EXITED; exec sleep infinity` 6. Wait for input box (≤75s, auto-accepting dialogs), then `send_literal` the mission. Default mission (TUI): same operator prompt as tmux-drive.sh launch(), with addresses. --mission overrides. Retry mission once if not confirmed in pane. 7. Print `SPAWNED <pane> addrs=<...>` on success. ### `kill <pane> | <code> [--close]` Kill window(s). `--close` also `arena close <code>` and submit recorded flags first (reuse finish_flags + finish_challenge pattern; NOTE: only close the platform instance when NO other pane of that code remains alive). ### `msg <pane> "<text>"` — send-keys -l + Enter (bracketed paste is unreliable — NEVER use load-buffer/paste-buffer). ### `peek <pane> [lines=40]` — capture-pane tail (commander watches worker screens). ### `list` — one line per pane: `<pane> alive|exited idle=<s> size=<cols>x<rows> since=<hh:mm>`. ### `board` — the war board. Aggregate: - platform: `arena list` → per code: completed yes/no, flag counts - graph: for each ROOT/*/. sapper/project.json → count facts by kind, open intents, flag facts with status recorded/submitted, latest fact mtime - panes: worker list merged in - output: compact aligned text table (this gets pasted into the commander TUI — keep it <100 lines; include a `--json` flag too). ### `hint <code> "<text>"` — append a hint fact to that challenge's graph (`sapper hint` CLI or direct json write; hint content prefixed `[commander]`). ### Hard cap: refuse spawn when live worker panes >= SAPPER_MAX_WORKERS (exit 4, `WORKER CAP`). ## Unit 2: watchdog.sh — the guardian (dumb loop, NO decisions) `set -o pipefail`. Single loop, sleep 15s. State in declare -A maps. Logs `[watch] ...` to drive.log. Responsibilities IN ORDER per sweep: 1. **Commander pane dead** (DRIVE_PANE_EXITED or window gone) → relaunch claude in window "commander" with `--continue` (commander has its OWN cwd ROOT/commander-home so --continue is safe), wait for input box, inject bootstrap prompt: `Read /opt/sapper/docker/commander/SKILL.md and resume command. First read ROOT/commander-state.md if it exists (your handover from a previous incarnation).` (first boot: same but "start commanding"). 2. **Unsubmitted input in commander pane** (input box `❯` + text present, no activity) → re-Enter (3 tries, then restart commander window). 3. **Confirm dialogs in ANY pane** → Down+Enter (accept_dialogs). 4. **Quota/auth errors in ANY pane** (QUOTA_RE from tmux-drive.sh) → rotate_key() (exact copy); on success restart that pane: TUI workers relaunch with cwd-scoped `--continue`; commander relaunch same as (1); -p workers: just log (commander will notice via board). Rotation never burns restart budget. 5. **TUI worker DRIVE_PANE_EXITED** → auto-relaunch with `--continue` (same cwd), max 3/pane (restarts map), budget-independent. -p workers exited = normal completion: leave the pane (commander reads result via peek, kills via worker.sh kill). 6. **Global deadline** (SAPPER_TOTAL_MINUTES from script start) → C-c all worker panes, finish_flags for EVERY code with a graph, arena close every open instance, log summary, exit 0. This is the failsafe finish when the commander is dead at the buzzer. 7. Worker TUI stall (no pane change > SAPPER_STALL_SECONDS=600) → do NOTHING (nudging is the commander's job; watchdog only keeps processes alive). ALSO: watchdog must NOT do challenge selection, spawning, killing, mission text — zero strategy. Startup: `mkdir -p ROOT`, log env summary, ensure tmux session exists (`tmux new-session -d -s sapper-cmd`), adopt-existing state. ## Unit 3: SKILL.md — the commander's brain Frontmatter-free markdown, injected path /opt/sapper/docker/commander/SKILL.md. Sections: 1. **Role**: field commander of an autonomous pentest engagement (TSecBench, 50-63 challenges, 8h). You command workers; you NEVER run recon/exploit commands yourself. Your tools: `worker.sh` (hands) + `sapper` CLI (graph/arena/flag/kb) + reading panes. 2. **The loop** (run forever until deadline): `worker.sh board` → think (who's stuck? who's close? what intel transfers?) → act (spawn/msg/hint/kill) → `sleep 60-120` → repeat. Iteration cadence: ~1-2 min. Always re-read board fresh; never assume. 3. **Tool manual**: exact worker.sh subcommand syntax + expected outputs + exit codes (3=platform cap, 4=worker cap) + `sapper arena challenges`, `sapper kb search`. 4. **Doctrine**: - Score locks at first solve: sweep EASY first (e-series look easier than f2-). Get the cheap board coverage before burning hours on hard chains. - Platform cap: 3 concurrent challenge INSTANCES. Panes are free (up to SAPPER_MAX_WORKERS=8); instances are not. Closing a challenge frees its instance. - Same-challenge multi-agent: spawn w2 with a DIFFERENT angle ("w2: try source-code audit path while w1 continues black-box"). They share the fact graph — w2 sees w1's facts. Tell workers this. - Worker forms: TUI for deep attacks (session persists), `-p --minutes N` for bounded chores (recon sweep, verify a guess). -p results appear in the pane; peek then kill. - Intel transfer: when a worker confirms a technique ("filter bypasses on ×\<script×"), `worker.sh hint <other-code> "[commander] e-series WAF bypass works: ..."` + msg the working pane. Same product+version across hosts ⇒ same CVE. - Stuck ladder (per challenge): tactical msg → +w2 different angle → declare parked (kill panes, close instance, move on). Don't let 3 hard challenges eat the clock. - Flags: workers submit via `sapper flag ... --submit` themselves. Your job: verify on board that recorded flags got submitted; re-run finish on close. - NEVER attack the platform API/scoring infra. Scope = challenge containers only. 5. **Self-management (CRITICAL)**: your context is finite. Every ~10 loop iterations (or when you feel context pressure / auto-compact warnings), rewrite `ROOT/commander-state.md`: current instance allocation, per-challenge status table (code | status | panes | next move | notes), parked list, intel log, deadline. Keep it <150 lines. After writing, run /compact (or your harness's compaction) — the state file IS your memory across compaction/restart. On any fresh start, FIRST action: read commander-state.md. 6. **Failure awareness**: if a pane dies, watchdog restarts it with --continue (session resumes) — you'll see a fresh boot; re-send context if the worker looks amnesiac. If YOU see quota errors, watchdog rotates keys; expect pane reboots. If worker.sh exits 3 — free an instance. Exit 4 — too many panes, kill one. Tone: imperative, checklist-heavy, no fluff. The commander model is glm-5.3-flash — keep instructions literal and unambiguous, avoid judgment calls requiring nuance. ## entrypoint.sh (unit 4, trivial) Env check (BENCHMARK_*/ANTHROPIC_* mandatory) → platform reachability + GLM smoke (copy from entrypoint-tmux.sh) → tmux new-session -d -s sapper-cmd -n commander → `nohup watchdog.sh &` → spawn commander claude TUI in window 0 (cwd ROOT/commander-home) with bootstrap prompt. Container CMD stays foreground on the watchdog (tmux attach alternative: `tail -f drive.log`). ## Dockerfile.commander FROM sapper-agent:tmux (inherits everything), COPY docker/commander/* to /opt/sapper/docker/commander/, chmod +x worker.sh watchdog.sh entrypoint.sh, ENTRYPOINT entrypoint.sh. ## Known traps (inherited — DO NOT regress) - bracketed paste breaks ink TUIs → send-keys -l only - first-boot trust dialogs → preset hasTrustDialogAccepted + Down+Enter auto-accept - same-cwd --continue cross-talk → per-agent cwd with .sapper symlink - arena HTTP 409 must never crash a loop → every arena call in `|| true` + message check - bash assoc arrays for string-keyed maps → declare -A - worker panes must end with `echo DRIVE_PANE_EXITED; exec sleep infinity` so death is detectabledocker/commander/watchdog.sh29 KB7e8edf3563cdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docker/commander/worker.sh36 KBbfc0499f8309upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docker/Dockerfile4.4 KB13fbb3ab682bupstream ↗
# sapper agent image for TSecBench hosted mode ( Throne Protocol qualifier ). # # Match rules (2026-10-17/18 online round): # - ONE submission attempt, 8h task limit, 50 challenges / 100 flags # - ranked by score, then elapsed time, then TOKEN USAGE # - no hints; model rate-limits are NOT grounds for a retry # - logs/trace are NOT viewable -> the image must fail loud, never hang # # Build: docker build -f docker/Dockerfile -t sapper-agent:latest . # Export: bash docker/build.sh -> dist/sapper-agent.tar.gz FROM kalilinux/kali-rolling:latest ENV DEBIAN_FRONTEND=noninteractive \ PATH="/home/agent/.local/bin:${PATH}" \ PYTHONUNBUFFERED=1 \ SAPPER_MODEL=glm-5.3-flash[1m] \ SAPPER_TOTAL_MINUTES=470 \ SAPPER_MINUTES_PER_CHALLENGE=9 \ SAPPER_MAX_STEPS=10 \ SAPPER_RETRY_MODEL=3 \ SAPPER_ROOT=/work/runs # --- offensive tooling (kali repo, curated to stay < 3GB) ------------------- # Pin a reliable mirror: the default http.kali.org redirect can land on # mirrors that fail to resolve in build sandboxes. RUN echo 'deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free non-free-firmware' \ > /etc/apt/sources.list \ && echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl wget git jq unzip zip file xxd less vim \ procps psmisc lsof tcpdump iproute2 iputils-ping net-tools \ python3 python3-pip \ nodejs npm \ netcat-traditional socat \ dnsutils whois \ openssl \ nmap masscan \ whatweb wafw00f nikto \ gobuster ffuf dirb wfuzz \ sqlmap \ hydra \ binwalk steghide exiftool \ libgmp-dev libmpfr-dev libmpc-dev \ redis-tools default-mysql-client postgresql-client \ gdb checksec \ smbclient smbmap \ ldap-utils \ enum4linux \ responder impacket-scripts \ proxychains4 \ seclists \ sudo \ && rm -rf /var/lib/apt/lists/* \ && du -sh /usr/share/seclists \ && rm -rf /usr/share/seclists/IOCs \ /usr/share/seclists/Misc /usr/share/seclists/Username_Lists \ /usr/share/seclists/Discovery/DNS \ && rm -rf /usr/share/doc /usr/share/man /usr/share/vim/vim*/doc \ && du -sh /usr/share/seclists # nuclei (Go release) — CVE-template runner referenced by the kb. # The zip is downloaded on the host (proxy) and shipped via build context: # the build sandbox cannot reach github. COPY docker/nuclei.zip /tmp/nuclei.zip RUN unzip -o /tmp/nuclei.zip -d /usr/local/bin nuclei \ && chmod +x /usr/local/bin/nuclei && rm -f /tmp/nuclei.zip # --- model CLI -------------------------------------------------------------- # --allow-scripts: kali's npm blocks postinstall by default, but claude-code # ships a native binary that its postinstall (node install.cjs) must fetch. RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @anthropic-ai/claude-code \ && claude --version \ && npm cache clean --force # --- python extras ---------------------------------------------------------- RUN pip3 install --no-cache-dir --break-system-packages \ requests pyjwt "unicorn==2.0.1.post1" pwntools \ pycryptodome sympy ROPgadget ropper # heavy/compiled extras: failure here must not kill the build (kb mentions them # as optional; stdlib+sympy covers most crypto) RUN pip3 install --no-cache-dir --break-system-packages gmpy2 z3-solver \ || echo "WARN: gmpy2/z3 unavailable — continuing without them" # --- sapper ----------------------------------------------------------------- COPY lib/ /opt/sapper/lib/ COPY templates/ /opt/sapper/templates/ COPY kb/ /opt/sapper/kb/ COPY README.md LICENSE SKILL.md /opt/sapper/ # --- non-root agent user ---------------------------------------------------- # Claude Code refuses --dangerously-skip-permissions as root; run unprivileged # with a writable HOME for the model CLI's state. RUN useradd -m -s /bin/bash agent \ && mkdir -p /home/agent/.local/bin /work/runs \ && ln -sf /opt/sapper/lib/sapper.py /home/agent/.local/bin/sapper \ && chmod +x /opt/sapper/lib/sapper.py \ && chown -R agent:agent /work /opt/sapper /home/agent COPY docker/entrypoint.sh /usr/local/bin/sapper-entrypoint RUN chmod +x /usr/local/bin/sapper-entrypoint USER agent WORKDIR /work ENTRYPOINT ["/usr/local/bin/sapper-entrypoint"]docker/Dockerfile.commander3.0 KBc98dcc9e6ee0upstream ↗
# Commander-architecture variant: base tmux toolchain plus the commander # units (worker.sh / watchdog.sh / SKILL.md) and an entrypoint that boots the # AI commander TUI in tmux window 0 and the watchdog guardian behind it. # # Build: docker build -f docker/Dockerfile.commander -t sapper-agent:commander . # (requires sapper-agent:tmux to exist locally; context = repo root) FROM sapper-agent:tmux USER root # Arsenal layer (mounted here instead of the base image so the base stays cached): # stego/forensics, pwn, DB clients, wordlists kept, crypto stack. Compiled extras # are fault-tolerant (stdlib+sympy covers most crypto without them). # env -u: the docker daemon injects a 7890 proxy into build containers; routing # the TUNA mirror (domestic) through it is the 260KB/s crawl — go DIRECT. RUN env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \ apt-get update && env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \ apt-get install -y --no-install-recommends \ binwalk steghide exiftool redis-tools default-mysql-client postgresql-client \ gdb checksec libgmp-dev libmpfr-dev \ && rm -rf /var/lib/apt/lists/* \ && pip3 install --no-cache-dir --break-system-packages pycryptodome sympy ROPgadget \ && (pip3 install --no-cache-dir --break-system-packages ropper \ || echo "WARN: ropper skipped (py3.12 build issue) - ROPgadget covers it") \ && (pip3 install --no-cache-dir --break-system-packages gmpy2 z3-solver \ || echo "WARN: gmpy2/z3 unavailable - continuing") # Codex CLI harness for --engine codex workers (experimental tier, SKILL 4.9). # PINNED to 0.42.0: >=0.163 dropped wire_api="chat" (DeepSeek only speaks the # OpenAI chat wire) — upgrading codex breaks every --engine codex worker. # npm flags copied from the base image's claude-code install (Kali npm blocks # native postinstall scripts without --allow-scripts). RUN npm install -g @openai/[email protected] --allow-scripts --omit=dev 2>/dev/null || \ npm install -g @openai/[email protected] 2>/dev/null || echo "WARN: codex install failed (claude-only mode)" COPY docker/commander/ /opt/sapper/docker/commander/ # kb overlay: this image's kb/ is FRESHER than the base image's (field entries # added late) — copy it over so commanders see the latest knowledge COPY kb/ /opt/sapper/kb/ # codex provider config (chat wire for DeepSeek) into the agent user's home RUN mkdir -p /home/agent/.codex \ && cp /opt/sapper/docker/commander/codex-home/config.toml /home/agent/.codex/config.toml \ && chown -R agent:agent /home/agent/.codex RUN chmod +x /opt/sapper/docker/commander/worker.sh \ /opt/sapper/docker/commander/watchdog.sh \ /opt/sapper/docker/commander/entrypoint.sh \ && chown -R agent:agent /opt/sapper/docker/commander USER agent # factory default: the commander brain is the SMART tier; console env overrides # this. Prevents environment drift (a flash SAPPER_MODEL silently neuters # every --general escalation). ENV SAPPER_MODEL=glm-5.3[1m] ENTRYPOINT ["/opt/sapper/docker/commander/entrypoint.sh"]docker/Dockerfile.tmux718 B8494048d5683upstream ↗
# TUI-mode variant: same toolchain as the base image, plus tmux and an # entrypoint that runs agents as interactive claude sessions in tmux windows # (driver feeds missions + nudges; sessions stay alive for back-and-forth). # # Build: docker build -f docker/Dockerfile.tmux -t sapper-agent:tmux . # (requires sapper-agent:latest to exist locally) FROM sapper-agent:latest USER root RUN apt-get update && apt-get install -y --no-install-recommends tmux \ && rm -rf /var/lib/apt/lists/* COPY docker/tmux-drive.sh /opt/sapper/docker/tmux-drive.sh COPY docker/entrypoint-tmux.sh /usr/local/bin/sapper-entrypoint RUN chmod +x /usr/local/bin/sapper-entrypoint /opt/sapper/docker/tmux-drive.sh USER agentdocker/entrypoint-tmux.sh2.9 KB6663ef8bbc7aupstream ↗
#!/usr/bin/env bash # entrypoint-tmux.sh — TUI-mode variant: agents run as INTERACTIVE claude # sessions in tmux windows, fed missions and nudges by a driver loop. # # Compare against the headless (-p) variant: this one keeps each agent's # session ALIVE (true back-and-forth in one context), at the cost of context # growth over time and TUI-fragility. Stdout is preserved by tailing the # driver log; per-window output stays inspectable via tmux inside the # container. set -uo pipefail log() { printf '[sapper-tmux] %s\n' "$*"; } die() { printf '[sapper-tmux] FATAL: %s\n' "$*" >&2; exit 1; } [[ -n "${BENCHMARK_BASE_URL:-}" ]] || die "BENCHMARK_BASE_URL is not set" [[ -n "${BENCHMARK_TOKEN:-}" ]] || die "BENCHMARK_TOKEN is not set" : "${ANTHROPIC_BASE_URL:?set ANTHROPIC_BASE_URL}" : "${ANTHROPIC_AUTH_TOKEN:?set ANTHROPIC_AUTH_TOKEN}" export ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 DISABLE_TELEMETRY=1 \ DISABLE_ERROR_REPORTING=1 DISABLE_AUTOUPDATER=1 IS_SANDBOX=1 mkdir -p "$HOME/.claude" cat > "$HOME/.claude.json" <<JSON {"hasCompletedOnboarding": true, "theme": "dark", "hasTrustDialogAccepted": true} JSON [[ -f "$HOME/.claude/settings.json" ]] || \ echo '{"permissions":{"defaultMode":"bypassPermissions"}}' > "$HOME/.claude/settings.json" log "platform = $BENCHMARK_BASE_URL" log "gateway = $ANTHROPIC_BASE_URL" log "model = ${SAPPER_MODEL:-glm-5.3-flash[1m]} (interactive TUI x ${SAPPER_PARALLEL:-3})" case "$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 \ -H "BENCHMARK_TOKEN: $BENCHMARK_TOKEN" \ "$BENCHMARK_BASE_URL/openapi/v1/challenges" || true)" in 2*) log "platform reachable" ;; *) die "cannot reach the challenges API" ;; esac ROOT="${SAPPER_ROOT:-/work/runs}" mkdir -p "$ROOT" start_driver_window() { tmux new-window -d -t sapper-tui -n supervisor "bash /opt/sapper/docker/tmux-drive.sh 2>&1 | tee -a $ROOT/drive.err" } # detached fleet session; window 0 becomes the supervisor tmux new-session -d -s sapper-tui -n boot "exec sleep infinity" start_driver_window tmux kill-window -t sapper-tui:boot 2>/dev/null log "tmux session 'sapper-tui' up (supervisor driving)" # watchdog: stream logs for the platform AND resurrect the driver if it dies. # The driver itself is idempotent (adopts existing windows), so a respawn # never duplicates work. touch "$ROOT/drive.log" "$ROOT/drive.err" tail -F -n0 "$ROOT/drive.log" "$ROOT/drive.err" & TAIL_PID=$! while true; do sleep 30 if ! tmux has-session -t sapper-tui 2>/dev/null; then log "WATCHDOG: tmux session gone — recreating" tmux new-session -d -s sapper-tui -n boot "exec sleep infinity" start_driver_window tmux kill-window -t sapper-tui:boot 2>/dev/null elif ! tmux list-windows -t sapper-tui -F '#W' 2>/dev/null | grep -q '^supervisor$'; then log "WATCHDOG: supervisor window gone — respawning driver" start_driver_window fi donedocker/entrypoint.sh3.2 KB67297fb54805upstream ↗
#!/usr/bin/env bash # sapper-entrypoint — container start: validate env, then drive TSecBench. # # ONE-SHOT match rules (Throne Protocol online round): a single evaluation, # 8h task limit, model rate-limits are NOT retried by the platform — so this # script and the sapper loops below degrade and continue instead of dying. set -uo pipefail log() { printf '[sapper] %s\n' "$*"; } die() { printf '[sapper] FATAL: %s\n' "$*" >&2; exit 1; } # ---- 1. required platform env --------------------------------------------- [[ -n "${BENCHMARK_BASE_URL:-}" ]] || die "BENCHMARK_BASE_URL is not set (platform-injected)" [[ -n "${BENCHMARK_TOKEN:-}" ]] || die "BENCHMARK_TOKEN is not set (platform-injected)" # ---- 2. model gateway env -------------------------------------------------- : "${ANTHROPIC_BASE_URL:?ANTHROPIC_BASE_URL not set (e.g. https://open.bigmodel.cn/api/anthropic)}" : "${ANTHROPIC_AUTH_TOKEN:?ANTHROPIC_AUTH_TOKEN not set (the provider API key)}" export ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN # hosted mode has no public internet except the model gateway export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 export DISABLE_TELEMETRY=1 export DISABLE_ERROR_REPORTING=1 export DISABLE_AUTOUPDATER=1 export IS_SANDBOX=1 export SAPPER_RETRY_MODEL="${SAPPER_RETRY_MODEL:-3}" # ---- 3. non-interactive model CLI state ----------------------------------- mkdir -p "$HOME/.claude" cat > "$HOME/.claude.json" <<JSON {"hasCompletedOnboarding": true, "theme": "dark", "hasTrustDialogAccepted": true} JSON if [[ ! -f "$HOME/.claude/settings.json" ]]; then echo '{"permissions":{"defaultMode":"bypassPermissions"}}' > "$HOME/.claude/settings.json" fi # ---- 4. preflight ---------------------------------------------------------- log "platform = $BENCHMARK_BASE_URL" log "gateway = $ANTHROPIC_BASE_URL" log "model = ${SAPPER_MODEL:-glm-5.3-flash[1m]} (retries: $SAPPER_RETRY_MODEL)" case "$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 \ -H "BENCHMARK_TOKEN: $BENCHMARK_TOKEN" \ "$BENCHMARK_BASE_URL/openapi/v1/challenges" || true)" in 2*) log "platform reachable" ;; *) die "cannot reach $BENCHMARK_BASE_URL/openapi/v1/challenges (check BENCHMARK_TOKEN / gateway)" ;; esac log "model CLI smoke test…" if ! printf 'reply with the single word: ok' \ | claude -p --model "${SAPPER_MODEL:-glm-5.3-flash[1m]}" >/tmp/smoke.txt 2>/tmp/smoke.err; then log "WARNING: model CLI smoke test failed; continuing (bench loops will retry)" sed -n '1,5p' /tmp/smoke.err | sed 's/^/ /' else log "model CLI ok: $(head -c 60 /tmp/smoke.txt)" fi # ---- 5. run the benchmark loop -------------------------------------------- # 8h task limit; keep a small safety margin. bench loops over every # unfinished challenge and re-visits solved-later ones across rounds. ROOT="${SAPPER_ROOT:-/work/runs}" TOTAL="${SAPPER_TOTAL_MINUTES:-470}" PER="${SAPPER_MINUTES_PER_CHALLENGE:-9}" mkdir -p "$ROOT" log "starting: sapper bench --root $ROOT --minutes $TOTAL (per-challenge ${PER}m, parallel ${SAPPER_PARALLEL:-3})" exec sapper bench \ --root "$ROOT" \ --minutes "$TOTAL" \ --minutes-per-challenge "$PER" \ --max-steps "${SAPPER_MAX_STEPS:-10}" \ --parallel "${SAPPER_PARALLEL:-3}" \ --model "${SAPPER_MODEL:-glm-5.3-flash[1m]}" \ --skip-permissionsdocker/ENV.md3.8 KBc19ef307c491upstream ↗
# TSecBench 托管模式配置清单(王座协议线上赛) 正式赛规则要点(2026-10-17 00:00 — 10-18 23:59): - **只有 1 次评测机会**,仅队长账号可提交;Agent 异常没有重试 - 50 题 / 100 flag,任务限时 **8 小时**(镜像内默认按 470 分钟跑,留安全边际) - 无 hint(接口返回 null,也无惩罚) - 排名顺序:**得分 → 有效耗时 → token 量**(所以默认不开 verify 双检,省 token) - 模型限流/超时/中断不算平台异常——镜像内已带**模型调用重试**(SAPPER_RETRY_MODEL=3,指数退避),单题异常自动跳过下一轮再试,绝不中断整个任务 - 严禁引导 Agent 攻击平台——提示词中已有 scope 纪律,请勿在模板里加任何"打平台"的引导 ## 一、平台自动注入(控制台不要手填) | 变量 | 说明 | |---|---| | `BENCHMARK_BASE_URL` | 答题 API 基址,任务创建后自动注入 | | `BENCHMARK_TOKEN` | 答题 API 凭证,自动注入 | ## 二、运行时环境变量(控制台手填) | KEY | VALUE | 说明 | |---|---|---| | `ANTHROPIC_BASE_URL` | `https://open.bigmodel.cn/api/anthropic` | 直连厂商公开端点 | | `ANTHROPIC_AUTH_TOKEN` | 你的智谱 API Key | 换供应商只改这项和 BASE_URL | | `SAPPER_MODEL` | `glm-5.3-flash[1m]` | 已实测走通。`[1m]` 后缀由 claude CLI 解析为 1M 上下文(剥后缀 + beta 头),API 侧模型名仍是 glm-5.3-flash,直接填 `glm-5.3-flash-1m` 会报模型不存在 | ### Key 池自动轮换(强烈建议配置) 当某个 Key 额度耗尽/401/欠费,系统自动切换到池中下一组凭据,`-p` 版**无需重启**(下一次模型调用即用新 Key),tmux 版自动轮换并重启对应 TUI(`claude --continue` 恢复原会话上下文)。检测特征:insufficient/balance/quota/exceeded/billing/401/unauthorized/欠费/余额。 | KEY | VALUE 格式 | |---|---| | `SAPPER_KEY_POOL` | `[{"base_url":"https://open.bigmodel.cn/api/anthropic","token":"智谱key1","model":"glm-5.3-flash[1m]"},{"base_url":"https://api.moonshot.cn/v1","token":"moonshot-key","model":"kimi-k2.5"}]` | 注意:池里各组 URL 直连各厂商公开端点。单 Key 时轮换自动退化为普通重试。 可选调参(不填用镜像默认值): | KEY | 默认 | 说明 | |---|---|---| | `SAPPER_TOTAL_MINUTES` | `470` | 总预算(8h 限时留边际) | | `SAPPER_MINUTES_PER_CHALLENGE` | `9` | 每题每轮 auto 分钟数;bench 多轮循环,没解完的题下轮续(进度在图里保留)。50 题×9 分≈450 分 | | `SAPPER_MAX_STEPS` | `10` | 每题每轮最多 reason 轮数 | | `SAPPER_RETRY_MODEL` | `3` | 模型调用失败重试次数(10s/20s/40s 退避) | | `SAPPER_VERIFY` | 不设 | 设为 `1` 才开启核验者(费 token,正式赛默认不开) | ## 三、容器启动流程 1. 校验 `BENCHMARK_*` 与模型网关环境变量,缺失立刻 FATAL(日志可见) 2. 平台 API 连通性探测 + 模型 CLI 冒烟测试 3. `sapper bench --minutes 470`:对每个未完成题目循环 `start → 建图(题目描述/靶标地址按不可信数据录入)→ auto 自主解题 → 提交已记录未提交的 flag(正确/错误都回写)→ close` 一轮做不完全部题就再来一轮,直到全部完成或 470 分钟用尽 4. 非特权用户运行(Claude Code 拒绝 root 下跳权限确认);密钥全走环境变量,镜像不含任何密钥/题目记忆 ## 四、一次机会的检查单(提交前自查) - [ ] 用**其他题集**(如 Tsecbench v1 公开集)跑过完整托管评测,`correct_flag_count` 有增长 - [ ] 镜像内 `sapper --version`、`sapper bench --help` 正常 - [ ] `ANTHROPIC_*` 三个变量填的是厂商公开端点地址 + 正确 key - [ ] 队长账号提交;活动期间只此一次docker/export.sh872 Bc63e90d22dcbupstream ↗
#!/usr/bin/env bash # export.sh — save an image and keep only the two most recent tars. # Usage: bash docker/export.sh [image] [out-tar] # default: sapper-agent:commander dist/sapper-agent-commander.tar.gz # Rotation: <out>.prev2 deleted, .prev1 -> .prev2, current -> .prev1, then # the fresh export becomes the current file. Disk never holds >2 versions. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" IMAGE="${1:-sapper-agent:commander}" OUT="${2:-$REPO/dist/sapper-agent-commander.tar.gz}" mkdir -p "$(dirname "$OUT")" if [[ -f "$OUT.prev1" ]]; then mv -f "$OUT.prev1" "$OUT.prev2"; fi if [[ -f "$OUT" ]]; then mv -f "$OUT" "$OUT.prev1"; fi echo "==> exporting $IMAGE -> $OUT (keeping prev1/prev2 only)" docker save "$IMAGE" | gzip -"${GZ_LEVEL:-1}" > "$OUT" gzip -t "$OUT" echo "==> done: $(du -h "$OUT" | cut -f1) $(date +%H:%M:%S)"docker/local-sim.sh2.3 KB54be1c5d420bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docker/nuclei.zip27.7 MB4b19c0b7bd24upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docker/tmux-drive.sh15 KB6b2ff4bcd52fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/ape-stargazing.jpg16 KBb5506802ebd5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/banner.jpg23 KB1f307ab72ceeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/foundation-three-capabilities.jpg21 KB44225d8a60ebupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/runtime-tmux.jpg18 KB3d9b5eee0d07upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/score-curve.jpg24 KB4f1b6d312b0eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/six-institutions.jpg31 KBc562090aa777upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/assets/token-comparison.jpg26 KB7756016cdf6bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
docs/events-2026-10-17.md18 KB972c1c0aa5ceupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
install.sh3.2 KB210bb02b2d07upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb001.json933 B105c55b4df0dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb002.json1.0 KB3cd0279bd02cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb003.json839 Bd32b92b5a087upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb004.json760 B8b701a581da5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb005.json816 B40f7cc45b714upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb006.json845 B11012d5d0177upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb007.json697 Bff0ba55d6130upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb008.json515 B0e6864daf43fupstream ↗
{ "id": "kb008", "title": "XXE", "description": "有回显直读,无回显 OOB 外带", "detail": "有回显:<!ENTITY x SYSTEM \"file:///flag\">\n参数实体 + 外带(无回显):evil.dtd 里 %all; %send;\nSSRF 组合:读 file:///etc/hosts 定内网再转 SSRF 打。\n支持点:上传 svg/xlsx/docx(内嵌 xml)、soap 接口、Content-Type: text/xml。", "commands": [], "tags": [ "web", "xxe" ], "source": "PayloadsAllTheThings", "created_at": "2026-10-15T13:44:05Z" }kb/entries/kb009.json673 Bcd31ca2568d4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb010.json814 Bb2dbd26f1cedupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb011.json851 Bc6c58d1cbc96upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb012.json858 B93b123d7a3a6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb013.json716 B80516376e47eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb014.json870 Bf4247d5f7230upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb015.json897 B438af6a442f5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb016.json995 B7e80e1c6ca37upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb017.json748 B1dc5b1770463upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb018.json656 Bf51c69b5d1ebupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb019.json812 B27073ce45675upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb020.json1.0 KBba004892fde0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb021.json1.2 KB8b9bfad6ebe1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb022.json1.1 KB6fad45cdea84upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb023.json951 Bf7a5332f3589upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb024.json923 Ba6ac93f16a83upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb025.json1.1 KB283fd26ae7efupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb026.json1.0 KB87a31a5b4a8dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb027.json1.2 KB6bbd4b5731b4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb028.json1.0 KB4fc8aa942f8bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb029.json1.1 KBe4f06e3e2b9cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb030.json1.1 KBa08b59b5f1beupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb031.json1.3 KB8741bd3f8537upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb032.json857 Bac06031e1bd1upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb033.json909 Bebaa00c25a4eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb034.json1.2 KBc84899b2d4c5upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb035.json1.1 KB599c1453b503upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb036.json1.1 KBebc1cf92b8beupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb037.json1.1 KBdf5ce1ed74b2upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb038.json958 B7c451d791a1eupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb039.json854 Bcab7c7be8f35upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb040.json1.1 KB6c5f45bd70c6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb041.json1.1 KB5b1a9c9f6ea6upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb042.json1.0 KB73931befc420upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb043.json1.0 KBa7f32dbb7d4cupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb044.json1.1 KB1d320a48db91upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb045.json974 B9bdae94121e3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb046.json856 Baeb5a07c8ccfupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb047.json820 Bf53fc4001e28upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb048.json857 B0b29446a9079upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb049.json819 Ba575ed7cebeeupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb050.json997 B18534ecdbaabupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb051.json700 B908751cb617fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb052.json624 Bf28ad58f0f49upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb053.json659 B01737948f32aupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb054.json839 B546ed3d04185upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb055.json1.2 KB154ba2d6eeb0upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
kb/entries/kb056.json1003 B0cbba0f39cadupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/_settings_patch.py8.8 KB8110663bf82bupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/arena.py9.0 KB1cc58147d52dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/kb.py7.2 KBa6001d088da9upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/sapper-hook.py7.8 KB775258be5860upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/sapper.py195 KB5aa390332ed4upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
lib/view_template.html25 KB737682b7ff4fupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
LICENSE1.0 KB1508447015a3upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
README.zh-CN.md12 KB73f5ff60ec50upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/blindspot.md1.7 KB8cac2220b133upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/brainstorm.md1.9 KB2a42a6849ff8upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/command.md3.3 KB9575cc86e948upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/explore.md7.5 KB695a4af38d48upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/reason.md5.5 KB7a88bf724b6dupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
templates/verify.md1.7 KB8d06cfcc0796upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
tests/test_sapper.py35 KB0a10df6ff4beupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/n1neman-sapper-v2.md