# sapper

Adopt this skill: https://proskills.md/skills/other/n1neman-sapper-v2

Page: https://proskills.md/skills/other/n1neman-sapper-v2
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: docker/commander/SKILL.md
size: 24399
sha: 6566633ba8a411f61399d5467bd1c973befb2d16
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/SKILL.md

````text
# 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.
````

## File: SKILL.md
size: 15615
sha: 11bf0f8bfb361165e9fdf401fb0a40998a129512
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/SKILL.md

````text
---
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)
````

## File: skill/SKILL.md
size: 6542
sha: 69faddf20999bbf2f9a375aecb9f661070022b05
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/skill/SKILL.md

````text
---
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.
````

## File: examples/README.md
size: 2565
sha: f224c49b31d86ebda593cfa52b18c9ceb8aa2b40
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/examples/README.md

````text
# 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
```
````

## File: README.md
size: 13382
sha: 2b32a312a3633b844295707b9dc44bd2c7a8c2c9
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/README.md

````text
# 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))
````

## File: .dockerignore
size: 58
sha: e0078ffa9112cb0878c9e633cdef2a40ef48321c
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/.dockerignore

```text
.git
dist
tests
*.pyc
__pycache__
.sapper
runs
fleet.json
```

## File: .gitignore
size: 661
sha: e898f1a458816672f7050f39a9dba0725b116d5d
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/.gitignore

```text
# 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.md
```

## File: BATTLE-REPORT.md
size: 8073
sha: 20eea2553c0988c57ed30a818fd8205d1fc92184
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/BATTLE-REPORT.md

````text
# 泡泡爸军团 · 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 小时开发迭代
```
````

## File: CHANGELOG.md
size: 15142
sha: f6e9e682ea7e1073e97e07a43b774b348e725c2c
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/CHANGELOG.md

````text
# 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.
````

## File: DISCLAIMER.md
size: 2981
sha: de3a3be7bcbf2017feed3decdd458946bbb85edf
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/DISCLAIMER.md

```text
# 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. **可追责示踪剂。** 去中心化可追责示踪剂（难以完整剥除的运行证据、防守方可无密钥验证）正在开发线路上积极开发中，计划随后续版本发布。**滥用者应假定：后续版本——包括静默更新——均可能携带可溯源标记，任何版本都不保证'不可追踪'。**
```

## File: docker/build.sh
size: 1005
sha: 8af2bba4eb26725456e2a04dd4ba2030cc313145
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/build.sh

```text
#!/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."
```

## File: docker/commander/codex-home/config.toml
size: 623
sha: b0e86ddc41c3b1688ae0855c4a8d3c667d2e34ed
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/codex-home/config.toml

```text
# 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"
```

## File: docker/commander/entrypoint.sh
size: 8218
sha: b3a1423427cff071cd48bb94478d5242750e5fc5
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/entrypoint.sh

```text
#!/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 || true
```

## File: docker/commander/SPEC.md
size: 11831
sha: a3124992d2d8551e9cfe33f906a3993a3449fbf7
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/SPEC.md

````text
# 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 detectable
````

## File: docker/commander/watchdog.sh
size: 30173
sha: 7e8edf3563cd3ca7b3e9bb31de7d9f0c48e25a16
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/watchdog.sh

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/watchdog.sh

## File: docker/commander/worker.sh
size: 36511
sha: bfc0499f8309641679d13009886de6eb1cb6f4c4
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/worker.sh

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/commander/worker.sh

## File: docker/Dockerfile
size: 4523
sha: 13fbb3ab682b101356fa01225e55fd8994fe35a4
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/Dockerfile

```text
# 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"]
```

## File: docker/Dockerfile.commander
size: 3111
sha: c98dcc9e6ee04f15b001c8cef5872cb9715eb074
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/Dockerfile.commander

```text
# 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/codex@0.42.0 --allow-scripts --omit=dev 2>/dev/null || \
    npm install -g @openai/codex@0.42.0 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"]
```

## File: docker/Dockerfile.tmux
size: 718
sha: 8494048d5683196e05192c2e5b4294e83020d0ed
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/Dockerfile.tmux

```text
# 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 agent
```

## File: docker/entrypoint-tmux.sh
size: 2947
sha: 6663ef8bbc7aa899d251c3d839d77b26cd8ad326
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/entrypoint-tmux.sh

```text
#!/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
done
```

## File: docker/entrypoint.sh
size: 3293
sha: 67297fb54805058ab73be2215e880629c884aeee
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/entrypoint.sh

```text
#!/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-permissions
```

## File: docker/ENV.md
size: 3859
sha: c19ef307c491f306904b390db735265419ac10ac
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/ENV.md

