All Mermaid validated in Mermaid Live Editor. n8n JSON is structurally valid and importable.
Type: n8n workflow (importable JSON) Purpose: The primary visual — the meta-agent-plus-specialists architecture. Shows a challenge entering the router, being classified into one of six domains, and dispatched to the matching specialized agent with its tool subset.
{
"name": "S04 — CTF Domain Router",
"nodes": [
{ "name": "Challenge In", "type": "n8n-nodes-base.manualTrigger", "position": [200, 360], "notes": "Challenge description + attached artifacts (binary, pcap, image, URL)" },
{ "name": "Meta-Agent / Classifier", "type": "n8n-nodes-base.set", "position": [440, 360], "notes": "Reads description + file-type signal. Emits domain label. Routes to specialist. Allowed to be uncertain." },
{ "name": "WebAgent", "type": "n8n-nodes-base.set", "position": [760, 80], "notes": "Burp headless, sqlmap, ffuf, curated XSS payloads. Request-send-inspect loop." },
{ "name": "PwnAgent", "type": "n8n-nodes-base.set", "position": [760, 200], "notes": "pwntools, GDB+pwndbg, checksec, ROPgadget, one_gadget. checksec-before-reasoning loop." },
{ "name": "CryptoAgent", "type": "n8n-nodes-base.set", "position": [760, 320], "notes": "CyberChef recipes, SageMath, frequency analyzers, padding-oracle scripts." },
{ "name": "ForensicsAgent", "type": "n8n-nodes-base.set", "position": [760, 440], "notes": "binwalk, strings, exiftool, tshark, Volatility. Artifact-triage loop." },
{ "name": "ReversingAgent", "type": "n8n-nodes-base.set", "position": [760, 560], "notes": "Ghidra headless, radare2/rizin, ltrace, strace, gdb. Decompile-triage loop. Heaviest context consumer." },
{ "name": "MiscAgent", "type": "n8n-nodes-base.set", "position": [760, 680], "notes": "zsteg, stegsolve, CyberChef, custom Python. Variable reasoning loop." },
{ "name": "TriageAgent (uncertain)", "type": "n8n-nodes-base.set", "position": [440, 540], "notes": "When meta-agent is uncertain: runs cheap probes (file, checksec, strings freq, HTTP code) then re-routes." },
{ "name": "Flag Extractor (middleware)", "type": "n8n-nodes-base.set", "position": [1020, 360], "notes": "Regex sweep over every tool output. Surfaces flag candidates to verifier." },
{ "name": "Scoreboard (CTFd)", "type": "n8n-nodes-base.httpRequest", "position": [1240, 360], "notes": "POST /api/v1/challenges/attempt with token. Rate-limited. correct/incorrect/already_solved handling." }
],
"connections": {
"Challenge In": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
"Meta-Agent / Classifier": { "main": [
[{ "node": "WebAgent", "type": "main", "index": 0 }],
[{ "node": "PwnAgent", "type": "main", "index": 0 }],
[{ "node": "CryptoAgent", "type": "main", "index": 0 }],
[{ "node": "ForensicsAgent", "type": "main", "index": 0 }],
[{ "node": "ReversingAgent", "type": "main", "index": 0 }],
[{ "node": "MiscAgent", "type": "main", "index": 0 }],
[{ "node": "TriageAgent (uncertain)", "type": "main", "index": 0 }]
] },
"TriageAgent (uncertain)": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
"WebAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"PwnAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"CryptoAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"ForensicsAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"ReversingAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"MiscAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"Flag Extractor (middleware)": { "main": [[{ "node": "Scoreboard (CTFd)", "type": "main", "index": 0 }]] }
}
}
Reading the diagram: Left to right. A challenge enters the meta-agent with its description and attached artifacts. The meta-agent emits a domain label and dispatches to one of six specialized agents, each with its own prompt and tool subset. If the meta-agent cannot route with confidence, the challenge goes to the TriageAgent, which runs cheap probes (file type, checksec, strings frequency, HTTP response code) and re-routes with the added signal. Every specialized agent's tool output flows through the Flag Extractor middleware, which scans for flag patterns and submits verified candidates to the CTFd scoreboard. The router is allowed to be uncertain; the agents are not.
Type: Mermaid (flowchart) Purpose: The six domains side by side, with their canonical tool stacks and the reasoning loop that distinguishes each. The diagram a student pins above the desk.
flowchart LR
subgraph WEB["Web"]
W1["Burp headless"]
W2["sqlmap"]
W3["ffuf"]
W4["curated XSS set"]
WL["loop:<br/>send - inspect - escalate"]
W1 --> WL
W2 --> WL
W3 --> WL
W4 --> WL
end
subgraph PWN["Pwn"]
P1["checksec"]
P2["pwntools"]
P3["GDB + pwndbg"]
P4["ROPgadget / one_gadget"]
PL["loop:<br/>protections - offset - chain"]
P1 --> PL
P2 --> PL
P3 --> PL
P4 --> PL
end
subgraph CRY["Crypto"]
C1["CyberChef recipes"]
C2["SageMath"]
C3["frequency analyzers"]
C4["padding-oracle scripts"]
CL["loop:<br/>weakness - math exploit"]
C1 --> CL
C2 --> CL
C3 --> CL
C4 --> CL
end
subgraph FOR["Forensics"]
F1["file / binwalk"]
F2["strings"]
F3["exiftool"]
F4["tshark / Volatility"]
FL["loop:<br/>triage - extract - search"]
F1 --> FL
F2 --> FL
F3 --> FL
F4 --> FL
end
subgraph REV["Reversing"]
R1["Ghidra headless"]
R2["radare2 / rizin"]
R3["ltrace / strace"]
R4["gdb"]
RL["loop:<br/>decompile - locate - invert"]
R1 --> RL
R2 --> RL
R3 --> RL
R4 --> RL
end
subgraph MIS["Misc"]
M1["zsteg"]
M2["stegsolve"]
M3["CyberChef"]
M4["custom Python"]
ML["loop:<br/>medium - canonical transforms"]
M1 --> ML
M2 --> ML
M3 --> ML
M4 --> ML
end
style WEB fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style PWN fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style CRY fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style FOR fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style REV fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style MIS fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style WL fill:#14141f,stroke:#5eead4,color:#5eead4
style PL fill:#14141f,stroke:#5eead4,color:#5eead4
style CL fill:#14141f,stroke:#5eead4,color:#5eead4
style FL fill:#14141f,stroke:#5eead4,color:#5eead4
style RL fill:#14141f,stroke:#5eead4,color:#5eead4
style ML fill:#14141f,stroke:#5eead4,color:#5eead4
Reading the diagram: Six domains, each a self-contained subgraph. The tools inside each subgraph are loaded only when the router dispatches to that domain — they are not present in other agents' manifests. The teal box at the bottom of each is the domain's reasoning loop, encoded into the specialized prompt: Web iterates send-inspect-escalate, Pwn runs checksec before anything else, Crypto hunts a mathematical weakness, Forensics triages artifacts before extracting, Reversing decompiles and locates the comparison to invert, Misc tries canonical transforms against the encoding medium. Reversing is the heaviest context consumer (decompiler output saturates fast); Forensics rarely saturates.
Type: Mermaid (flowchart, comparison) Purpose: Side-by-side timing of the same challenge solved with approval gates versus without. Shows where the time goes and why HITL removal matters in CTF contexts — and the single gate that stays even in autonomous mode.
flowchart TD
START["Challenge dispatched<br/>to PwnAgent"]
START --> A1["Agent: I will run checksec"]
A1 --> GATE1{"HITL gate?<br/>approve"}
GATE1 -->|"wait 25s for human"| A2["checksec executes (2s)"]
GATE1 -.->|"autonomous: skip gate"| A2B["checksec executes (2s)"]
A2 --> A3["Agent: I will run ROPgadget"]
A2B --> A3
A3 --> GATE2{"HITL gate?<br/>approve"}
GATE2 -->|"wait 30s for human"| A4["ROPgadget executes (5s)"]
GATE2 -.->|"autonomous: skip gate"| A4B["ROPgadget executes (5s)"]
A4 --> A5["Agent: send exploit payload"]
A4B --> A5
A5 --> GATE3{"HITL gate?<br/>approve"}
GATE3 -->|"wait 28s for human"| A6["payload executes (1s)"]
GATE3 -.->|"autonomous: skip gate"| A6B["payload executes (1s)"]
A6 --> FLAG["Flag extracted"]
A6B --> FLAG
KEEP["Credential reuse gate<br/>STAYS in autonomous mode"]
KEEP -.->|"even with HITL removed"| CRED["Discovered creds are NOT<br/>auto-reused against other services"]
style GATE1 fill:#2a1810,stroke:#a04000,color:#f0a868
style GATE2 fill:#2a1810,stroke:#a04000,color:#f0a868
style GATE3 fill:#2a1810,stroke:#a04000,color:#f0a868
style A2B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style A4B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style A6B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style KEEP fill:#2a0d0d,stroke:#a00000,color:#f08080
style FLAG fill:#14141f,stroke:#5eead4,color:#5eead4
Reading the diagram: Left column is HITL-enabled, right column (dotted) is autonomous. The same three tool calls — checksec, ROPgadget, exploit payload — take roughly 85 seconds of wall-clock time waiting for human approval in HITL mode, versus 8 seconds of pure execution in autonomous mode. That is the budget that decides whether the harness is competitive. The bottom node is the exception that proves the rule: the credential-reuse gate stays even when every other gate is removed, because credential reuse against other services is the one unbounded-risk action in a CTF context. Remove the speed-killing gates; keep the blast-radius gate.
Type: Mermaid (flowchart) Purpose: What happens between "tool produced output" and "flag submitted to scoreboard". Shows the regex sweep, the decoy-filtering verifier, the rate-limited submission client, and the three CTFd response states.
flowchart TD
TOOL["Tool output<br/>(any domain agent)"]
SWEEP["Regex sweep<br/>over structured output"]
PATTERNS["Flag patterns:<br/>flag{...}, CTF{...},<br/>competition prefix"]
CANDIDATES["Flag candidates<br/>(0, 1, or many)"]
TOOL --> SWEEP
PATTERNS --> SWEEP
SWEEP --> CANDIDATES
CANDIDATES -->|"0 found"| CONTINUE["Agent continues solving"]
CANDIDATES -->|"1+ found"| VERIFIER{"Verifier:<br/>plausibly the answer<br/>to THIS challenge?"}
VERIFIER -->|"decoy / example / description text"| DROP["Drop<br/>log, do not submit"]
VERIFIER -->|"plausible true flag"| SUBMIT["Submission client<br/>POST to CTFd"]
SUBMIT --> RATE["Rate limiter<br/>2-3s between submissions<br/>per-challenge lockout after N wrong"]
RATE --> CTFD{"CTFd response"}
CTFD -->|"correct"| SOLVED["Mark solved<br/>kill parallel attempts<br/>free slot"]
CTFD -->|"incorrect"| LOG["Log attempt<br/>agent continues"]
CTFD -->|"already_solved"| TREAT["Treat as correct<br/>do not re-submit"]
style SWEEP fill:#14141f,stroke:#5eead4,color:#5eead4
style VERIFIER fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style RATE fill:#2a1810,stroke:#a04000,color:#f0a868
style SOLVED fill:#0d2818,stroke:#1e8449,color:#82e0aa
style DROP fill:#2a0d0d,stroke:#a00000,color:#f08080
style CTFD fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
Reading the diagram: Every tool output — every one, from every agent — passes through the regex sweep. The sweep uses configurable patterns (the competition publishes its flag format). Zero matches means the agent keeps solving. One or more matches go to the verifier, which exists because challenge descriptions, examples, and decoy output all contain flag-shaped strings that would burn submission attempts. The verifier confirms plausibility before the submission client touches the scoreboard. The submission client is rate-limited (2–3 seconds between submissions, per-challenge lockout after N wrong attempts) to avoid self-inflicted throttling. CTFd returns one of three states: correct (mark solved, kill parallel attempts, free the slot), incorrect (log and continue), already_solved (treat as correct — a parallel session or teammate got there first; do not re-submit).
Type: Mermaid (flowchart) Purpose: The full multi-challenge parallel harness. Shows the challenge pool, the concurrency-limited dispatcher, isolated sessions running different domains concurrently, the intra-challenge parallelism pattern (two hypotheses, first flag wins), and the single scorecard that gives the operator visibility into all of it.
flowchart TD
POOL["Challenge pool<br/>fetched from CTFd"]
DISP["Dispatcher<br/>concurrency limit N<br/>(typically 3-5 slots)"]
POOL --> DISP
DISP --> SA["Session A"]
DISP --> SB["Session B"]
DISP --> SC["Session C"]
DISP --> SD["Session D"]
SA --> RA["Router -> WebAgent<br/>Challenge 1"]
SB --> RB["Router -> PwnAgent<br/>Challenge 2"]
SC --> RC1["Router -> PwnAgent<br/>Challenge 3: stack overflow hypothesis"]
SD --> RC2["Router -> PwnAgent<br/>Challenge 3: format string hypothesis"]
RA --> FA["FlagExtractor"]
SB --> FB["FlagExtractor"]
SC --> FC1["FlagExtractor"]
SD --> FC2["FlagExtractor"]
FA --> SCORE["Scorecard<br/>(operator view)"]
FB --> SCORE
FC1 --> SCORE
FC2 --> SCORE
WIN{"First flag<br/>from C1 or C2"}
RC1 -.->|"wins"| WIN
RC2 -.->|"loses -> KILL"| KILL["Session killed<br/>slot freed"]
WIN --> SCORE
SCORE --> NEXT["Pull next challenge<br/>when slot frees"]
NEXT --> DISP
style DISP fill:#14141f,stroke:#5eead4,color:#5eead4
style SCORE fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style WIN fill:#0d2818,stroke:#1e8449,color:#82e0aa
style KILL fill:#2a0d0d,stroke:#a00000,color:#f08080
Reading the diagram: The challenge pool feeds a concurrency-limited dispatcher (3–5 slots, bounded by compute and per-team rate limits). Each slot runs an isolated session with its own router dispatch, specialized agent, and flag extractor. Sessions C and D show intra-challenge parallelism: the same challenge, two PwnAgents pursuing two hypotheses (stack overflow vs format string), first flag wins, the losing session is killed and its slot freed. Sessions do not share memory — sharing would reintroduce the context pollution that domain routing was designed to eliminate. Every session reports to a single scorecard, which is the operator's only sane view into five concurrent solve attempts. When a slot frees (challenge solved or hypothesis lost), the dispatcher pulls the next challenge from the pool. The scoreboard moves in parallel; the scorecard is how you keep track of it.
# Diagrams — Module S04: CTF Harness Engineering
> All Mermaid validated in Mermaid Live Editor. n8n JSON is structurally valid and importable.
---
## Diagram 1 — The CTF Domain Router (n8n)
**Type**: n8n workflow (importable JSON)
**Purpose**: The primary visual — the meta-agent-plus-specialists architecture. Shows a challenge entering the router, being classified into one of six domains, and dispatched to the matching specialized agent with its tool subset.
```json
{
"name": "S04 — CTF Domain Router",
"nodes": [
{ "name": "Challenge In", "type": "n8n-nodes-base.manualTrigger", "position": [200, 360], "notes": "Challenge description + attached artifacts (binary, pcap, image, URL)" },
{ "name": "Meta-Agent / Classifier", "type": "n8n-nodes-base.set", "position": [440, 360], "notes": "Reads description + file-type signal. Emits domain label. Routes to specialist. Allowed to be uncertain." },
{ "name": "WebAgent", "type": "n8n-nodes-base.set", "position": [760, 80], "notes": "Burp headless, sqlmap, ffuf, curated XSS payloads. Request-send-inspect loop." },
{ "name": "PwnAgent", "type": "n8n-nodes-base.set", "position": [760, 200], "notes": "pwntools, GDB+pwndbg, checksec, ROPgadget, one_gadget. checksec-before-reasoning loop." },
{ "name": "CryptoAgent", "type": "n8n-nodes-base.set", "position": [760, 320], "notes": "CyberChef recipes, SageMath, frequency analyzers, padding-oracle scripts." },
{ "name": "ForensicsAgent", "type": "n8n-nodes-base.set", "position": [760, 440], "notes": "binwalk, strings, exiftool, tshark, Volatility. Artifact-triage loop." },
{ "name": "ReversingAgent", "type": "n8n-nodes-base.set", "position": [760, 560], "notes": "Ghidra headless, radare2/rizin, ltrace, strace, gdb. Decompile-triage loop. Heaviest context consumer." },
{ "name": "MiscAgent", "type": "n8n-nodes-base.set", "position": [760, 680], "notes": "zsteg, stegsolve, CyberChef, custom Python. Variable reasoning loop." },
{ "name": "TriageAgent (uncertain)", "type": "n8n-nodes-base.set", "position": [440, 540], "notes": "When meta-agent is uncertain: runs cheap probes (file, checksec, strings freq, HTTP code) then re-routes." },
{ "name": "Flag Extractor (middleware)", "type": "n8n-nodes-base.set", "position": [1020, 360], "notes": "Regex sweep over every tool output. Surfaces flag candidates to verifier." },
{ "name": "Scoreboard (CTFd)", "type": "n8n-nodes-base.httpRequest", "position": [1240, 360], "notes": "POST /api/v1/challenges/attempt with token. Rate-limited. correct/incorrect/already_solved handling." }
],
"connections": {
"Challenge In": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
"Meta-Agent / Classifier": { "main": [
[{ "node": "WebAgent", "type": "main", "index": 0 }],
[{ "node": "PwnAgent", "type": "main", "index": 0 }],
[{ "node": "CryptoAgent", "type": "main", "index": 0 }],
[{ "node": "ForensicsAgent", "type": "main", "index": 0 }],
[{ "node": "ReversingAgent", "type": "main", "index": 0 }],
[{ "node": "MiscAgent", "type": "main", "index": 0 }],
[{ "node": "TriageAgent (uncertain)", "type": "main", "index": 0 }]
] },
"TriageAgent (uncertain)": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
"WebAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"PwnAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"CryptoAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"ForensicsAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"ReversingAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"MiscAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
"Flag Extractor (middleware)": { "main": [[{ "node": "Scoreboard (CTFd)", "type": "main", "index": 0 }]] }
}
}
```
**Reading the diagram**: Left to right. A challenge enters the meta-agent with its description and attached artifacts. The meta-agent emits a domain label and dispatches to one of six specialized agents, each with its own prompt and tool subset. If the meta-agent cannot route with confidence, the challenge goes to the TriageAgent, which runs cheap probes (file type, checksec, strings frequency, HTTP response code) and re-routes with the added signal. Every specialized agent's tool output flows through the Flag Extractor middleware, which scans for flag patterns and submits verified candidates to the CTFd scoreboard. The router is allowed to be uncertain; the agents are not.
---
## Diagram 2 — Per-Domain Tool Stacks
**Type**: Mermaid (flowchart)
**Purpose**: The six domains side by side, with their canonical tool stacks and the reasoning loop that distinguishes each. The diagram a student pins above the desk.
```mermaid
flowchart LR
subgraph WEB["Web"]
W1["Burp headless"]
W2["sqlmap"]
W3["ffuf"]
W4["curated XSS set"]
WL["loop:<br/>send - inspect - escalate"]
W1 --> WL
W2 --> WL
W3 --> WL
W4 --> WL
end
subgraph PWN["Pwn"]
P1["checksec"]
P2["pwntools"]
P3["GDB + pwndbg"]
P4["ROPgadget / one_gadget"]
PL["loop:<br/>protections - offset - chain"]
P1 --> PL
P2 --> PL
P3 --> PL
P4 --> PL
end
subgraph CRY["Crypto"]
C1["CyberChef recipes"]
C2["SageMath"]
C3["frequency analyzers"]
C4["padding-oracle scripts"]
CL["loop:<br/>weakness - math exploit"]
C1 --> CL
C2 --> CL
C3 --> CL
C4 --> CL
end
subgraph FOR["Forensics"]
F1["file / binwalk"]
F2["strings"]
F3["exiftool"]
F4["tshark / Volatility"]
FL["loop:<br/>triage - extract - search"]
F1 --> FL
F2 --> FL
F3 --> FL
F4 --> FL
end
subgraph REV["Reversing"]
R1["Ghidra headless"]
R2["radare2 / rizin"]
R3["ltrace / strace"]
R4["gdb"]
RL["loop:<br/>decompile - locate - invert"]
R1 --> RL
R2 --> RL
R3 --> RL
R4 --> RL
end
subgraph MIS["Misc"]
M1["zsteg"]
M2["stegsolve"]
M3["CyberChef"]
M4["custom Python"]
ML["loop:<br/>medium - canonical transforms"]
M1 --> ML
M2 --> ML
M3 --> ML
M4 --> ML
end
style WEB fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style PWN fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style CRY fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style FOR fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style REV fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style MIS fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style WL fill:#14141f,stroke:#5eead4,color:#5eead4
style PL fill:#14141f,stroke:#5eead4,color:#5eead4
style CL fill:#14141f,stroke:#5eead4,color:#5eead4
style FL fill:#14141f,stroke:#5eead4,color:#5eead4
style RL fill:#14141f,stroke:#5eead4,color:#5eead4
style ML fill:#14141f,stroke:#5eead4,color:#5eead4
```
**Reading the diagram**: Six domains, each a self-contained subgraph. The tools inside each subgraph are loaded only when the router dispatches to that domain — they are not present in other agents' manifests. The teal box at the bottom of each is the domain's reasoning loop, encoded into the specialized prompt: Web iterates send-inspect-escalate, Pwn runs checksec before anything else, Crypto hunts a mathematical weakness, Forensics triages artifacts before extracting, Reversing decompiles and locates the comparison to invert, Misc tries canonical transforms against the encoding medium. Reversing is the heaviest context consumer (decompiler output saturates fast); Forensics rarely saturates.
---
## Diagram 3 — HITL vs Autonomous: The Speed Comparison
**Type**: Mermaid (flowchart, comparison)
**Purpose**: Side-by-side timing of the same challenge solved with approval gates versus without. Shows where the time goes and why HITL removal matters in CTF contexts — and the single gate that stays even in autonomous mode.
```mermaid
flowchart TD
START["Challenge dispatched<br/>to PwnAgent"]
START --> A1["Agent: I will run checksec"]
A1 --> GATE1{"HITL gate?<br/>approve"}
GATE1 -->|"wait 25s for human"| A2["checksec executes (2s)"]
GATE1 -.->|"autonomous: skip gate"| A2B["checksec executes (2s)"]
A2 --> A3["Agent: I will run ROPgadget"]
A2B --> A3
A3 --> GATE2{"HITL gate?<br/>approve"}
GATE2 -->|"wait 30s for human"| A4["ROPgadget executes (5s)"]
GATE2 -.->|"autonomous: skip gate"| A4B["ROPgadget executes (5s)"]
A4 --> A5["Agent: send exploit payload"]
A4B --> A5
A5 --> GATE3{"HITL gate?<br/>approve"}
GATE3 -->|"wait 28s for human"| A6["payload executes (1s)"]
GATE3 -.->|"autonomous: skip gate"| A6B["payload executes (1s)"]
A6 --> FLAG["Flag extracted"]
A6B --> FLAG
KEEP["Credential reuse gate<br/>STAYS in autonomous mode"]
KEEP -.->|"even with HITL removed"| CRED["Discovered creds are NOT<br/>auto-reused against other services"]
style GATE1 fill:#2a1810,stroke:#a04000,color:#f0a868
style GATE2 fill:#2a1810,stroke:#a04000,color:#f0a868
style GATE3 fill:#2a1810,stroke:#a04000,color:#f0a868
style A2B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style A4B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style A6B fill:#0d2818,stroke:#1e8449,color:#82e0aa
style KEEP fill:#2a0d0d,stroke:#a00000,color:#f08080
style FLAG fill:#14141f,stroke:#5eead4,color:#5eead4
```
**Reading the diagram**: Left column is HITL-enabled, right column (dotted) is autonomous. The same three tool calls — checksec, ROPgadget, exploit payload — take roughly 85 seconds of wall-clock time waiting for human approval in HITL mode, versus 8 seconds of pure execution in autonomous mode. That is the budget that decides whether the harness is competitive. The bottom node is the exception that proves the rule: the credential-reuse gate stays even when every other gate is removed, because credential reuse against other services is the one unbounded-risk action in a CTF context. Remove the speed-killing gates; keep the blast-radius gate.
---
## Diagram 4 — The Flag Extraction Pipeline
**Type**: Mermaid (flowchart)
**Purpose**: What happens between "tool produced output" and "flag submitted to scoreboard". Shows the regex sweep, the decoy-filtering verifier, the rate-limited submission client, and the three CTFd response states.
```mermaid
flowchart TD
TOOL["Tool output<br/>(any domain agent)"]
SWEEP["Regex sweep<br/>over structured output"]
PATTERNS["Flag patterns:<br/>flag{...}, CTF{...},<br/>competition prefix"]
CANDIDATES["Flag candidates<br/>(0, 1, or many)"]
TOOL --> SWEEP
PATTERNS --> SWEEP
SWEEP --> CANDIDATES
CANDIDATES -->|"0 found"| CONTINUE["Agent continues solving"]
CANDIDATES -->|"1+ found"| VERIFIER{"Verifier:<br/>plausibly the answer<br/>to THIS challenge?"}
VERIFIER -->|"decoy / example / description text"| DROP["Drop<br/>log, do not submit"]
VERIFIER -->|"plausible true flag"| SUBMIT["Submission client<br/>POST to CTFd"]
SUBMIT --> RATE["Rate limiter<br/>2-3s between submissions<br/>per-challenge lockout after N wrong"]
RATE --> CTFD{"CTFd response"}
CTFD -->|"correct"| SOLVED["Mark solved<br/>kill parallel attempts<br/>free slot"]
CTFD -->|"incorrect"| LOG["Log attempt<br/>agent continues"]
CTFD -->|"already_solved"| TREAT["Treat as correct<br/>do not re-submit"]
style SWEEP fill:#14141f,stroke:#5eead4,color:#5eead4
style VERIFIER fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style RATE fill:#2a1810,stroke:#a04000,color:#f0a868
style SOLVED fill:#0d2818,stroke:#1e8449,color:#82e0aa
style DROP fill:#2a0d0d,stroke:#a00000,color:#f08080
style CTFD fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
```
**Reading the diagram**: Every tool output — every one, from every agent — passes through the regex sweep. The sweep uses configurable patterns (the competition publishes its flag format). Zero matches means the agent keeps solving. One or more matches go to the verifier, which exists because challenge descriptions, examples, and decoy output all contain flag-shaped strings that would burn submission attempts. The verifier confirms plausibility before the submission client touches the scoreboard. The submission client is rate-limited (2–3 seconds between submissions, per-challenge lockout after N wrong attempts) to avoid self-inflicted throttling. CTFd returns one of three states: correct (mark solved, kill parallel attempts, free the slot), incorrect (log and continue), already_solved (treat as correct — a parallel session or teammate got there first; do not re-submit).
---
## Diagram 5 — Parallel Solve Architecture (multi-challenge)
**Type**: Mermaid (flowchart)
**Purpose**: The full multi-challenge parallel harness. Shows the challenge pool, the concurrency-limited dispatcher, isolated sessions running different domains concurrently, the intra-challenge parallelism pattern (two hypotheses, first flag wins), and the single scorecard that gives the operator visibility into all of it.
```mermaid
flowchart TD
POOL["Challenge pool<br/>fetched from CTFd"]
DISP["Dispatcher<br/>concurrency limit N<br/>(typically 3-5 slots)"]
POOL --> DISP
DISP --> SA["Session A"]
DISP --> SB["Session B"]
DISP --> SC["Session C"]
DISP --> SD["Session D"]
SA --> RA["Router -> WebAgent<br/>Challenge 1"]
SB --> RB["Router -> PwnAgent<br/>Challenge 2"]
SC --> RC1["Router -> PwnAgent<br/>Challenge 3: stack overflow hypothesis"]
SD --> RC2["Router -> PwnAgent<br/>Challenge 3: format string hypothesis"]
RA --> FA["FlagExtractor"]
SB --> FB["FlagExtractor"]
SC --> FC1["FlagExtractor"]
SD --> FC2["FlagExtractor"]
FA --> SCORE["Scorecard<br/>(operator view)"]
FB --> SCORE
FC1 --> SCORE
FC2 --> SCORE
WIN{"First flag<br/>from C1 or C2"}
RC1 -.->|"wins"| WIN
RC2 -.->|"loses -> KILL"| KILL["Session killed<br/>slot freed"]
WIN --> SCORE
SCORE --> NEXT["Pull next challenge<br/>when slot frees"]
NEXT --> DISP
style DISP fill:#14141f,stroke:#5eead4,color:#5eead4
style SCORE fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
style WIN fill:#0d2818,stroke:#1e8449,color:#82e0aa
style KILL fill:#2a0d0d,stroke:#a00000,color:#f08080
```
**Reading the diagram**: The challenge pool feeds a concurrency-limited dispatcher (3–5 slots, bounded by compute and per-team rate limits). Each slot runs an isolated session with its own router dispatch, specialized agent, and flag extractor. Sessions C and D show intra-challenge parallelism: the same challenge, two PwnAgents pursuing two hypotheses (stack overflow vs format string), first flag wins, the losing session is killed and its slot freed. Sessions do not share memory — sharing would reintroduce the context pollution that domain routing was designed to eliminate. Every session reports to a single scorecard, which is the operator's only sane view into five concurrent solve attempts. When a slot frees (challenge solved or hypothesis lost), the dispatcher pulls the next challenge from the pool. The scoreboard moves in parallel; the scorecard is how you keep track of it.