Vulnerability Chainer (/mantis-chain)
System Goal
Exploit Chain Architect. Analyzes isolated, individually-validated security
findings and historical knowledge base primitives to identify and construct
complex, multi-step exploit chains.
Command Definition
- Command:
/mantis-chain
- Description: Analyzes individual security findings to identify and
construct complex exploit chains.
- Arguments (all optional; supplied by the orchestrator per the harness
lifecycle contract — see
schema.json "Non-JSON Contracts"):
--snapshot_root / SNAPSHOT_ROOT — the pinned, immutable code snapshot
root for this pass. Consumed by Block A.
--snapshot_id / SNAPSHOT_ID — the snapshot identity string for this
pass. Consumed by Block A (sentinel) and Block B (match check).
--state_root — parent directory of workspace/. When absent, defaults to
the current directory (Block A step 1c/1d).
--target_root — an already-prepared tree that OVERRIDES the snapshot root
(rarely used by this stage; honored by Block A step 1a). When NONE of these
are passed, Block A falls back to active_snapshot in
workspace/.mantis_state.json; if that is absent too, the stage runs
MODE-OFF (snapshot_pinned = false) and behaves exactly as today (see
Backward-compat).
Input/Output Contract
- Reads:
workspace/findings/ (validated finding JSON files where status is
"VALID", and viability is "VIABLE", "CONDITIONAL_VIABLE", or
"SAMPLE_OR_TEST").
workspace/kb/entities/*.md and workspace/kb/vulnerabilities/*.md
(knowledge base primitives).
workspace/.mantis_state.json (to track current loop pass).
- Writes:
- Net-new exploit chain finding JSON files to
workspace/findings/<new_uuid>.json. Original findings are left unmodified.
- Preconditions:
- Validated or viable findings must exist in
workspace/findings/.
- Idempotency Guarantee:
- Before writing a new exploit chain finding, the skill must check existing
exploit chain findings by comparing the constituent finding sequence. Scan
BOTH the current
workspace/findings/ directory AND every archived pass
under workspace/archive/findings_pass_*/ AND legacy
workspace/archive/loop*_findings/ (all STATE-RELATIVE — never prefix
CODE_ROOT). A chain JSON is any finding whose constituent_findings array
is present and non-empty. If a chain with the EXACT same ordered constituent
sequence already exists in EITHER location, skip creating a duplicate.
- Signature-keyed idempotency (preferred) — with a code_paths tiebreak: If
ALL constituent findings have a
signature field, compare the ordered
constituent signature sequence (not UUIDs). Treat two chains as the SAME
(and skip creating the new one) ONLY IF their ordered signature sequences
are equal AND, for each corresponding constituent pair, at least one
code_paths entry (line-inclusive, i.e. compared WITH its trailing :line)
is identical. If the signature sequences match but any corresponding
constituent's code_paths differ, the chains are DISTINCT — create the new
chain. (Over-reporting is safe; a signature collision must never suppress
a genuinely new exploit chain, because signature strips line numbers and
can collide between distinct same-file bugs.)
- UUID fallback (today's behavior): If ANY constituent finding lacks a
signature field, fall back to comparing the ordered constituent UUID
sequence exactly as before. This preserves today's behavior for legacy /
un-upgraded findings.
- This archive scan is now fully effective with stable finding signatures: two
chains whose constituents share the same signatures are correctly
deduplicated across passes. When signatures are absent (legacy findings),
the scan remains HARMLESS — it cannot falsely suppress a legitimate new
chain (no silent dropped result) and simply prevents exact-UUID
re-duplication within a resumed / rerun pass. If the
workspace/archive/
directory does not exist, treat the archived set as empty and proceed.
Instructions
Locator Resolution (run first — all stages)
LOCATOR RESOLUTION (before reading ANY target code or artifact):
0. ROLE: If this skill NEVER reads target source (report, calibrate, reflect),
you are a FINDINGS-ONLY stage: skip steps 2-6; still read active_snapshot from
state for provenance/annotation; NEVER stop merely because a code root is unset.
1. Determine CODE_ROOT, in this priority order:
a. If --target_root is passed on THIS invocation, CODE_ROOT = --target_root.
It is AUTHORITATIVE and OVERRIDES SNAPSHOT_ROOT and the state fallback
(used when a caller hands you a prepared tree, e.g. a patched shadow).
b. Else if --snapshot_root (or SNAPSHOT_ROOT) is passed, use it.
c. Else read state_root/workspace/.mantis_state.json (state_root from
--state_root if passed, else ./workspace/... relative to the current dir)
-> active_snapshot.root / .snapshot_id / .snapshot_pinned.
d. Else (no arg AND no readable active_snapshot): CODE_ROOT = current directory,
treat snapshot_pinned = false (MODE-OFF). Do NOT stop.
2. SENTINEL CHECK (only if snapshot_pinned is true AND you did NOT take path 1a):
verify CODE_ROOT/.mantis_snapshot_id exists and equals SNAPSHOT_ID. If missing
or different -> STOP "snapshot sentinel mismatch". (A --target_root tree (1a) is
deliberately mutated and is sentinel-EXEMPT.)
3. PATH FIELDS:
- SNAPSHOT-RELATIVE (read under CODE_ROOT): code_paths entries; plan target_files
that are file paths. Strip ONLY a trailing ":<digits>". A code_paths entry
containing "://" is a URL/endpoint, NOT a file read. A code_paths entry that is
NOT of the form <existing-path>:<integer> is a non-source LOCATOR
(symbol/offset/endpoint): only check that the artifact/symbol exists; skip ALL
line-range and line-existence logic.
- STATE-RELATIVE (read/write under state_root/workspace, NEVER prefix CODE_ROOT):
kb_references, repro_file_path, reattack_file_path, helper scripts, report
files, and all state/findings JSON.
4. Never WRITE under CODE_ROOT when snapshot_pinned is true. Any command that
compiles, generates, or writes artifacts MUST run in a PRIVATE SHADOW copy
(mktemp -d from CODE_ROOT), never with cwd=CODE_ROOT. Read-only inspection may
cd into CODE_ROOT.
5. VCS-METADATA CARVE-OUT: history-log extraction and any VCS diff/blame command
run in the LIVE repository root (which still has .git/.hg/.repo), NOT CODE_ROOT
(the snapshot copy strips VCS metadata). Do NOT stop merely because CODE_ROOT
lacks .git/.hg/.repo.
6. Every shell command uses ABSOLUTE paths and sets its own working directory on
that call. Do NOT assume the working directory persists between calls.
[!NOTE] CURRENT-PASS CHECK (defensive; the binding guarantee is on the
harness per mantis-pipeline-adapter Scenario 2): if active_snapshot is
present AND active_snapshot.pass != state.pass_number, treat the snapshot as
STALE for this pass — STOP "stale active_snapshot: pass mismatch" or degrade
as HALT (snapshot_pinned effectively false: no authoritative verdicts, Block
B NOT_MATCHED, reproduce not_attempted). This catches a custom harness that
preserved active_snapshot across the Stage 15 pass increment without
re-pinning. The reference meta-agent re-pins every pass, so this check never
fires there. Block B itself cannot detect this (it is snapshot_id-only, not
pass-aware).
Stage-specific notes for Block A:
mantis-chain is NOT a findings-only stage (it is not
report/calibrate/reflect), so Block A runs steps 1–6 and resolves CODE_ROOT,
SNAPSHOT_ID, and snapshot_pinned from --snapshot_root / --snapshot_id
/ state active_snapshot.
- This stage WRITES only new finding JSON to
workspace/findings/
(STATE-RELATIVE, Block A step 3). It never writes under CODE_ROOT, so Block
A step 4 is satisfied trivially. If you optionally re-inspect a constituent's
code_paths to sanity-check a chain, read them SNAPSHOT-RELATIVE under
CODE_ROOT (Block A step 3) and never cd-and-write there.
- A chain finding's
code_paths are COPIED verbatim from its constituents (they
are already SNAPSHOT-RELATIVE). Do not re-prefix, re-resolve, or renumber
them.
Snapshot Match Check (used to select constituents)
SNAPSHOT MATCH CHECK for finding F (decides MATCHED vs NOT_MATCHED):
1. If snapshot_pinned is false -> NOT_MATCHED. Stop.
2. Read F.discovery_commit:
- missing OR empty OR the literal "MIXED" -> NOT_MATCHED.
- not exactly equal to SNAPSHOT_ID -> NOT_MATCHED.
- exactly equal to SNAPSHOT_ID -> MATCHED.
There is no other route to MATCHED; never fuzzy-compare. The global "default the
field and proceed" backward-compat rule does NOT apply to discovery_commit:
absent = NOT_MATCHED. (There is NO separate "dirty" gate: a dirty tree's
SNAPSHOT_ID already embeds the working-tree content hash, so within-pass findings
MATCH and cross-pass bare-commit findings do not.)
You will run Block B once per candidate constituent finding in Step 1 below,
using that finding's discovery_commit as F.discovery_commit.
Read the current batch of validated findings and explore whether multiple
seemingly low-severity or disparate vulnerabilities can be sequentially combined
to achieve a higher-impact compromise.
Execute the chaining stage as follows:
Load Primitives & Validated Findings:
- Read the JSON files in the
workspace/findings/ directory. Filter for
findings that have passed validation (e.g., status is "VALID" and
viability is "VIABLE", "CONDITIONAL_VIABLE", or "SAMPLE_OR_TEST").
- Snapshot eligibility gate (NEW — branches on
active_snapshot presence,
3-state model):
- MODE-OFF (no
active_snapshot in state — no --sync was requested,
today's default): SKIP this gate entirely: every finding that passed the
validation/viability filter above is an eligible constituent, exactly as
today. Build chains normally.
- HALT (
active_snapshot IS present but snapshot_pinned == false):
do NOT build any chains this pass. In HALT, findings' locators and
pre/post-conditions may be stale (the tree raced or could not be pinned),
so no authoritative chain verdicts may be produced. Log "no chain: HALT
mode (active_snapshot present, snapshot_pinned=false)" and stop the
chaining stage cleanly (produce no chain files). This mirrors the
minimum-eligibility guard below — a chain requires
proven-against-snapshot constituents, which HALT cannot provide.
- PINNED (
active_snapshot present AND snapshot_pinned == true): run
the Snapshot Match Check (Block B) on EACH such finding, using its
discovery_commit. Keep ONLY findings that return MATCHED as
eligible constituents. A finding that returns NOT_MATCHED — including
any finding whose discovery_commit is absent/empty, is the literal
"MIXED", or differs from the current SNAPSHOT_ID — was discovered
against a DIFFERENT code snapshot; its file:line locators and its
pre/post-conditions may no longer hold, so it MUST NOT be used as a chain
link this pass.
- Minimum-eligibility guard (NEW): If fewer than 2 eligible constituents
remain, do NOT construct any chain this pass — a chain requires at
least two links proven against the SAME snapshot. Log "no chain: fewer than
2 snapshot-matched constituents" and stop the chaining stage cleanly (this
is not an error; simply produce no chain files). This mirrors today's
behavior whenever there are fewer than two chainable findings.
- Read the Markdown Knowledge Base (
workspace/kb/entities/ and
workspace/kb/vulnerabilities/) to identify architectural primitives that
might not be bugs on their own, but could serve as stepping stones (e.g.,
"User controls file upload path", "Service runs as root").
Cross-Finding Analysis (The Chaining Matrix):
- Analyze the preconditions and postconditions of each validated finding.
- Ask: Can the output or side-effect of Finding A satisfy the strict
precondition required to trigger Finding B?
- Example Chains to look for:
- Path Traversal + Loose Permissions = RCE: A low-severity path
traversal (Finding A) allows writing to
/tmp, but a separate
misconfiguration (Finding B) allows a cron job to execute scripts in
/tmp.
- XSS + CSRF = Account Takeover: A stored XSS (Finding A) can be used
to harvest an anti-CSRF token to execute a state-changing action (Finding
B).
- Info Leak (Memory Revelation) + Buffer Overflow = ASLR Bypass: An
info leak (Finding A) reveals base pointers, satisfying the precondition
to exploit a stack buffer overflow (Finding B).
Construct "Super Findings":
- If a viable exploit chain is discovered, do NOT modify or delete the
original isolated findings. They still need to be patched individually.
- Idempotency check (run BEFORE minting a UUID): Apply the Idempotency
Guarantee above — scan current
workspace/findings/ AND
workspace/archive/findings_pass_*/ AND legacy
workspace/archive/loop*_findings/ for a chain whose constituent sequence
equals this chain's ordered constituent sequence. If ALL constituents have
signature, compare ordered signature sequences AND require the
per-constituent line-inclusive code_paths tiebreak described in the
Idempotency Guarantee (a signature-sequence match ALONE is NOT enough to
skip); if ANY constituent lacks signature, fall back to ordered
constituent UUID sequences (today's behavior). Only if a match satisfies
the tiebreak, SKIP this chain (do not create a file) and move on to the
next candidate.
- Instead, generate a net-new UUID and create a new finding JSON file in
workspace/findings/<new_uuid>.json.
- Constituent Findings: You must record the array of constituent finding
UUIDs in the structured
"constituent_findings" property (e.g.,
["UUID_A", "UUID_B"]). This clearly documents the links of the exploit
chain.
- Determine Discovery Snapshot (
discovery_commit): Set the chain
finding's discovery_commit from its ELIGIBLE constituents (the ones you
kept in Step 1):
- MODE-OFF short-circuit (3-state rule): if
active_snapshot is ABSENT
in state (MODE-OFF — no --sync was requested), OMIT discovery_commit
on the chain finding entirely. In MODE-OFF, researcher OMITS
discovery_commit on every constituent (researcher:317,340-341: "OMIT
this key entirely if active_snapshot is absent or snapshot_pinned is
false"), so every constituent has a missing/empty discovery_commit. The
SAME/MIXED logic below would thus write the literal "MIXED" on every
chain — a snapshot-era artifact that did not exist in Phase 1 and
violates MODE-OFF's "byte-for-byte today's behavior" guarantee
(schema.json:4). The chain's own schema comment (chain:307) says
"Absent on legacy/unpinned runs," confirming the intended MODE-OFF
behavior is omission, not "MIXED". (Functional impact is low —
schema.json:232 treats both "absent" and "MIXED" as NOT_MATCHED — but
mode purity matters.) Do NOT write "MIXED" or any value in MODE-OFF.
- If ALL eligible constituents have the SAME non-empty
discovery_commit
value, set the chain's discovery_commit to that exact string.
- If the constituents'
discovery_commit values DIFFER from one another,
OR any eligible constituent has a missing/empty discovery_commit (only
reachable in PINNED/HALT mode — in MODE-OFF, the short-circuit above
already OMITted the field), set the chain's discovery_commit to the
literal string "MIXED". Do NOT invent, hash, or fuzzy- normalize this
value — copy an exact string or write the literal "MIXED". Rationale:
in pinned mode every eligible constituent is MATCHED, so they all share
the current SNAPSHOT_ID, and the chain inherits it (a later Block B on
the chain then MATCHES this pass). The "MIXED" sentinel makes Block B
return NOT_MATCHED for the chain (Block B step 2 treats the literal
"MIXED" as NOT_MATCHED) — the safe branch for a chain whose links
cannot be proven against a single snapshot.
- Compute
signature (deterministic content-identity hash for the
chain):
- Collect the
signature field from every ELIGIBLE constituent finding
(the same set used for discovery_commit in Step 1).
- If EVERY eligible constituent has a non-empty
signature: sort the
constituent signatures lexicographically (ascending), then compute
signature = first 16 hex characters of
sha256("chain|" + "|".join(sorted_constituent_signatures)). The
"chain|" prefix prevents any collision with a non-chain finding's
signature (which is hashed from title+cwe+target, a different input
domain). Sorting makes the chain signature invariant under constituent
re-ordering.
- If ANY eligible constituent lacks a
signature field (legacy /
un-upgraded finding): set the chain's signature to the EMPTY STRING and
OMIT it from the JSON (downstream consumers then fall back to UUID-based
fold/dedupe per schema.json:256). This is the safe branch — it can
never cause a wrong fold, only an over-report.
- Compute the signature ONCE at chain creation and NEVER recompute, edit,
or invent it (same rule as
discovery_commit and as researcher:241-242
for regular findings).
- Compute
lineage_id (cross-pass chain lineage):
- Scan
workspace/archive/findings_pass_*/ AND legacy
workspace/archive/loop*_findings/ for any archived chain finding JSON
(any finding with a non-empty constituent_findings array) whose
signature field EXACTLY equals this chain's computed signature.
- If a match is found:
lineage_id = the archived ancestor's lineage_id
(inherit the lineage chain so report can fold across passes). If MULTIPLE
archived chains share the same signature, inherit from the MOST RECENT
(highest pass number) ancestor. All ancestors with the same chain
signature SHOULD share the same lineage_id; if they don't, inherit
from the most recent one and log a warning.
- If no match (including when this chain's
signature is the EMPTY
STRING): lineage_id = a fresh UUIDv4.
- These are STATE-RELATIVE paths (Block A step 3) — read under
--state_root/workspace/archive/, NEVER under CODE_ROOT. (Same scan
targets and same algorithm shape as researcher:246-272 for regular
findings — chains simply skip the basename-normalized rename fallback,
because a chain's identity is the set of its constituents, not a single
file path.)
- Determine Entry Point Privileges: The
privileges_required field for
the chain must represent the privilege level required to initiate the
first step of the chain (the entry point). For example, if the chain
starts with an unauthenticated exploit (NONE) that leads to admin access,
which is then used to trigger RCE, the chain's privileges_required must
be set to NONE.
- Determine Attacker Position: The
attacker_position field for the
chain must inherit the attacker position from the entry point / first
constituent finding of the chain.
- Determine User Interaction Requirement: The
user_interaction field
for the chain must be set to REQUIRED if the entry point or any step in
the chain requires user interaction. It should only be set to NONE if the
entire chain is zero-click.
- Determine Status: Set
status to "VALID".
- Determine Production Viability: Inherit from constituent findings. If
any constituent is
"SAMPLE_OR_TEST", set to "SAMPLE_OR_TEST". Else if
any constituent is "CONDITIONAL_VIABLE", set to "CONDITIONAL_VIABLE".
Otherwise, set to "VIABLE".
- Determine Reproduction Status: Inherit from constituent findings:
- If any constituent has a
repro_status of "failed_to_reproduce" or
"not_attempted", set to "not_attempted".
- Otherwise, if all constituents have a
repro_status of "reproduced" or
"statically_confirmed", set to "statically_confirmed".
- An exploit chain must never inherit
"reproduced" (as reproduction
of constituents does not prove the end-to-end chain works).
Chain Findings Schema Format (Per File)
{
"id": "A unique identifier generated for this chain finding. Must match filename.",
"title": "Exploit Chain: [Impact] via [Finding A] and [Finding B]",
"description": "Step-by-step documentation of the exploit chain. Start with Step 1 (Triggering Finding A) and explain how its outcome feeds into Step N (Triggering Finding Z).",
"impact": "The combined, escalated impact of the chain (e.g., Remote Code Execution, Full Database Exfiltration). This should be higher than the individual findings.",
"severity": "CRITICAL / HIGH",
"privileges_required": "NONE / LOW / HIGH",
"user_interaction": "NONE / REQUIRED",
"code_paths": [
"relative/file/path_A.c:line_number",
"relative/file/path_B.c:line_number"
],
"attacker_position": "EXTERNAL / LOCAL / etc. (inherited from entry point)",
"mitigation": "Recommended strategy to break the chain. Usually involves fixing at least one, if not all, of the underlying links.",
"status": "VALID",
"production_viability": "VIABLE / SAMPLE_OR_TEST / CONDITIONAL_VIABLE",
"repro_status": "statically_confirmed / not_attempted",
"constituent_findings": ["UUID_A", "UUID_B"],
"signature": "First 16 hex chars of sha256(\"chain|\" + \"|\".join(sorted(constituent_signatures))) if EVERY constituent has a non-empty `signature`; else the EMPTY STRING (absent-equivalent -> downstream falls back to UUID-only behavior). Computed ONCE at chain creation and NEVER recomputed (same rule as discovery_commit). See the signature/lineage computation steps above.",
"lineage_id": "Inherited from the most-recent archived chain finding whose `signature` equals this chain's `signature` (scan workspace/archive/findings_pass_*/ AND workspace/archive/loop*_findings/); else a fresh UUIDv4. See the signature/lineage computation steps above.",
"discovery_commit": "The SNAPSHOT_ID shared by all constituents, or the literal \"MIXED\" if they differ / are missing (only computed in PINNED/HALT mode). OMITTED (absent) on legacy/unpinned runs (MODE-OFF) — see the MODE-OFF short-circuit in the discovery_commit computation step above.",
"history": [
{
"stage": "chainer",
"action": "created",
"details": "Constructed by chaining findings [UUID_A] and [UUID_B].",
"pass_number": <current_pass_number>,
"timestamp": "<current_iso8601_timestamp>"
}
]
}
Chain Deduplication Tagging:
- To ensure
/mantis-dedupe treats these chains differently than raw
findings, ensure the word "Chain" is prominently featured in the "title"
and "history" fields as shown in the schema.
Ensure any newly constructed chain files are written to the
workspace/findings/ directory. When complete, notify the user.
1---2name: mantis-chain3description: Analyzes individual security findings to identify and construct complex exploit chains. Use after validation stages to see if multiple low-severity bugs can be combined into a higher impact vulnerability. Don't use for initial codebase auditing or writing patch code.4---5
6# Vulnerability Chainer (/mantis-chain)
7
8## System Goal
9
10Exploit Chain Architect. Analyzes isolated, individually-validated security
11findings and historical knowledge base primitives to identify and construct
12complex, multi-step exploit chains.
13
14## Command Definition
15
16- **Command:** `/mantis-chain`
17- **Description:** Analyzes individual security findings to identify and
18 construct complex exploit chains.
19- **Arguments (all optional; supplied by the orchestrator per the harness
20 lifecycle contract — see `schema.json` "Non-JSON Contracts"):**
21 - `--snapshot_root` / `SNAPSHOT_ROOT` — the pinned, immutable code snapshot
22 root for this pass. Consumed by Block A.
23 - `--snapshot_id` / `SNAPSHOT_ID` — the snapshot identity string for this
24 pass. Consumed by Block A (sentinel) and Block B (match check).
25 - `--state_root` — parent directory of `workspace/`. When absent, defaults to
26 the current directory (Block A step 1c/1d).
27 - `--target_root` — an already-prepared tree that OVERRIDES the snapshot root
28 (rarely used by this stage; honored by Block A step 1a). When NONE of these
29 are passed, Block A falls back to `active_snapshot` in
30 `workspace/.mantis_state.json`; if that is absent too, the stage runs
31 MODE-OFF (`snapshot_pinned = false`) and behaves exactly as today (see
32 Backward-compat).
33
34## Input/Output Contract
35
36- **Reads**:
37 - `workspace/findings/` (validated finding JSON files where status is
38 `"VALID"`, and viability is `"VIABLE"`, `"CONDITIONAL_VIABLE"`, or
39 `"SAMPLE_OR_TEST"`).
40 - `workspace/kb/entities/*.md` and `workspace/kb/vulnerabilities/*.md`
41 (knowledge base primitives).
42 - `workspace/.mantis_state.json` (to track current loop pass).
43- **Writes**:
44 - Net-new exploit chain finding JSON files to
45 `workspace/findings/<new_uuid>.json`. Original findings are left unmodified.
46- **Preconditions**:
47 - Validated or viable findings must exist in `workspace/findings/`.
48- **Idempotency Guarantee**:
49 - Before writing a new exploit chain finding, the skill must check existing
50 exploit chain findings by comparing the constituent finding sequence. Scan
51 BOTH the current `workspace/findings/` directory AND every archived pass
52 under `workspace/archive/findings_pass_*/` AND legacy
53 `workspace/archive/loop*_findings/` (all STATE-RELATIVE — never prefix
54 `CODE_ROOT`). A chain JSON is any finding whose `constituent_findings` array
55 is present and non-empty. If a chain with the EXACT same ordered constituent
56 sequence already exists in EITHER location, skip creating a duplicate.
57 - **Signature-keyed idempotency (preferred) — with a code_paths tiebreak:** If
58 ALL constituent findings have a `signature` field, compare the ordered
59 constituent `signature` sequence (not UUIDs). Treat two chains as the SAME
60 (and skip creating the new one) ONLY IF their ordered `signature` sequences
61 are equal AND, for each corresponding constituent pair, at least one
62 `code_paths` entry (line-inclusive, i.e. compared WITH its trailing `:line`)
63 is identical. If the signature sequences match but any corresponding
64 constituent's `code_paths` differ, the chains are DISTINCT — create the new
65 chain. (Over-reporting is safe; a `signature` collision must never suppress
66 a genuinely new exploit chain, because `signature` strips line numbers and
67 can collide between distinct same-file bugs.)
68 - **UUID fallback (today's behavior):** If ANY constituent finding lacks a
69 `signature` field, fall back to comparing the ordered constituent UUID
70 sequence exactly as before. This preserves today's behavior for legacy /
71 un-upgraded findings.
72 - This archive scan is now fully effective with stable finding signatures: two
73 chains whose constituents share the same signatures are correctly
74 deduplicated across passes. When signatures are absent (legacy findings),
75 the scan remains HARMLESS — it cannot falsely suppress a legitimate new
76 chain (no silent dropped result) and simply prevents exact-UUID
77 re-duplication within a resumed / rerun pass. If the `workspace/archive/`
78 directory does not exist, treat the archived set as empty and proceed.
79
80## Instructions
81
82### Locator Resolution (run first — all stages)
83
84```
85LOCATOR RESOLUTION (before reading ANY target code or artifact):
860. ROLE: If this skill NEVER reads target source (report, calibrate, reflect),
87 you are a FINDINGS-ONLY stage: skip steps 2-6; still read active_snapshot from
88 state for provenance/annotation; NEVER stop merely because a code root is unset.
891. Determine CODE_ROOT, in this priority order:
90 a. If --target_root is passed on THIS invocation, CODE_ROOT = --target_root.
91 It is AUTHORITATIVE and OVERRIDES SNAPSHOT_ROOT and the state fallback
92 (used when a caller hands you a prepared tree, e.g. a patched shadow).
93 b. Else if --snapshot_root (or SNAPSHOT_ROOT) is passed, use it.
94 c. Else read state_root/workspace/.mantis_state.json (state_root from
95 --state_root if passed, else ./workspace/... relative to the current dir)
96 -> active_snapshot.root / .snapshot_id / .snapshot_pinned.
97 d. Else (no arg AND no readable active_snapshot): CODE_ROOT = current directory,
98 treat snapshot_pinned = false (MODE-OFF). Do NOT stop.
992. SENTINEL CHECK (only if snapshot_pinned is true AND you did NOT take path 1a):
100 verify CODE_ROOT/.mantis_snapshot_id exists and equals SNAPSHOT_ID. If missing
101 or different -> STOP "snapshot sentinel mismatch". (A --target_root tree (1a) is
102 deliberately mutated and is sentinel-EXEMPT.)
1033. PATH FIELDS:
104 - SNAPSHOT-RELATIVE (read under CODE_ROOT): code_paths entries; plan target_files
105 that are file paths. Strip ONLY a trailing ":<digits>". A code_paths entry
106 containing "://" is a URL/endpoint, NOT a file read. A code_paths entry that is
107 NOT of the form <existing-path>:<integer> is a non-source LOCATOR
108 (symbol/offset/endpoint): only check that the artifact/symbol exists; skip ALL
109 line-range and line-existence logic.
110 - STATE-RELATIVE (read/write under state_root/workspace, NEVER prefix CODE_ROOT):
111 kb_references, repro_file_path, reattack_file_path, helper scripts, report
112 files, and all state/findings JSON.
1134. Never WRITE under CODE_ROOT when snapshot_pinned is true. Any command that
114 compiles, generates, or writes artifacts MUST run in a PRIVATE SHADOW copy
115 (mktemp -d from CODE_ROOT), never with cwd=CODE_ROOT. Read-only inspection may
116 cd into CODE_ROOT.
1175. VCS-METADATA CARVE-OUT: history-log extraction and any VCS diff/blame command
118 run in the LIVE repository root (which still has .git/.hg/.repo), NOT CODE_ROOT
119 (the snapshot copy strips VCS metadata). Do NOT stop merely because CODE_ROOT
120 lacks .git/.hg/.repo.
1216. Every shell command uses ABSOLUTE paths and sets its own working directory on
122 that call. Do NOT assume the working directory persists between calls.
123```
124
125> [!NOTE] **CURRENT-PASS CHECK (defensive; the binding guarantee is on the
126> harness per `mantis-pipeline-adapter` Scenario 2):** if `active_snapshot` is
127> present AND `active_snapshot.pass != state.pass_number`, treat the snapshot as
128> STALE for this pass — STOP "stale active_snapshot: pass mismatch" or degrade
129> as HALT (`snapshot_pinned` effectively false: no authoritative verdicts, Block
130> B NOT_MATCHED, reproduce `not_attempted`). This catches a custom harness that
131> preserved `active_snapshot` across the Stage 15 pass increment without
132> re-pinning. The reference meta-agent re-pins every pass, so this check never
133> fires there. Block B itself cannot detect this (it is `snapshot_id`-only, not
134> `pass`-aware).
135
136Stage-specific notes for Block A:
137
138- `mantis-chain` is NOT a findings-only stage (it is not
139 report/calibrate/reflect), so Block A runs steps 1–6 and resolves `CODE_ROOT`,
140 `SNAPSHOT_ID`, and `snapshot_pinned` from `--snapshot_root` / `--snapshot_id`
141 / state `active_snapshot`.
142- This stage WRITES only new finding JSON to `workspace/findings/`
143 (STATE-RELATIVE, Block A step 3). It never writes under `CODE_ROOT`, so Block
144 A step 4 is satisfied trivially. If you optionally re-inspect a constituent's
145 `code_paths` to sanity-check a chain, read them SNAPSHOT-RELATIVE under
146 `CODE_ROOT` (Block A step 3) and never cd-and-write there.
147- A chain finding's `code_paths` are COPIED verbatim from its constituents (they
148 are already SNAPSHOT-RELATIVE). Do not re-prefix, re-resolve, or renumber
149 them.
150
151### Snapshot Match Check (used to select constituents)
152
153```
154SNAPSHOT MATCH CHECK for finding F (decides MATCHED vs NOT_MATCHED):
1551. If snapshot_pinned is false -> NOT_MATCHED. Stop.
1562. Read F.discovery_commit:
157 - missing OR empty OR the literal "MIXED" -> NOT_MATCHED.
158 - not exactly equal to SNAPSHOT_ID -> NOT_MATCHED.
159 - exactly equal to SNAPSHOT_ID -> MATCHED.
160There is no other route to MATCHED; never fuzzy-compare. The global "default the
161field and proceed" backward-compat rule does NOT apply to discovery_commit:
162absent = NOT_MATCHED. (There is NO separate "dirty" gate: a dirty tree's
163SNAPSHOT_ID already embeds the working-tree content hash, so within-pass findings
164MATCH and cross-pass bare-commit findings do not.)
165```
166
167You will run Block B once per candidate constituent finding in Step 1 below,
168using that finding's `discovery_commit` as `F.discovery_commit`.
169
170Read the current batch of validated findings and explore whether multiple
171seemingly low-severity or disparate vulnerabilities can be sequentially combined
172to achieve a higher-impact compromise.
173
174Execute the chaining stage as follows:
175
1761. **Load Primitives & Validated Findings:**
177
178 - Read the JSON files in the `workspace/findings/` directory. Filter for
179 findings that have passed validation (e.g., status is `"VALID"` and
180 viability is `"VIABLE"`, `"CONDITIONAL_VIABLE"`, or `"SAMPLE_OR_TEST"`).
181 - **Snapshot eligibility gate (NEW — branches on `active_snapshot` presence,
182 3-state model):**
183 - **MODE-OFF** (no `active_snapshot` in state — no `--sync` was requested,
184 today's default): SKIP this gate entirely: every finding that passed the
185 validation/viability filter above is an eligible constituent, exactly as
186 today. Build chains normally.
187 - **HALT** (`active_snapshot` IS present but `snapshot_pinned == false`):
188 do NOT build any chains this pass. In HALT, findings' locators and
189 pre/post-conditions may be stale (the tree raced or could not be pinned),
190 so no authoritative chain verdicts may be produced. Log "no chain: HALT
191 mode (active_snapshot present, snapshot_pinned=false)" and stop the
192 chaining stage cleanly (produce no chain files). This mirrors the
193 minimum-eligibility guard below — a chain requires
194 proven-against-snapshot constituents, which HALT cannot provide.
195 - **PINNED** (`active_snapshot` present AND `snapshot_pinned == true`): run
196 the **Snapshot Match Check (Block B)** on EACH such finding, using its
197 `discovery_commit`. Keep ONLY findings that return **MATCHED** as
198 eligible constituents. A finding that returns **NOT_MATCHED** — including
199 any finding whose `discovery_commit` is absent/empty, is the literal
200 `"MIXED"`, or differs from the current `SNAPSHOT_ID` — was discovered
201 against a DIFFERENT code snapshot; its `file:line` locators and its
202 pre/post-conditions may no longer hold, so it MUST NOT be used as a chain
203 link this pass.
204 - **Minimum-eligibility guard (NEW):** If fewer than 2 eligible constituents
205 remain, do **NOT** construct any chain this pass — a chain requires at
206 least two links proven against the SAME snapshot. Log "no chain: fewer than
207 2 snapshot-matched constituents" and stop the chaining stage cleanly (this
208 is not an error; simply produce no chain files). This mirrors today's
209 behavior whenever there are fewer than two chainable findings.
210 - Read the Markdown Knowledge Base (`workspace/kb/entities/` and
211 `workspace/kb/vulnerabilities/`) to identify architectural primitives that
212 might not be bugs on their own, but could serve as stepping stones (e.g.,
213 "User controls file upload path", "Service runs as root").
214
2152. **Cross-Finding Analysis (The Chaining Matrix):**
216
217 - Analyze the preconditions and postconditions of each validated finding.
218 - Ask: *Can the output or side-effect of Finding A satisfy the strict
219 precondition required to trigger Finding B?*
220 - Example Chains to look for:
221 - **Path Traversal + Loose Permissions = RCE:** A low-severity path
222 traversal (Finding A) allows writing to `/tmp`, but a separate
223 misconfiguration (Finding B) allows a cron job to execute scripts in
224 `/tmp`.
225 - **XSS + CSRF = Account Takeover:** A stored XSS (Finding A) can be used
226 to harvest an anti-CSRF token to execute a state-changing action (Finding
227 B).
228 - **Info Leak (Memory Revelation) + Buffer Overflow = ASLR Bypass:** An
229 info leak (Finding A) reveals base pointers, satisfying the precondition
230 to exploit a stack buffer overflow (Finding B).
231
2323. **Construct "Super Findings":**
233
234 - If a viable exploit chain is discovered, do **NOT** modify or delete the
235 original isolated findings. They still need to be patched individually.
236 - **Idempotency check (run BEFORE minting a UUID):** Apply the Idempotency
237 Guarantee above — scan current `workspace/findings/` AND
238 `workspace/archive/findings_pass_*/` AND legacy
239 `workspace/archive/loop*_findings/` for a chain whose constituent sequence
240 equals this chain's ordered constituent sequence. If ALL constituents have
241 `signature`, compare ordered `signature` sequences AND require the
242 per-constituent line-inclusive `code_paths` tiebreak described in the
243 Idempotency Guarantee (a signature-sequence match ALONE is NOT enough to
244 skip); if ANY constituent lacks `signature`, fall back to ordered
245 constituent UUID sequences (today's behavior). Only if a match satisfies
246 the tiebreak, SKIP this chain (do not create a file) and move on to the
247 next candidate.
248 - Instead, generate a **net-new UUID** and create a new finding JSON file in
249 `workspace/findings/<new_uuid>.json`.
250 - **Constituent Findings**: You must record the array of constituent finding
251 UUIDs in the structured `"constituent_findings"` property (e.g.,
252 `["UUID_A", "UUID_B"]`). This clearly documents the links of the exploit
253 chain.
254 - **Determine Discovery Snapshot (`discovery_commit`)**: Set the chain
255 finding's `discovery_commit` from its ELIGIBLE constituents (the ones you
256 kept in Step 1):
257 - **MODE-OFF short-circuit (3-state rule):** if `active_snapshot` is ABSENT
258 in state (MODE-OFF — no `--sync` was requested), OMIT `discovery_commit`
259 on the chain finding entirely. In MODE-OFF, researcher OMITS
260 `discovery_commit` on every constituent (`researcher:317,340-341`: "OMIT
261 this key entirely if active_snapshot is absent or snapshot_pinned is
262 false"), so every constituent has a missing/empty `discovery_commit`. The
263 SAME/MIXED logic below would thus write the literal `"MIXED"` on every
264 chain — a snapshot-era artifact that did not exist in Phase 1 and
265 violates MODE-OFF's "byte-for-byte today's behavior" guarantee
266 (`schema.json:4`). The chain's own schema comment (`chain:307`) says
267 "Absent on legacy/unpinned runs," confirming the intended MODE-OFF
268 behavior is omission, not `"MIXED"`. (Functional impact is low —
269 `schema.json:232` treats both "absent" and `"MIXED"` as NOT_MATCHED — but
270 mode purity matters.) Do NOT write `"MIXED"` or any value in MODE-OFF.
271 - If ALL eligible constituents have the SAME non-empty `discovery_commit`
272 value, set the chain's `discovery_commit` to that exact string.
273 - If the constituents' `discovery_commit` values DIFFER from one another,
274 OR any eligible constituent has a missing/empty `discovery_commit` (only
275 reachable in PINNED/HALT mode — in MODE-OFF, the short-circuit above
276 already OMITted the field), set the chain's `discovery_commit` to the
277 literal string `"MIXED"`. Do NOT invent, hash, or fuzzy- normalize this
278 value — copy an exact string or write the literal `"MIXED"`. Rationale:
279 in pinned mode every eligible constituent is MATCHED, so they all share
280 the current `SNAPSHOT_ID`, and the chain inherits it (a later Block B on
281 the chain then MATCHES this pass). The `"MIXED"` sentinel makes Block B
282 return NOT_MATCHED for the chain (Block B step 2 treats the literal
283 `"MIXED"` as NOT_MATCHED) — the safe branch for a chain whose links
284 cannot be proven against a single snapshot.
285 - **Compute `signature` (deterministic content-identity hash for the
286 chain):**
287 - Collect the `signature` field from every ELIGIBLE constituent finding
288 (the same set used for `discovery_commit` in Step 1).
289 - If EVERY eligible constituent has a non-empty `signature`: sort the
290 constituent signatures lexicographically (ascending), then compute
291 `signature` = first 16 hex characters of
292 `sha256("chain|" + "|".join(sorted_constituent_signatures))`. The
293 `"chain|"` prefix prevents any collision with a non-chain finding's
294 `signature` (which is hashed from title+cwe+target, a different input
295 domain). Sorting makes the chain signature invariant under constituent
296 re-ordering.
297 - If ANY eligible constituent lacks a `signature` field (legacy /
298 un-upgraded finding): set the chain's `signature` to the EMPTY STRING and
299 OMIT it from the JSON (downstream consumers then fall back to UUID-based
300 fold/dedupe per `schema.json:256`). This is the safe branch — it can
301 never cause a wrong fold, only an over-report.
302 - Compute the signature ONCE at chain creation and NEVER recompute, edit,
303 or invent it (same rule as `discovery_commit` and as `researcher:241-242`
304 for regular findings).
305 - **Compute `lineage_id` (cross-pass chain lineage):**
306 - Scan `workspace/archive/findings_pass_*/` AND legacy
307 `workspace/archive/loop*_findings/` for any archived chain finding JSON
308 (any finding with a non-empty `constituent_findings` array) whose
309 `signature` field EXACTLY equals this chain's computed `signature`.
310 - If a match is found: `lineage_id` = the archived ancestor's `lineage_id`
311 (inherit the lineage chain so report can fold across passes). If MULTIPLE
312 archived chains share the same `signature`, inherit from the MOST RECENT
313 (highest pass number) ancestor. All ancestors with the same chain
314 `signature` SHOULD share the same `lineage_id`; if they don't, inherit
315 from the most recent one and log a warning.
316 - If no match (including when this chain's `signature` is the EMPTY
317 STRING): `lineage_id` = a fresh UUIDv4.
318 - These are STATE-RELATIVE paths (Block A step 3) — read under
319 `--state_root/workspace/archive/`, NEVER under CODE_ROOT. (Same scan
320 targets and same algorithm shape as `researcher:246-272` for regular
321 findings — chains simply skip the basename-normalized rename fallback,
322 because a chain's identity is the set of its constituents, not a single
323 file path.)
324 - **Determine Entry Point Privileges**: The `privileges_required` field for
325 the chain must represent the privilege level required to initiate the
326 *first* step of the chain (the entry point). For example, if the chain
327 starts with an unauthenticated exploit (NONE) that leads to admin access,
328 which is then used to trigger RCE, the chain's `privileges_required` must
329 be set to `NONE`.
330 - **Determine Attacker Position**: The `attacker_position` field for the
331 chain must inherit the attacker position from the entry point / first
332 constituent finding of the chain.
333 - **Determine User Interaction Requirement**: The `user_interaction` field
334 for the chain must be set to `REQUIRED` if the entry point or any step in
335 the chain requires user interaction. It should only be set to `NONE` if the
336 entire chain is zero-click.
337 - **Determine Status**: Set `status` to `"VALID"`.
338 - **Determine Production Viability**: Inherit from constituent findings. If
339 any constituent is `"SAMPLE_OR_TEST"`, set to `"SAMPLE_OR_TEST"`. Else if
340 any constituent is `"CONDITIONAL_VIABLE"`, set to `"CONDITIONAL_VIABLE"`.
341 Otherwise, set to `"VIABLE"`.
342 - **Determine Reproduction Status**: Inherit from constituent findings:
343 - If any constituent has a `repro_status` of `"failed_to_reproduce"` or
344 `"not_attempted"`, set to `"not_attempted"`.
345 - Otherwise, if all constituents have a `repro_status` of `"reproduced"` or
346 `"statically_confirmed"`, set to `"statically_confirmed"`.
347 - An exploit chain must **never** inherit `"reproduced"` (as reproduction
348 of constituents does not prove the end-to-end chain works).
349
350 ### Chain Findings Schema Format (Per File)
351
352 ```json
353 {
354 "id": "A unique identifier generated for this chain finding. Must match filename.",
355 "title": "Exploit Chain: [Impact] via [Finding A] and [Finding B]",
356 "description": "Step-by-step documentation of the exploit chain. Start with Step 1 (Triggering Finding A) and explain how its outcome feeds into Step N (Triggering Finding Z).",
357 "impact": "The combined, escalated impact of the chain (e.g., Remote Code Execution, Full Database Exfiltration). This should be higher than the individual findings.",
358 "severity": "CRITICAL / HIGH",
359 "privileges_required": "NONE / LOW / HIGH",
360 "user_interaction": "NONE / REQUIRED",
361 "code_paths": [
362 "relative/file/path_A.c:line_number",
363 "relative/file/path_B.c:line_number"
364 ],
365 "attacker_position": "EXTERNAL / LOCAL / etc. (inherited from entry point)",
366 "mitigation": "Recommended strategy to break the chain. Usually involves fixing at least one, if not all, of the underlying links.",
367 "status": "VALID",
368 "production_viability": "VIABLE / SAMPLE_OR_TEST / CONDITIONAL_VIABLE",
369 "repro_status": "statically_confirmed / not_attempted",
370 "constituent_findings": ["UUID_A", "UUID_B"],
371 "signature": "First 16 hex chars of sha256(\"chain|\" + \"|\".join(sorted(constituent_signatures))) if EVERY constituent has a non-empty `signature`; else the EMPTY STRING (absent-equivalent -> downstream falls back to UUID-only behavior). Computed ONCE at chain creation and NEVER recomputed (same rule as discovery_commit). See the signature/lineage computation steps above.",
372 "lineage_id": "Inherited from the most-recent archived chain finding whose `signature` equals this chain's `signature` (scan workspace/archive/findings_pass_*/ AND workspace/archive/loop*_findings/); else a fresh UUIDv4. See the signature/lineage computation steps above.",
373 "discovery_commit": "The SNAPSHOT_ID shared by all constituents, or the literal \"MIXED\" if they differ / are missing (only computed in PINNED/HALT mode). OMITTED (absent) on legacy/unpinned runs (MODE-OFF) — see the MODE-OFF short-circuit in the discovery_commit computation step above.",
374 "history": [
375 {
376 "stage": "chainer",
377 "action": "created",
378 "details": "Constructed by chaining findings [UUID_A] and [UUID_B].",
379 "pass_number": <current_pass_number>,
380 "timestamp": "<current_iso8601_timestamp>"
381 }
382 ]
383 }
384 ```
385
3864. **Chain Deduplication Tagging:**
387
388 - To ensure `/mantis-dedupe` treats these chains differently than raw
389 findings, ensure the word "Chain" is prominently featured in the `"title"`
390 and `"history"` fields as shown in the schema.
391
392Ensure any newly constructed chain files are written to the
393`workspace/findings/` directory. When complete, notify the user.