```text
# 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
- [ ] 队长账号提交；活动期间只此一次
```

## File: docker/export.sh
size: 872
sha: c63e90d22dcbac53c99b3afd231da4181de94676
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/export.sh

```text
#!/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)"
```

## File: docker/local-sim.sh
size: 2360
sha: 54be1c5d420b4b4d134d25c2655e87b285b6536c
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/local-sim.sh

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/local-sim.sh

## File: docker/nuclei.zip
size: 29058291
sha: 4b19c0b7bd244104b1c26955b57ccbb83dc393f9
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/nuclei.zip

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/nuclei.zip

## File: docker/tmux-drive.sh
size: 15210
sha: 6b2ff4bcd52f6cf5a81005701d02ded5dacb4f4c
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/tmux-drive.sh

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docker/tmux-drive.sh

## File: docs/assets/ape-stargazing.jpg
size: 16768
sha: b5506802ebd5fc391e83419628cf34f31ea51709
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/ape-stargazing.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/ape-stargazing.jpg

## File: docs/assets/banner.jpg
size: 23409
sha: 1f307ab72cee0c3c41efcd395635fe4f5397babc
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/banner.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/banner.jpg

## File: docs/assets/foundation-three-capabilities.jpg
size: 21791
sha: 44225d8a60eba74d4cf75ab875819950c6241d2b
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/foundation-three-capabilities.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/foundation-three-capabilities.jpg

## File: docs/assets/runtime-tmux.jpg
size: 18493
sha: 3d9b5eee0d07e3517847deb42c12b76089369d1f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/runtime-tmux.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/runtime-tmux.jpg

## File: docs/assets/score-curve.jpg
size: 24648
sha: 4f1b6d312b0e9f8fc361d1379ec3b108880635b8
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/score-curve.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/score-curve.jpg

## File: docs/assets/six-institutions.jpg
size: 31686
sha: c562090aa777b5a3a18e4068f0f7762a3e831317
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/six-institutions.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/six-institutions.jpg

## File: docs/assets/token-comparison.jpg
size: 26291
sha: 7756016cdf6bba977080b1076aac54c717986552
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/token-comparison.jpg

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/assets/token-comparison.jpg

## File: docs/events-2026-10-17.md
size: 17934
sha: 972c1c0aa5ce35ff329bce5ed3774d7606d1c192
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/events-2026-10-17.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/docs/events-2026-10-17.md

## File: install.sh
size: 3296
sha: 210bb02b2d07931c3099ba2b72f6949d1bd81db3
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/install.sh

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/install.sh

## File: kb/entries/kb001.json
size: 933
sha: 105c55b4df0d7ab5770745e8191a4240edc8d670
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb001.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb001.json

## File: kb/entries/kb002.json
size: 1024
sha: 3cd0279bd02cd909fc86d09a5b1af1ac2139746e
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb002.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb002.json

## File: kb/entries/kb003.json
size: 839
sha: d32b92b5a0877c77a369b167b46838ffc032589d
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb003.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb003.json

## File: kb/entries/kb004.json
size: 760
sha: 8b701a581da5347ca2d04645d57576eb05bfcdb5
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb004.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb004.json

## File: kb/entries/kb005.json
size: 816
sha: 40f7cc45b71497c07a02c5cc91eda83758132b7e
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb005.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb005.json

## File: kb/entries/kb006.json
size: 845
sha: 11012d5d01778a504f96eada90f00059ae9a1ad9
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb006.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb006.json

## File: kb/entries/kb007.json
size: 697
sha: ff0ba55d61306e51ba67e6f1363c65bb5aaaf8de
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb007.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb007.json

## File: kb/entries/kb008.json
size: 515
sha: 0e6864daf43fa9e6e178238617f7375b8fe02f4a
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb008.json

```text
{
  "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"
}
```

## File: kb/entries/kb009.json
size: 673
sha: cd31ca2568d4b4b6ae1c1bd2b96c5e23335dcc47
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb009.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb009.json

## File: kb/entries/kb010.json
size: 814
sha: b2dbd26f1ced9fd4b9b83fed0d5f5b020def4acd
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb010.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb010.json

## File: kb/entries/kb011.json
size: 851
sha: c6c58d1cbc966730ad1c0dc349e03d48b2548eba
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb011.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb011.json

## File: kb/entries/kb012.json
size: 858
sha: 93b123d7a3a6aa3328cce254553189eb4d3715df
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb012.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb012.json

## File: kb/entries/kb013.json
size: 716
sha: 80516376e47ecd0eae8738dfb47601cc68a1d4c2
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb013.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb013.json

