docs(release): align workflow instructions, hypothesis discipline, and release v3.0.1 (#64)

Merge dev into master
This commit is contained in:
Dan
2026-08-04 17:18:44 +01:00
committed by GitHub
parent 1ef97dee9e
commit 5071b9689f
14 changed files with 441 additions and 31 deletions
+16 -9
View File
@@ -36,14 +36,18 @@ Violin **must not** touch a target (no curl, nmap, browser, web_search for the t
| 4 | `$ENG_DIR/hypotheses.md` (hypothesis board) | `init-engagement` | `check-bootstrap` |
| 5 | `$ENG_DIR/state/history.md` (command log) | `init-engagement` | `check-bootstrap` |
**Session-start/skill-load precondition** (code-enforced via `check-command`):
**Session-start/skill-delivery precondition** (code-enforced via the plugin hook):
Launch with `hermes chat --skills pentest` when possible. If the profile was already started, load `pentest` before creating the marker below. Check the current state at any time with `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py status --eng-dir "$ENG_DIR" --section skill`.
Launch with `hermes chat --skills pentest` when possible. For every active PTT
task, call `violin_record_ptt` with its routed `skill`, `technique`, and any
required hypothesis. The first call prepares real `skill_view` content without
changing the PTT; after that result returns, repeat the same call in the next
model continuation to bind the receipt. Check the current state with
`violin_status` or `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py
status --eng-dir "$ENG_DIR" --section skill`.
```text
printf 'skill-loaded: %s\n' "$(date -Iseconds)" > "$ENG_DIR/state/.skill-loaded-<session-id>"
python $HOME/.hermes/profiles/violin/scripts/violin_guard.py status --eng-dir "$ENG_DIR" --section skill
```
Never create `.skill-loaded-*` files. Legacy markers are migration hints only
and are not evidence that Hermes delivered a skill.
Then pass the same session ID into every target-touching command check — `--eng-dir` is mandatory because it activates the PTT, history, hypothesis, and synchronization guards:
@@ -51,7 +55,10 @@ Then pass the same session ID into every target-touching command check — `--en
python $HOME/.hermes/profiles/violin/scripts/violin_guard.py check-command --scope $ENG_DIR/scope/scope.yaml --eng-dir "$ENG_DIR" --target "<primary-host-or-ip>" --session-id "<session-id>" --phase <PHASE> --command "<cmd>"
```
**Run-to-completion rule:** once created, a skill-load marker holds for the current engagement work block. Do not recreate it between commands. After `/goal set` or context compression, continue in the same conversation, re-read `$ENG_DIR/state/`, and create or verify the marker for the current session ID before the next target action.
**Run-to-completion rule:** after `/goal set` or context compression, continue
in the same conversation, re-read `$ENG_DIR/state/`, and call `violin_status`.
If the receipt binding needs recovery, follow its reported action; do not
manufacture marker files or assume the prior skill delivery still applies.
**What this prevents:** the Nimbus-style failure where the agent skipped scoping/bootstrap, never created a PTT or hypothesis board, and burned the entire 20-turn budget calling `curl` variations on the same endpoint without tracking what it learned.
@@ -62,7 +69,7 @@ The Violin agent **may**:
- **Run pentest tools** through `violin_exec`, `violin_exec_burst`, or the typed guard adapters. `violin_exec` has no binary allowlist: any installed non-interactive Kali/Parrot CLI tool may run against the explicit in-scope target while retaining scope, phase, PTT, hypothesis, history, evidence, timeout, and sync gates. Installation, root, hardware, service, GUI, and interactive-TTY requirements remain runtime constraints. Raw `terminal` is limited to host-local preparation and administration.
- **Use `execute_code` only with an audit header** — its first line must be `# violin: {"eng_dir":"...","phase":"...","target":"...","session_id":"..."}`. Violin validates that metadata, saves the submitted source under `$ENG_DIR/evidence/<phase>/`, and appends its digest and completion status to history. This is auditability, not a substitute for typed target execution.
- **Research online** — Use the `web` toolset (`web_search`, `web_extract`) to find CVEs, exploits, PoC code, security advisories, tool documentation, and tutorials.
- **Browse target websites** — Use the `browser` toolset (`browser_navigate`, `browser_click`, `browser_type`, `browser_vision`, `browser_snapshot`) to navigate target web apps, interact with login forms and dashboards, capture screenshots for evidence, inspect DOM elements, crawl visible links, and enumerate client-side routes.
- **Browse target websites carefully** — Use the `browser` toolset only for an explicitly approved in-scope URL after the PTT and skill-receipt gates pass. In v3.0.0 the hook gates browser activity on that workflow binding; it is not a network-level browser allowlist and does not contain redirects, subresources, forms, JavaScript navigation, history, or WebSockets. Stop and review the URL after each navigation or action; do not deliberately follow an out-of-scope redirect.
- **Discover installed tools** — Check what tools exist on the workstation using `command -v` or filesystem searches.
- **Read tool docs** — Use `read_file`, `terminal`, or `browser` to read help pages, man pages, READMEs, and documentation.
- **Write scripts** — Create Python, Bash, PowerShell, or other scripts under `$ENG_DIR/exploits/` for exploitation, automation, or evidence processing.
@@ -75,7 +82,7 @@ The Violin agent **may**:
- **Summarise results** — After each logical tool batch, report: (a) what ran, (b) key results found or nothing notable, (c) evidence saved where. 3-5 lines max. Do not dump raw command output.
- **Check in next-step** — After each sub-phase or completed batch, ask the user what to do next with concrete options. E.g. "DNS enumeration done. Found 3 subdomains. Next: tech detection with whatweb, or move to active scanning with nmap?" Do not advance the workflow silently.
- **Ask via `clarify`** — Ask targeted questions during scoping and throughout the engagement whenever ambiguity arises.
- **Delegate** — Use background `terminal` processes to run parallel reconnaissance or long-running tasks.
- **Delegate** — Use `violin_exec` with `background=true` or `violin_listener` for tracked long-running work. Do not start target-facing background jobs through raw `terminal`.
- **Use vision** — Capture and analyse screenshots with `browser_vision` or `vision_analyze` for evidence and context understanding.
## Conversation & Memory Isolation
+60
View File
@@ -0,0 +1,60 @@
# Strategic-Automation/violin — AI Developer Guidance
> Workspace-scoped developer guidance for AI coding agents (Antigravity, Hermes, Codex, Cursor, etc.) developing, testing, or maintaining the `violin` codebase.
>
> *Note:* For deployed end-user Hermes pentest installations, runtime identity and engagement rules are packaged in `SOUL.md`, `.hermes.md`, and `skills/pentest/SKILL.md`. This file governs AI agent developer behavior within this repository workspace.
---
## 1. Stack & Environment Setup
- **Python Version:** 3.11 (pinned in `.python-version` and `pyproject.toml` to match Hermes runtime).
- **Package Manager:** `uv` (use `uv sync --dev` to sync development environment).
- **Virtual Environment:** `.venv` created and managed via `uv`.
---
## 2. Mandatory Verification Commands
Run these commands to verify any code changes before declaring completion:
```bash
# 1. Run full test suite (must pass 100%)
uv run pytest
# 2. Run linter check
uv run ruff check .
# 3. Check code formatting (fix with `uv run ruff format .`)
uv run ruff format --check .
# 4. Validate release gate
uv run python -m plugins.violin_guard.release
```
---
## 3. Code Conventions & Architecture
- **Hermes Runtime Contract:** All target-touching CLI command execution in Violin engagements MUST go through `plugins.violin_guard` typed Hermes tool calls (`violin_exec`, `violin_record_ptt`, `violin_review_batch`, `violin_record_hypothesis`, `violin_target`, `violin_status`, `violin_listener`). Never invoke flat CLI scripts (`python violin_guard.py`) as a substitute for typed tool calls.
- **Fail-Closed Validation:** All state parsers (`hypotheses.py`, `ptt.py`, `command.py`, `targets.py`) must validate inputs fail-closed before mutating filesystem state.
- **Section Preservation:** File rewriters (`_rewrite_hypotheses`, `update_task`) must preserve structural template sections (`## Observations`, `## Decoy Trail`, `## Research Log`, `## Resolved Theories`, table columns).
- **Typed Schemas:** Use Pydantic v2 `BaseModel` models in `plugins/violin_guard/schemas.py` for all tool parameter specifications.
- **File Encoding:** Always specify `encoding="utf-8"` explicitly for all text file read/write operations.
---
## 4. Git & Branching Strategy
- **Feature/Docs Branches:** Use `codex/<topic>` or `dev`.
- **Merge Flow:** `codex/<topic>` ──► `dev` ──► PR to `master`.
- **Master Branch:** `master` is protected by GitHub repository rules (`GH013`); production releases require a Pull Request.
---
## 5. Hard Boundaries (What AI Agents Must Never Do)
1. **NEVER Bypass Target Execution Guards:** Never run target-touching commands directly in raw shell without `violin_exec` or `violin_exec_burst`.
2. **NEVER Swallow Exceptions or Patch Tests Superficialy:** Fix underlying root causes; never mask errors, return dummy fallbacks, or comment out failing assertions.
3. **NEVER Hardcode Target IPs:** Resolve target hosts dynamically via `violin_target` or `scope.yaml`.
4. **NEVER Declare Success Without Empirical Verification:** Always run `uv run pytest` and `uv run ruff check .` to prove zero regressions before finishing work.
+1 -1
View File
@@ -31,7 +31,7 @@ hermes -p violin
<tr><td width="280"><b>🔬 31 Methodology Playbooks</b></td><td>7 operational playbooks (five execution phases, optional post-exploitation, and the tools catalog) + 24 vulnerability-class playbooks, routed across the `pentest`, `web-attacks`, and `access-control` skills.</td></tr>
<tr><td><b>🛡️ Multi-Layer Safety</b></td><td>Interactive scoping (9 questions) → scope validation → guard check → approval gates — every target-touching command validated before execution.</td></tr>
<tr><td><b>🧠 Pentesting Task Tree</b></td><td>Structured artifact tracking every task via `[x]/[ ]/[~]` markers across phases, with executor-owned history, hypothesis linking, and guard-bound batch reviews.</td></tr>
<tr><td><b>🌐 Browser + Web Research</b></td><td>Browser toolset for website enumeration. Web toolset for CVE lookup, exploit search, and OSINT.</td></tr>
<tr><td><b>🌐 Browser + Web Research</b></td><td>Browser toolset for approved in-scope website enumeration; v3.0.0 gates the engagement workflow but does not provide a network-level browser allowlist. Web toolset for CVE lookup, exploit search, and OSINT.</td></tr>
<tr><td><b>📋 Evidence-Driven Reporting</b></td><td>Reproducible evidence with screenshots, tool output, and request/response pairs. CVSS 3.1 + 4.0 crosswalks and optional remediation patches.</td></tr>
<tr><td><b>🔗 Hermes-Native</b></td><td>Inherits your existing Hermes provider, model, and tool backends. Violin introduces no separate credential store or broker.</td></tr>
</table>
+65 -12
View File
@@ -52,6 +52,12 @@ _FIELD_NAMES = {
"source evidence": "source_evidence",
"runtime evidence": "runtime_evidence",
"updated": "updated",
"confidence": "confidence",
"timebox": "timebox",
"cheapest test": "cheapest_test",
"kill criteria": "kill_criteria",
"next step": "next_step",
"linked findings": "linked_findings",
}
@@ -60,6 +66,9 @@ class Hypothesis:
id: str
title: str
status: str = "Candidate"
confidence: str = ""
timebox: str = ""
cheapest_test: str = ""
phase: str = ""
service: str = ""
port: str = ""
@@ -72,7 +81,10 @@ class Hypothesis:
test_command: str = ""
test_response: str = ""
verification_status: str = ""
kill_criteria: str = ""
rejection_reason: str = ""
next_step: str = ""
linked_findings: str = ""
candidate_source: str = ""
entry_point: str = ""
data_flow: str = ""
@@ -88,6 +100,9 @@ class Hypothesis:
"id": self.id,
"title": self.title,
"status": self.canonical_status(),
"confidence": self.confidence,
"timebox": self.timebox,
"cheapest_test": self.cheapest_test,
"phase": self.phase,
"service": self.service,
"port": self.port,
@@ -100,7 +115,10 @@ class Hypothesis:
"test_command": self.test_command,
"test_response": self.test_response,
"verification_status": self.verification_status,
"kill_criteria": self.kill_criteria,
"rejection_reason": self.rejection_reason,
"next_step": self.next_step,
"linked_findings": self.linked_findings,
"candidate_source": self.candidate_source,
"entry_point": self.entry_point,
"data_flow": self.data_flow,
@@ -113,7 +131,12 @@ class Hypothesis:
now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M")
lines = [f"### H-{self.id}: {self.title}"]
lines.append(f"- **Status:** {self.canonical_status()}")
lines.append(f"- **Updated:** {self.updated or now}")
if self.confidence:
lines.append(f"- **Confidence:** {self.confidence}")
if self.timebox:
lines.append(f"- **Timebox:** {self.timebox}")
if self.cheapest_test:
lines.append(f"- **Cheapest test:** {self.cheapest_test}")
if self.phase:
lines.append(f"- **Phase:** {self.phase}")
if self.service:
@@ -138,8 +161,14 @@ class Hypothesis:
lines.append(f"- **Test Response:** {self.test_response}")
if self.verification_status:
lines.append(f"- **Verification Status:** {self.verification_status}")
if self.kill_criteria:
lines.append(f"- **Kill criteria:** {self.kill_criteria}")
if self.rejection_reason:
lines.append(f"- **Rejection Reason:** {self.rejection_reason}")
if self.next_step:
lines.append(f"- **Next step:** {self.next_step}")
if self.linked_findings:
lines.append(f"- **Linked findings:** {self.linked_findings}")
for label, value in (
("Candidate Source", self.candidate_source),
("Entry Point", self.entry_point),
@@ -149,7 +178,7 @@ class Hypothesis:
):
if value:
lines.append(f"- **{label}:** {value}")
lines.append(f"- **Updated:** {self.updated or now} UTC")
lines.append(f"- **Updated:** {self.updated or now}")
return "\n".join(lines) + "\n"
@@ -322,6 +351,9 @@ def update_hypothesis(
id=merged_fields["id"],
title=merged_fields.get("title", "") or f"Hypothesis {merged_fields['id']}",
status=(merged_fields.get("status") or "Candidate"),
confidence=(merged_fields.get("confidence") or "").strip(),
timebox=(merged_fields.get("timebox") or "").strip(),
cheapest_test=(merged_fields.get("cheapest_test") or "").strip(),
phase=(merged_fields.get("phase") or "").strip(),
service=(merged_fields.get("service") or "").strip(),
port=(merged_fields.get("port") or "").strip(),
@@ -334,7 +366,10 @@ def update_hypothesis(
test_command=(merged_fields.get("test_command") or "").strip(),
test_response=(merged_fields.get("test_response") or "").strip(),
verification_status=(merged_fields.get("verification_status") or "").strip(),
kill_criteria=(merged_fields.get("kill_criteria") or "").strip(),
rejection_reason=(merged_fields.get("rejection_reason") or "").strip(),
next_step=(merged_fields.get("next_step") or "").strip(),
linked_findings=(merged_fields.get("linked_findings") or "").strip(),
candidate_source=(merged_fields.get("candidate_source") or "").strip(),
entry_point=(merged_fields.get("entry_point") or "").strip(),
data_flow=(merged_fields.get("data_flow") or "").strip(),
@@ -378,23 +413,41 @@ def update_hypothesis(
def _rewrite_hypotheses(path: Path, hypotheses_list: list[Hypothesis]) -> None:
"""Rewrite the entire hypotheses file."""
"""Rewrite the hypotheses file while preserving structural sections (Decoy Trail, Observations, etc.)."""
path.parent.mkdir(parents=True, exist_ok=True)
template = path.read_text(encoding="utf-8") if path.exists() else "# Hypothesis Board\n\n"
# Template instructions are an HTML comment containing an example H-001
# heading. Remove that comment before locating real records, otherwise a
# newly written hypothesis is accidentally placed inside the comment.
# Remove template instruction HTML comment if present
import re
comment_start = template.find("<!--")
if comment_start != -1:
comment_end = template.find("-->", comment_start)
if comment_end != -1:
template = template[:comment_start] + template[comment_end + 3 :]
# Keep any header content before first hypothesis
header_end = template.find("### H-")
if header_end == -1:
header = template.strip() + "\n\n"
# Preserve section structure (e.g. ## Active Theories ... ## Observations ... ## Decoy Trail)
active_heading = "## Active Theories"
active_pos = template.find(active_heading)
if active_pos != -1:
header = template[: active_pos + len(active_heading)].strip() + "\n\n"
# Find next section header after Active Theories
next_sec = re.search(r"\n##\s+(?!Active Theories)", template[active_pos:])
trailer = template[active_pos + next_sec.start() + 1 :].lstrip() if next_sec else ""
else:
header = template[:header_end].rstrip() + "\n\n"
# Fallback: locate first hypothesis heading starting with ### H-
first_h = re.search(r"^###\s+H-", template, re.MULTILINE)
if first_h:
header = template[: first_h.start()].rstrip() + "\n\n"
next_sec = re.search(r"\n##\s+", template[first_h.start() :])
trailer = (
template[first_h.start() + next_sec.start() + 1 :].lstrip() if next_sec else ""
)
else:
header = template.strip() + "\n\n"
trailer = ""
body = "\n".join(h.to_markdown() for h in hypotheses_list)
path.write_text(header + body, encoding="utf-8")
content = header + body + ("\n\n" + trailer if trailer else "\n")
path.write_text(content, encoding="utf-8")
+11
View File
@@ -58,6 +58,11 @@ class RecordHypothesisArgsModel(BaseModel):
id: str = ""
title: str = ""
status: str = ""
confidence: str = Field("", description="0.1-1.0 guesstimate; escalate only with evidence")
timebox: str = Field("", description="e.g. 4 tool batches or 30 min — then re-evaluate")
cheapest_test: str = Field(
"", description="Single cheapest probe that discriminates this theory"
)
phase: str = ""
target: str = Field("", description="target host/IP (must be in scope)")
vuln_class: str = ""
@@ -80,9 +85,15 @@ class RecordHypothesisArgsModel(BaseModel):
test_command: str = Field("", description="Exact syntax tested, including argument order")
test_response: str = Field("", description="Exact decisive response or error")
verification_status: str = ""
kill_criteria: str = Field(
"",
description="Evidence that contradicts, or no new info in N batches — then kill & log in Decoy Trail",
)
rejection_reason: str = Field(
"", description="Why a rejected hypothesis is safe to stop pursuing"
)
next_step: str = ""
linked_findings: str = ""
candidate_source: str = ""
entry_point: str = ""
data_flow: str = ""
+21 -6
View File
@@ -55,7 +55,7 @@ The agent operates as the **Pentest Lead** with supervised autonomy only after s
**Available capabilities:**
- **`terminal`** — host-local preparation and administration only; best-effort policy blocks obvious target traffic, while target-touching commands use the Violin guard tools below
- **`web`** (`web_search`, `web_extract`) — research: CVE lookup, exploit search, OSINT, documentation, PoC search
- **`browser`** (`browser_navigate`, `browser_click`, `browser_type`, `browser_vision`, `browser_snapshot`) — website enumeration and interaction: navigate target web apps, interact with login forms and dashboards, capture screenshots, inspect DOM, crawl visible links, enumerate client-side routes
- **`browser`** (`browser_navigate`, `browser_click`, `browser_type`, `browser_vision`, `browser_snapshot`) — website enumeration and interaction for explicitly approved in-scope URLs. The v3.0.0 hook checks the PTT/skill-receipt binding before supported browser actions, but it is not a network-level scope firewall: redirects, subresources, forms, JavaScript navigation, history, and WebSockets are not contained by the guard. Re-check the visible URL after each action and stop for review if it leaves scope.
- **`file`** (`write_file`, `read_file`, `search_files`) — evidence collection, report writing, config manipulation
- **`code_execution`** — host-local Python automation only with a first-line audit header: `# violin: {"eng_dir":"...","phase":"...","target":"...","session_id":"..."}`. The source and completion record are saved to the named engagement; use typed Violin tools for target execution.
- **`clarify`** — ask the user structured questions during scoping and when decisions are needed
@@ -140,10 +140,16 @@ The phase workflow is mandatory for the entire session, including long, compress
1. Check/update `todo` with a single active `phase-gate` item named for the current phase.
2. Confirm an approved `$ENG_DIR/scope/scope.yaml` exists before touching any target. If it does not, remain in SCOPING and ask via `clarify`. Verify with `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py validate-scope --scope $ENG_DIR/scope/scope.yaml` (exit 0 required).
For an authorized HTB/CTF lab, `init-engagement --ctf --host <ip> --session-id <id> "$ENG_DIR"` creates a ready-to-test scope and active RECON PTT row; prepare and bind the routed skill before target activity. This exact guard CLI action is host-local and accepts a direct `--host` value through `terminal`; never hide it in a file, environment variable, or shell substitution.
3. **Read and activate the PTT task**`read_file path="$ENG_DIR/state/ptt.md"` — and select the next open `[ ]` task for the current phase. Before any target command, mark exactly one task `[~]`:
```bash
python $HOME/.hermes/profiles/violin/scripts/violin_guard.py record-ptt --eng-dir "$ENG_DIR" --id PT-XXX --status "[~]" --note "starting task" --skill pentest --technique "<technique>" [--hypothesis-id H-001]
3. **Read and activate the PTT task**`read_file path="$ENG_DIR/state/ptt.md"` — and select the next open `[ ]` task for the current phase. Before any target command, mark exactly one task `[~]` through the Hermes tool:
```text
violin_record_ptt(
eng_dir="$ENG_DIR", id="PT-XXX", status="[~]", note="starting task",
skill="pentest", technique="<technique>", hypothesis_id="H-001" # when required
)
```
The first call prepares the routed skill receipt; repeat it after that tool
result returns to bind the transition. The standalone CLI is diagnostic and
administrative; it cannot substitute for Hermes `skill_view` delivery.
With no pending batch, this is the one permitted PTT start transition. The guard hard-blocks target execution when there is no unambiguous active task or the task sits under another phase heading. The executor never changes this row or its `*Last updated*` timestamp. After each bounded batch, review the results and call `violin_review_batch` with `[~]`, `[x]`, `[!]`, or `[-]` plus a truthful result summary.
4. **Command history is executor-owned**`violin_exec` appends every completed target command automatically. There is no public history-writing command; inspect recent history before a new batch to avoid repeats.
5. Re-read this skill or the active playbook after context compression, `/resume`, or any uncertainty about the workflow. **Also read back evidence and hypotheses:** `read_file path="$ENG_DIR/hypotheses.md"` and `search_files path="$ENG_DIR/evidence" pattern="<target>" target="files"` to restore investigation state.
@@ -170,6 +176,7 @@ The phase workflow is mandatory for the entire session, including long, compress
- VULN RESEARCH → `playbooks/vuln-research.md`
- EXPLOITATION → `playbooks/exploitation.md` plus relevant vuln-class playbook
- POST EXPLOITATION → `playbooks/post-exploitation.md`
- FLAGS (authorised training/lab objective capture) → `playbooks/post-exploitation.md` + `references/flags-mode.md` (`templates/flag-capture-register.md`)
- REPORTING → `playbooks/reporting.md`
- RETROSPECTIVE → `references/retrospective.md`
8. Before exploit validation, re-check the vuln playbook's `## Stop Conditions` and `## Blocked Actions`.
@@ -209,7 +216,7 @@ IF 5 consecutive attempts fail on the same target component:
- **When stuck, research online before re-running.** The cheapest unstuck move is almost always new information, not another scan. If a command stalls, errors, or yields nothing new: `web_search` the exact error string + tool name, pull the upstream docs / PoC / CVE advisory via `web_extract`, and only then change the variable (different host, param, wordlist, technique) or move to a new task. Treat the `web` capability as a primary recovery lever, not a last resort.
- **Five-attempt research trigger.** After five failed attempts against the same feature or exploit class, stop. Re-read captured source first; then research the exact parser, protocol, or primitive; then test one documented variant. If the source is unavailable or the next safe variant is unclear, ask the user for a hint rather than continuing a circular loop.
- **False-negative checkpoint.** Before declaring a feature unavailable, stubbed, or not implemented, match the exact test command and argument order against the captured source/parser. Record the command, decisive response, and `verification_status` in the hypothesis. `syntax_uncertain` is not a rejection: it requires a corrected re-test.
- **Context boundary.** Prefix controller output with `[VICTIM]`; prefix assessment-host commands and notes with `[ATTACKER]`. `/proc`, `/run`, UNIX sockets, and local service state belong to the machine on which the command runs. Host-local preparation (for example, starting an approved HTTP server or hashing a local artifact) may use the terminal directly; commands sent to a target still use `violin_exec` or one pre-approved `violin_exec_burst`.
- **Context boundary.** Prefix controller output with `[VICTIM]`; prefix assessment-host commands and notes with `[ATTACKER]`. `/proc`, `/run`, UNIX sockets, and local service state belong to the machine on which the command runs. Hashing and other non-networking preparation may use the terminal directly. For a long-lived local service, use tracked `violin_exec` with `background=true` and then `violin_exec_status`/`violin_exec_cancel`; no typed HTTP-server readiness tool ships in v3.0.0.
- **Mandatory research-loop (VULN RESEARCH / EXPLOITATION):** for each detected version/service, record the actual NVD/ExploitDB/GitHub research and update the hypothesis when its semantic state changes (Candidate/Likely/Validated/Rejected). Do not fabricate a hypothesis update merely because another payload ran.
- **Research-attempt gate:** before any EXPLOITATION, POST_EXPLOITATION, PRIVESC, or FLAGS target command, the matching hypothesis must contain non-empty `CVE Research` and `Exploit Research` fields. Each field records the online query, source, and outcome. A truthful `no results`, `not applicable`, or `source unavailable` outcome satisfies the attempt requirement; an omitted field blocks execution. Local SearchSploit alone does not satisfy the online attempt.
- **Split continuity contract.** Every approved target command is automatically mirrored to `state/history.md`; PTT progress is never automatic. When the bounded window ends and `violin_exec` returns `sync_required`, stop, review the batch evidence, call `violin_review_batch`, then continue.
@@ -337,6 +344,14 @@ separate from confirmed findings, in a hypothesis board.
The board has five sections: Active Theories, Observations, Investigation Chains, Research Log, Resolved Theories.
**Per-hypothesis discipline:** every active hypothesis carries a **confidence**,
**timebox**, **cheapest discriminating test**, and explicit **kill criteria**
(see `templates/hypothesis-board.md`). Run the cheapest discriminating test
first; kill the path when evidence contradicts it OR it stops producing new
information, and log it in the board's **Decoy Trail** so a disproven technique
is never re-entered. Verification and corroboration rules (independent re-check,
≥2 evidence angles, blocker taxonomy) live in `references/evidence-and-verification-discipline.md`.
### Evidence Read-Back
The drift guard (SOUL.md, .hermes.md, §2) requires re-reading evidence and
@@ -378,6 +393,6 @@ and CVSS scoring rules.
## 12. Operator Notes & Supported Workflow Patterns
* **Reverse-Shell Catch Listeners (`violin_listener`)**: Reverse-shell catch listeners bind host-local interfaces (e.g. `0.0.0.0`, `127.0.0.1`, `localhost`, or the attacker's assigned VPN IP e.g. `10.10.14.x`). `violin_listener` explicitly allows host-local and VPN IP bindings, while rejecting binding attempts targeting victim/target addresses.
* **Persistent Background Tunnels (`ssh -f -N`, background listeners)**: Long-lived commands or SSH tunnels marked with `background=true` remain running after dispatch. `violin_review_batch` detects active background processes in `$ENG_DIR/state/executions/*.json` and treats them as acknowledged completed actions rather than blocking batch review with "pending command not yet in exact history".
* **Persistent Background Tunnels (`ssh -f -N`, background listeners)**: Long-lived commands or SSH tunnels marked with `background=true` remain running after dispatch. `violin_review_batch` detects active background processes in `$ENG_DIR/evidence/executions/*.json` and treats them as acknowledged completed actions rather than blocking batch review with "pending command not yet in exact history".
* **Clearing Stuck Active PTT Tasks**: If a target environment or box is reset, starting a new task via `violin_record_ptt` will automatically supersede and close an old active `[~]` task (updating its status to `[x]` with a `[superseded-by:task-id]` note) as long as no pending unreviewed command batch exists.
* **Semantic Anti-Stuck Lock Auto-Release**: Recording a hypothesis (`violin_record_hypothesis`) or submitting a batch review containing completed execution evidence/technique pivot automatically clears progress locks, preventing false anti-stuck blocks during rapid CTF iteration.
+5 -1
View File
@@ -202,7 +202,11 @@ wafw00f https://<target> -o $ENG_DIR/evidence/recon/tech/waf-detect.txt
> Use the `browser` toolset (`browser_navigate`, `browser_click`, `browser_type`,
> `browser_vision`, `browser_snapshot`) to interact with the target website
> directly. This is essential for website enumeration and interaction.
> directly. This is essential for website enumeration and interaction. In
> v3.0.0, browser activity is gated on the active PTT/skill receipt but is not
> network-level scope enforcement: only navigate to an explicitly approved
> in-scope URL, inspect the visible URL after every action, and stop for review
> if a redirect or client-side navigation leaves scope.
```bash
mkdir -p "$ENG_DIR/evidence/recon/browser"
@@ -0,0 +1,95 @@
# Evidence & Verification Discipline
Cross-cutting rules for turning observations into **Validated** findings and for
knowing when to stop. Applies across every phase and playbook. Complements the
receipt schema in `templates/verification-receipt.yaml` and the hypothesis
board in `templates/hypothesis-board.md`.
## 1. Verify by independent re-check, never an in-session "success"
A tool returning success, a UI accepting your value, or a command exiting 0 is
NOT a receipt. Before declaring anything complete:
- **Re-run the decisive command** and confirm the end-state is stable and
reproducible, not just the first time.
- **Re-read the saved evidence file** and confirm it is non-empty and actually
parseable / decodable — an empty or corrupt artifact saves the mark but
proves nothing.
- **Confirm the downstream end-state** that proves acceptance (e.g. the control
that confirms the objective is now flagged complete, the input is locked, the
expected confirmation is present) by an independent re-look, not by
remembering the earlier click.
Treat a single success message as provisional until independently confirmed.
## 2. Corroborate ≥2 independent angles before Validated
Never promote a finding to **Validated** on one self-report, one mirror, one
third-party re-statement, or one artifact you did not verify yourself.
- Require **two independent evidence angles** (different tool + manual
confirmation, or a primary artifact trace + an independent re-derivation), OR
a direct trace to the artifact with a plausibility/format check.
- **Third-party re-statements are not ground truth.** If public sources
disagree with what you re-derived from the actual response/parser/config,
trust the primary evidence and re-derive — do not copy the external value.
- **A clean offline/in-lab decode is not proof the live target agrees.** Only
the target's own accepted state confirms an objective; an offline derivation
is a strong signal, not a receipt.
- Apply a **shape/format check** to any value before using it (mask length,
expected charset, concrete structure) — a value that does not fit the shape
is the wrong value, not a reason to force it.
## 3. "No result" is not absence
A single empty search, one 404, or one failed tool call is a *degraded probe*,
not proof of absence.
- **Parallel multi-source sweep** — run independent queries/engines/sources in
one batch before concluding a CVE, exploit, technique, or feature is
unavailable.
- **Tool failure ≠ absence.** A backend error, a missing library, or a
credit/quota error is a tooling problem — fix and re-run, then re-check.
Do not record "no result" from a call that errored.
- **Gated preview ≠ usable source.** A search hit that renders only an intro /
paywall / "not ready to disclose" placeholder contains no answers; do not
cite it as coverage.
## 4. Classify by inspection, not assumption
Do not assume an endpoint is vulnerable to class X because it "looks like" X or
a label says so. Inspect the **actual** request/response, the deployed parser,
or the fixture before pattern-matching. A wrong classification wastes batches
on the wrong bypass set.
## 5. Lateral read / deliberate-twist re-read
When a path stalls or feels contrived, re-read the actual artifact literally and
ask "what is the deliberate misdirection here?" Consider:
- **Decoys / mislabelled values** — a field labelled one way that is actually a
different primitive.
- **Runtime-assembled values** — the real object is derived, not stored.
- **Alternate encodings** — different charset, escaping, or block boundaries.
- **Rendered / raw-byte reads** — inspect rendered output, raw bytes, or
non-obvious answer shapes (e.g. the shape implied by a masked field).
- **Off-by-class** — a category-shift where the real answer is a different
object than the obvious one.
This is a re-read of the *target evidence*, not a search for external answers.
## 6. Blocker taxonomy — know the difference between "hard-walled" and "keep going"
| State | Meaning | Action |
|-------|---------|--------|
| **Stuck, keep grinding** | Evidence absent but a new angle exists | Run the next cheap test / new source; do not repeat the same command unchanged |
| **Hard-walled now** | Blocked by something outside your control (needs user input, external access, an action that must be performed inside the target's own environment, expired access, an approval) | **Report the exact blocker + the unlock and STOP this path.** Do not loop or improvise |
Looping on the same probe, or improvising past a blocker it is not authorized
to cross, is the failure mode. Surface real blockers precisely and move on.
## 7. Never fabricate
If no primary/accessible evidence supports a value, report it as **unsupported /
unconfirmed** rather than reconstructing it and presenting it as observed. A
finding's confidence is only as honest as the evidence trail behind it.
+41
View File
@@ -0,0 +1,41 @@
# Flag-Capture Mode
Optional FLAGS phase for **authorised training / lab / challenge-style**
engagements where the test objective is to retrieve one or more secret "flag"
objects (a value the target mints or stores as proof of a completed objective).
Applies alongside `POST_EXPLOITATION` verb like privilege escalation, but the
deliverable is a captured value plus a verification receipt.
Use `templates/flag-capture-register.md` to track every captured value.
## Core invariants
1. **Target-sourced only.** The value is retrieved from the target — its
filesystem, a response body, a config node, memory, or re-derived from
target evidence you can read. It is **never** sourced from a blog, writeup,
search engine, or third-party dump. Searching online for the value is out of
scope for this mode and forbidden.
2. **Derive from evidence, not memory of sources.** If public descriptions of a
lab disagree with what the actual target returns, trust the primary target
evidence and re-derive. A value that only appears in a third-party re-statement
is not a capture.
3. **Shape/mask check.** Compare every candidate to the expected shape (length,
charset, structure) before recording or using it.
4. **Independent re-check.** Verify capture by re-reading the target's accepted
end-state (value retained / control disabled / objective complete), not from
the first accept toast.
5. **Paced, one at a time.** Apply capture values sequentially with backoff;
never blast a batch, which anti-automation swallows.
6. **Static analysis only.** Never execute a recovered binary or payload to
mint a value — analyze with parsers, disassemblers, and decoders and derive
the value.
7. **Report in-target blockers honestly.** If a step that mints or verifies the
value must run inside the target's own environment and cannot be reached from
this host, record what you can from accessible evidence, mark the row
`unsupported`/`blocked`, and report the exact blocker. Never fabricate.
## When it adds no value
Flag capture is not a substitute for a normal finding: it is an objective
marker for authorized training/lab work. For real-world engagements, the FLAGS
phase is skipped and objectives are reported as findings with CVSS + evidence.
@@ -11,6 +11,8 @@ Use this reference during the mandatory final engagement phase. Real-world targe
5. **Client feedback loop** — if the client later reports a missed issue, patch the relevant playbook and add a detection gate to `playbooks/vuln-research.md`.
6. **Recent-CVE follow-up** — after delivery, check whether new CVEs affect the identified technology stack and record any follow-up recommendations.
7. **False-rejection review** — inspect every Rejected hypothesis. Confirm it records the exact test command and response, a source/parser-verified status, and a rejection reason. Reopen any `syntax_uncertain` or `not_tested` item; document where an incorrect rejection caused circular testing or delayed the valid path.
8. **Decoy-trail review** — confirm the hypothesis board's Decoy Trail records every killed approach with a WHY and signature. Any path killed simply because it "stopped producing new info" but still has an untried cheap test should be reopened as a new hypothesis, not silently dropped.
9. **Flag-capture review (FLAGS mode only)** — verify every captured value in `templates/flag-capture-register.md` was **target-sourced** (never pulled from an online blog/writeup/dump), passed a shape/mask check, and was independently re-checked against the objective's accepted end-state. Any online-sourced value invalidates the capture — flag it in the retrospective and remove it from the report.
## Coverage Matrix
+5
View File
@@ -260,6 +260,11 @@ finding:
## 4. Verification
Verification-discipline rules (independent re-check, corroborate ≥2 evidence
angles before Validated, "no result ≠ absence", blocker taxonomy) are in
`references/evidence-and-verification-discipline.md`. Flag-capture (FLAGS) mode
rules are in `references/flags-mode.md`.
When checking that this document is being followed, verify:
```
@@ -0,0 +1,36 @@
# Flag Capture Register — <target> <YYYY-MM-DD>
> Objective-objective register for flag-capture (FLAGS) mode. One row per
> captured secret/proof value. A captured value is sourced ONLY from the target
> (its filesystem, response, config, memory, or derived from target evidence) —
> NEVER from a blog, writeup, search engine, or third-party dump. Sourcing a
> value online is out of scope and forbidden in this mode.
> Mode notes: `skills/pentest/references/flags-mode.md`.
| ID | Objective | Captured value | Source (on/from target) | Shape/mask check | Corroboration | Verif. (independent re-check) | Status | Notes |
|----|-----------|----------------|--------------------------|------------------|---------------|-------------------------------|--------|-------|
| FLG-001 | `<human-readable objective>` | `<exact captured string>` | `<path/endpoint/config node it came from + how>` | `<expected shape + matches?>` | `<second angle or re-derivation, or 'primary trace only'>` | `<accepted-state confirmed by re-look?>` | `captured / verified / unsupported` | `<any step that must run inside the target env>` |
## Rules
1. **Target-sourced only.** Capture the value from the live target or derive it
from target evidence. Do not search online for it. A third-party source that
states the value is ignored — never entered into this register.
2. **Shape/mask check first.** Compare every candidate to the expected shape
(length, charset, structure) BEFORE recording or using it. A mismatch means
re-derive, never force.
3. **Independent re-check.** Do not record a value as *verified* from the
moment a submission/accept toast fired. Re-look and confirm the target's
accepted end-state (value retained / control disabled / objective complete)
by an independent re-read.
4. **Paced, sequential.** Submit/apply capture values one objective at a time;
a blast of many values in one turn gets swallowed by rate/anti-automation
and gives no useful signal. Back off between attempts.
5. **In-target steps.** If a step that mints/verifies the value must be
performed inside the target's own environment (not reachable from this
host), capture and record everything you can from the accessible evidence,
mark it `unsupported`/`blocked`, and report the exact blocker — do not
fabricate the value.
6. **Unsupported beats fabricated.** If the accessible evidence cannot
produce the value, record `unsupported` and move on. Never reconstruct a
value and present it as captured.
+24 -2
View File
@@ -17,6 +17,15 @@ Candidate ──► Likely ──► Validated
- **Validated** — proven with working PoC. Document and report.
- **Rejected** — tested, not vulnerable. Log the negative result.
### Per-hypothesis discipline (mandatory fields)
Every active hypothesis carries a **confidence**, a **timebox**, the **cheapest
discriminating test**, and explicit **kill criteria**. Run the cheapest test
that can actually distinguish the theory FIRST; kill the path the moment
evidence contradicts it OR it stops producing new information — record why in
the Decoy Trail so the same trap is never re-entered. A hypothesis that keeps
yielding nothing new is not "in progress", it is stuck.
---
## Active Theories
@@ -24,6 +33,9 @@ Candidate ──► Likely ──► Validated
<!-- Add hypotheses below this line using the format:
### H-001: <short title>
- **Status:** Candidate
- **Confidence:** 0.11.0 (guesstimate; escalate only with evidence)
- **Timebox:** <e.g. 4 tool batches or 30 min — then re-evaluate>
- **Cheapest test:** <single cheapest probe that discriminates this theory>
- **Phase:** VULN_RESEARCH
- **Target:** <endpoint/host/parameter>
- **Vuln class:** <SQLi | XSS | IDOR | SSRF | ...>
@@ -34,14 +46,13 @@ Candidate ──► Likely ──► Validated
- **Test Command:** <exact syntax tested, including argument order>
- **Test Response:** <exact decisive response/error>
- **Verification Status:** <syntax_confirmed | syntax_uncertain | not_implemented | not_tested>
- **Kill criteria:** <evidence that contradicts, OR no new info in N batches — then kill & log in Decoy Trail>
- **Rejection Reason:** <required only for Rejected; syntax-uncertain tests must stay active for corrected re-test>
- **Next step:** <what to do next to confirm or reject>
- **Linked findings:** <FIND-001 or none>
- **Updated:** <YYYY-MM-DD HH:MM>
-->
--->
## Observations (ungrouped)
- **OBS-001:** <what> — <where> — <date> — <significance>
@@ -55,6 +66,17 @@ Candidate ──► Likely ──► Validated
---
## Decoy Trail (killed approaches — do NOT re-enter)
> Every killed path is logged here with WHY so a disproven technique is never
> re-attempted from scratch. Before re-running any probe, check this trail and
> the command history for the same signature. "Worth another look" belongs in
> Active Theories with a new cheap test, not here.
- **KILL-001:** <approach/technique/endpoint> — killed because <evidence contradicted / no new info in N batches / wrong class> — signature <command-or-probe shape> — <YYYY-MM-DD>
---
## Research Log
- **RES-001:** Trigger: <discovery> → Searched: <source> for <query> → Result: <CVE-XXXX / no results / PoC at URL> → Action: <created H-XXX / dismissed / escalated>
@@ -327,3 +327,62 @@ def test_sync_windows_are_phase_aware() -> None:
assert state.sync_credit_limit("RECON") == 10
assert state.sync_credit_limit("EXPLOITATION") == 20
assert state.sync_credit_limit("PRIVESC") == 20
def test_update_hypothesis_supports_discipline_fields(tmp_path: Path) -> None:
hyp_file = tmp_path / "hypotheses.md"
hyp_file.write_text(
"# Hypothesis Board\n\n## Active Theories\n\n",
encoding="utf-8",
)
h = hypotheses.update_hypothesis(
hyp_file,
id="001",
title="SQLi in authentication form",
status="Candidate",
confidence="0.8",
timebox="4 tool batches",
cheapest_test="' OR 1=1 --",
kill_criteria="Response status 404 or no error output",
next_step="Run sqlmap probe",
linked_findings="FIND-001",
)
assert h.confidence == "0.8"
assert h.timebox == "4 tool batches"
assert h.cheapest_test == "' OR 1=1 --"
assert h.kill_criteria == "Response status 404 or no error output"
assert h.next_step == "Run sqlmap probe"
assert h.linked_findings == "FIND-001"
parsed = hypotheses.parse_hypotheses(hyp_file)
assert len(parsed) == 1
assert parsed[0].confidence == "0.8"
assert parsed[0].cheapest_test == "' OR 1=1 --"
text = hyp_file.read_text(encoding="utf-8")
assert "- **Confidence:** 0.8" in text
assert "- **Timebox:** 4 tool batches" in text
assert "- **Cheapest test:** ' OR 1=1 --" in text
assert "- **Kill criteria:** Response status 404 or no error output" in text
def test_update_hypothesis_preserves_board_sections(tmp_path: Path) -> None:
template_path = ROOT / "skills" / "pentest" / "templates" / "hypothesis-board.md"
hyp_file = tmp_path / "hypotheses.md"
hyp_file.write_text(template_path.read_text(encoding="utf-8"), encoding="utf-8")
h = hypotheses.update_hypothesis(
hyp_file,
id="001",
title="Command injection in search endpoint",
status="Candidate",
)
assert h.id == "001"
text = hyp_file.read_text(encoding="utf-8")
assert "## Active Theories" in text
assert "### H-001: Command injection in search endpoint" in text
assert "## Observations (ungrouped)" in text
assert "## Investigation Chains" in text
assert "## Decoy Trail (killed approaches — do NOT re-enter)" in text
assert "## Research Log" in text
assert "## Resolved Theories" in text