## File: kb/entries/kb014.json
size: 870
sha: f4247d5f7230f31328486d74a739148c2c67e4c4
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb014.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb014.json

## File: kb/entries/kb015.json
size: 897
sha: 438af6a442f52e397bc277b2dc9aca8209501821
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb015.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb015.json

## File: kb/entries/kb016.json
size: 995
sha: 7e80e1c6ca3709a1b74f80302792568ae9e482a0
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb016.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb016.json

## File: kb/entries/kb017.json
size: 748
sha: 1dc5b17704635be86306292cf090b46a3f628b09
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb017.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb017.json

## File: kb/entries/kb018.json
size: 656
sha: f51c69b5d1eb9dcec5eb2a054029f511314d7725
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb018.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb018.json

## File: kb/entries/kb019.json
size: 812
sha: 27073ce45675d323c176fb9af5d115a67506613a
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb019.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb019.json

## File: kb/entries/kb020.json
size: 1052
sha: ba004892fde03411759b377bfef6ee1be9ec78ac
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb020.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb020.json

## File: kb/entries/kb021.json
size: 1258
sha: 8b9bfad6ebe153f836c9b441c8da114add0e3fcb
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb021.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb021.json

## File: kb/entries/kb022.json
size: 1114
sha: 6fad45cdea84c8c02dcc8c370f9a0697081b91fb
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb022.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb022.json

## File: kb/entries/kb023.json
size: 951
sha: f7a5332f3589406e00b2f59a17dba0060d4d2abf
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb023.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb023.json

## File: kb/entries/kb024.json
size: 923
sha: a6ac93f16a837ac6c680c7252f384205f865acd0
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb024.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb024.json

## File: kb/entries/kb025.json
size: 1100
sha: 283fd26ae7ef5cef341f4a9d2c7db4240c9497b2
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb025.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb025.json

## File: kb/entries/kb026.json
size: 1041
sha: 87a31a5b4a8d2d241df57aa86ea517d02b62f0ff
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb026.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb026.json

## File: kb/entries/kb027.json
size: 1200
sha: 6bbd4b5731b46658ed9ed7ade82e6037ccde8254
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb027.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb027.json

## File: kb/entries/kb028.json
size: 1048
sha: 4fc8aa942f8b520f8f1c3882490207fd4622b84b
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb028.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb028.json

## File: kb/entries/kb029.json
size: 1153
sha: e4f06e3e2b9cee2b87d5e2a4e2f9a8ac36124497
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb029.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb029.json

## File: kb/entries/kb030.json
size: 1122
sha: a08b59b5f1be7b813d00cb3141aa249c04f11afa
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb030.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb030.json

## File: kb/entries/kb031.json
size: 1318
sha: 8741bd3f8537953bc2c0bba73a8c00c1e0eac4e6
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb031.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb031.json

## File: kb/entries/kb032.json
size: 857
sha: ac06031e1bd11baefd5c2a6ac3841e29606e1331
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb032.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb032.json

## File: kb/entries/kb033.json
size: 909
sha: ebaa00c25a4e44353e1359f1a337674dbccf0e7e
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb033.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb033.json

## File: kb/entries/kb034.json
size: 1241
sha: c84899b2d4c5e8223948c45f2b2be4418eaae19f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb034.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb034.json

## File: kb/entries/kb035.json
size: 1124
sha: 599c1453b503f891ca556848aebeafc259330860
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb035.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb035.json

## File: kb/entries/kb036.json
size: 1156
sha: ebc1cf92b8be594ecc483e665a33cf5aa3490004
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb036.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb036.json

## File: kb/entries/kb037.json
size: 1084
sha: df5ce1ed74b2fad34063cd04afd9a43fed50f7ce
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb037.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb037.json

## File: kb/entries/kb038.json
size: 958
sha: 7c451d791a1eec174679ba313b925aea5f6f1124
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb038.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb038.json

## File: kb/entries/kb039.json
size: 854
sha: cab7c7be8f35fe018d6c5d67dc1c9c9ff0ed6ee1
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb039.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb039.json

## File: kb/entries/kb040.json
size: 1084
sha: 6c5f45bd70c61056a65738f9ff89bb1a8cb6894a
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb040.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb040.json

## File: kb/entries/kb041.json
size: 1086
sha: 5b1a9c9f6ea68e9b1375efdb47ea3734ae19e80c
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb041.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb041.json

## File: kb/entries/kb042.json
size: 1031
sha: 73931befc420c586e8c10fe6e8f6158074851a7a
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb042.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb042.json

## File: kb/entries/kb043.json
size: 1032
sha: a7f32dbb7d4c129de1d165715354638696fe8600
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb043.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb043.json

## File: kb/entries/kb044.json
size: 1105
sha: 1d320a48db917b45f47350f0b276acbea459ad26
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb044.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb044.json

## File: kb/entries/kb045.json
size: 974
sha: 9bdae94121e3b8d3efbd060785bdecf11c6116ee
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb045.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb045.json

## File: kb/entries/kb046.json
size: 856
sha: aeb5a07c8ccfee818d2cdbfe5d3d9c6852874147
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb046.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb046.json

## File: kb/entries/kb047.json
size: 820
sha: f53fc4001e28309942598bb75ce539033ce57645
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb047.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb047.json

## File: kb/entries/kb048.json
size: 857
sha: 0b29446a9079d36ce6b6750871a566c243065cab
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb048.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb048.json

## File: kb/entries/kb049.json
size: 819
sha: a575ed7cebee5dd6e8e70d39540797cfedda183f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb049.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb049.json

## File: kb/entries/kb050.json
size: 997
sha: 18534ecdbaab6ef9ff2aa81c23656f2a50c0302f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb050.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb050.json

## File: kb/entries/kb051.json
size: 700
sha: 908751cb617f2df0cd1e1c2a8d9c0ac39177a37b
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb051.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb051.json

## File: kb/entries/kb052.json
size: 624
sha: f28ad58f0f49d4b04a5aded3d9ff9b21270d386f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb052.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb052.json

## File: kb/entries/kb053.json
size: 659
sha: 01737948f32aa87d7ff10ab43227de722e808743
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb053.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb053.json

## File: kb/entries/kb054.json
size: 839
sha: 546ed3d04185bd4a0a05d757a097afeec623f7db
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb054.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb054.json

## File: kb/entries/kb055.json
size: 1240
sha: 154ba2d6eeb0baaa0cf0082791130b2bc49133f4
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb055.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb055.json

## File: kb/entries/kb056.json
size: 1003
sha: 0cbba0f39cada506d15be71fbd2dae87c84d54bd
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb056.json

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/kb/entries/kb056.json

## File: lib/_settings_patch.py
size: 8969
sha: 8110663bf82baae38905e19a19932e8b02198932
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/_settings_patch.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/_settings_patch.py

## File: lib/arena.py
size: 9190
sha: 1cc58147d52d352d3a6df49825773de9275fed2d
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/arena.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/arena.py

## File: lib/kb.py
size: 7377
sha: a6001d088da9241a9d870b4982eb51d1d070547d
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/kb.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/kb.py

## File: lib/sapper-hook.py
size: 7988
sha: 775258be5860f4642781b8f0958599d193ab66cd
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/sapper-hook.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/sapper-hook.py

## File: lib/sapper.py
size: 199997
sha: 5aa390332ed47becca1eb678365fc5cee71c79a1
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/sapper.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/sapper.py

## File: lib/view_template.html
size: 25364
sha: 737682b7ff4f14e0d78f247c3d90091e65f47f61
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/view_template.html

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/lib/view_template.html

## File: LICENSE
size: 1064
sha: 1508447015a322f3a90b344c18533c55dd2bf1df
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/LICENSE

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/LICENSE

## File: README.zh-CN.md
size: 11997
sha: 73f5ff60ec505e80ee3bbf1d12567997eee96ac6
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/README.zh-CN.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/README.zh-CN.md

## File: templates/blindspot.md
size: 1790
sha: 8cac2220b1333980afae1f9796223aa354b9b785
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/blindspot.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/blindspot.md

## File: templates/brainstorm.md
size: 1948
sha: 2a42a6849ff86fa2a4c0f07d5a522d2528576fc8
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/brainstorm.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/brainstorm.md

## File: templates/command.md
size: 3394
sha: 9575cc86e94829154f668dc99506238048470796
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/command.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/command.md

## File: templates/explore.md
size: 7720
sha: 695a4af38d483f5853124ce4b57898d2f9a978bb
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/explore.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/explore.md

## File: templates/reason.md
size: 5675
sha: 7a88bf724b6d8f2b0410d44f2c6f925bdd35974f
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/reason.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/reason.md

## File: templates/verify.md
size: 1783
sha: 8d06cfcc0796207ff9516d4ec843e278ed7aa475
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/verify.md

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/templates/verify.md

## File: tests/test_sapper.py
size: 36320
sha: 0a10df6ff4bea673a295dadfd092cd1b04c8ee19
upstream: https://github.com/n1neman/sapper-v2/blob/HEAD/tests/test_sapper.py

Content is not inlined in the catalog. Read the upstream file: https://github.com/n1neman/sapper-v2/blob/HEAD/tests/test_sapper.py
