Skip to content

perf + methodology: token/context efficiency, reasoning-quality fixes, exploitation methodology#4

Merged
s0ld13rr merged 17 commits into
mainfrom
perf/exploit-methodology
Jul 18, 2026
Merged

perf + methodology: token/context efficiency, reasoning-quality fixes, exploitation methodology#4
s0ld13rr merged 17 commits into
mainfrom
perf/exploit-methodology

Conversation

@s0ld13rr

Copy link
Copy Markdown
Owner

Summary

Three lines of work that lower per-turn cost, stop the agent from reasoning over
fabricated data, and make known-CVE exploitation more reliable — plus the
deterministic regression guards that gate them.

What's included

Token & context efficiency

  • Move static directives out of the volatile lane into the cached prefix (C-2)
  • Universal tool-output cap at the result boundary, not just bash (C-4)
  • Dedup the per-turn mutations rollup / split auto-critic boilerplate (C-1)
  • Prune tool output to a retrievable ref instead of erasing it (A-5)
  • Coalesce coordinator wake-ups on subagent completion (S-1)
  • Shrink + reframe the per-turn resolved-vectors block (NEW-1/2)

Reasoning quality

  • Stop fabricating all-pairs REACHABLE_FROM topology in nmap-parse (I-2)
  • Derive vuln confidence from evidence, not raw severity (I-3)
  • Read a typed result trailer from subagents instead of guessing from prose (I-1)
  • In-vector attempt-ledger so a re-spawned/compacted agent doesn't retry
    dead-end sub-approaches on the same vector

Exploitation methodology (prompts, fully generic)

Validation

A/B (our build vs the released one) on a web-exploitation CVE benchmark, matched
pairs. fix #1/#2 give a clear, mechanism-backed win on CVEs whose vector is
hidden in source (read-source / version-diff → solve), with no regression on
cases the model already one-shots. fix #3 is directionally right (improves
version-pinning reasoning) but not yet conclusively proven on its target class —
kept as low-risk, generic guidance.

Regression guards (deterministic, run per package):

  • C-4 cap: only large unhandled string outputs are capped, never double-capped,
    head preserved verbatim, full text kept retrievable by ref.
  • A-5 prune: never prunes the recent turns, protected tools, the recent
    protect-token budget, or across a summary/compacted boundary.
  • I-2: nmap-parse synthesizes no relationship edges.
  • Context-budget token check: 33× state → 2.1× context (caps hold), 200 resolved
    vectors (~800KB raw) → ~4.5KB rendered, a 52KB scan dump → ~16KB transcript
    (~69% off the hot path).

Not covered here: end-to-end multi-agent orchestration depth (S-1 / I-1 /
A-5 under real fan-out) — the risky code paths have unit-level guards, but
whether they affect strategist-level solve depth is a separate multi-host gate
still to run.

Notes

  • Prompt changes are generic — no CVE/target specifics.
  • Pure-helper extractions are behavior-preserving (no logic change).
  • typecheck 19/19; engagement + guard tests green.

s0ld13rr added 17 commits July 17, 2026 01:15
…grind

Extends the AR3 resolved-vectors ledger from vector-level to attempt-level.
A hard exploit with a KNOWN CVE (Java-deser gadget chains, payload/encoding
variants, shell-stabilization) burns many turns before the vector settles;
with no in-vector memory, a re-spawn or post-compaction turn re-explores blind.
Reconstructed from a PostExploitBench range-3 run: one known-CVE Dubbo deser
exploit consumed ~36 min / 25% of the engagement in a single silent grind with
zero state writes.

- schema.ts: VectorAttempt {technique, outcome, detail?, timestamp?}; attempt_log
  on ResolvedVector; VECTOR_ATTEMPT_LOG_MAX=20. toResolvedVectorsContext now
  surfaces FAILED sub-attempts only for in-progress vectors (attempted/blocked),
  one-liner for resolved/confirmed.
- store.ts: mergeAttemptLog dedups by technique, caps 20; wired into
  addResolvedVector merge and create paths.
- state-update.ts: record_vector accepts attempt:{technique,outcome,detail}.
- state-query.ts: renders attempt_log with success/partial/failed markers.
- prompts: IN-VECTOR MEMORY guidance in pentest.txt + exploiter, exploit-dev,
  post-exploit, infrastructure, webapp subagents (log each technique while
  grinding, keep status=attempted).
- test: attempt_log round-trip + in-progress-vs-resolved surfacing (7/7).

typecheck 19/19.
Co-scanning a set of hosts means the scanner reached each host, not that the
hosts can reach each other. The parser was creating N*(N-1)/2 REACHABLE_FROM
edges for every multi-host scan, which poisoned attack_path_suggest — its
Dijkstra/Yen search ran over invented reachability and recommended pivots that
never existed. Reachability edges are now created only from observed evidence
(cme_parse AUTHENTICATES_TO, explicit state_update add_relationship, bloodhound).

Part of context-wave-1 (I-2).
…e cached prefix

The engagement-context generator emitted large, rarely-changing instructions —
the entire ORCHESTRATOR_MODE text, the <orchestrator-dag> block, the REMINDER
line, and the mode/pause directives — inside `volatileSystem`, the trailing
per-turn system block. That block is cache-WRITTEN every turn, so this static
text was re-cached on every coordinator step, defeating the static/volatile
split it was supposed to benefit from.

The generator now returns `{ volatile, static }`: per-turn facts stay in the
volatile trailing block, while the directives (functions only of mode/pause/
agent/flags) ride the cached system prefix and are re-cached only when the user
actually changes mode/pause. No content is lost — it moves lanes.

Part of context-wave-1 (C-2).
AR2 cheap-model offload only covered `bash`, and only when a small model was
configured. Parser tools (nmap_parse, nuclei_parse, …), state_query, and
bash-without-a-small-model routed their output around both offload and
truncation, so a 10-50KB scan dump landed in the expensive transcript verbatim —
the single biggest avoidable input-token source in a real pentest.

Add an idempotent backstop at the tool-result boundary: any string output above
OUTPUT_HARD_CAP (16KB) that the tool did not already summarize/truncate/ref is
capped in the transcript, with the full text kept retrievable by ref. It skips
outputs already handled (summarized by AR2, or truncated/reffed by the tool),
so read/glob/grep/mcp and bash-offload paths are untouched. The cheap-model
digest still takes precedence for bash (cap sits above SUMMARIZE_THRESHOLD).
Parser data also persists to engagement state, so the transcript copy is
redundant and safe to cap.

Part of context-wave-1 (C-4).
…plate (C-1)

Targeted context dedup at the injection site (not the shared schema helpers, so
state_query output is unaffected):

- Mutations were printed twice per turn: the <diff> block lists the changes since
  last turn, and toOODAContext also rolled them up ("Recent changes: N mutations"
  + by-type). When the diff block is shown, OODA now receives [] so it skips the
  redundant rollup; coverage/gaps/alerts/sessions/segments/tasks/objectives (the
  unique OODA signal) are unchanged.
- The <auto-critic> block mixed a per-turn trigger (which findings need review)
  with a fixed "how to run a critic" instruction re-sent every turn. The static
  instruction moved to a cached <critic-protocol> in the system prefix; only the
  dynamic finding list stays in the volatile block.

Not the full ContextBudget assembler: on inspection the other flagged "overlaps"
(OODA coverage vs the 1-line summary, decision-history vs resolved-vectors vs
auto-critic) each carry distinct fields, so deleting either would lose signal.
Those are left intact; the assembler is deferred.

Part of context-wave-1 (C-1).
… (I-1)

Cross-agent memory was built by substring-scanning a subagent's free-form final
text: a line with "found" became a finding, "failed"/"could not" became a
failure. This silently misfiled real findings — e.g. "I could not crack it, but
/ftp/ exposes a KeePass DB" filed the KeePass discovery under failed_attempts,
so the next agent skipped a live lead.

Light typed-trailer approach: CONTEXT_PROTOCOL now asks every subagent to end
with a machine-read <agent-result> block (findings / dead_ends / next, one item
per '|'). buildContextSummary parses that block via parseResultTrailer and trusts
the subagent's own classification. When no trailer is present it falls back to the
legacy prose heuristic, so older/non-compliant output still works.

Tests: parseResultTrailer 6/6 (incl. the misfiled-finding regression). typecheck 19/19.

Part of context-wave-1 (I-1).
… (I-3)

nuclei_parse set both confidence (0.5/0.8) AND verification_status ("verified")
from a finding's SEVERITY. Severity is impact, not likelihood-of-being-real, and
nuclei is a scanner — a template match is unverified until a critic/manual check
confirms it. The fabricated "verified" also suppressed the auto-critic (which
gates on confidence + verification), so real findings skipped validation.

- New EngagementSchema.deriveConfidence(vuln): confidence from real signal —
  corroboration (independent tools, repeated observation) + exploitation state
  (exploited > confirmed > suspected); a verified/false_positive evidence item
  dominates. No severity input. Clamped to [0.1, 0.95].
- nuclei_parse now marks evidence verification_status="unverified" and sets
  confidence via deriveConfidence (a single unverified match → ~0.6, so the
  auto-critic correctly flags it for validation).
- cme/sqlmap/bloodhound confidence left as-is: those reflect a real success/
  confirmation signal (successful auth, confirmed injection, AD graph), not a
  severity proxy — deriveConfidence is available for future consistency.

Tests: deriveConfidence 6/6. typecheck 19/19. Confidence field kept, now measured.

Part of context-wave-2 (I-3).
…rasing (A-5)

Prune stamped time.compacted on old tool parts, which rendered them to the model
as "[Old tool result content cleared]" — the detail was gone. On a hard exploit
grind, compaction could drop the exact tool output the next step depended on.

Prune now keeps the full output retrievable: it reuses an existing ref (the
outputPath set by C-4 / truncate) or writes one via truncate.write, records it on
the part's metadata, then stamps compacted. The compacted placeholder in
message-v2 now points to that ref ("full output saved to <path>; read it if you
need the detail"). Cold detail is offloaded, not lost. Reuses the existing
truncate store (no new artifact-store component — A-2 dissolved into this).

The active vector's attempt-ledger is already safe (it lives in engagement state
and is re-injected each turn; prune only touches transcript tool parts).

typecheck 19/19. Behavior verified by reasoning; needs a live compaction run to
confirm end-to-end (session-layer, like AR2).

Part of context-wave-2 (A-5 + A-2).
…ion (S-1)

Each subagent completion re-drove the coordinator with a synthetic user turn
(inject) — and every coordinator turn replays the whole transcript. A wave of N
completions produced N coordinator turns, N transcript replays.

Completions now go through a short debounce (OPENCODE_ORCHESTRATOR_COALESCE_MS,
default 1500ms): the first arms a flush, any that land in the window join the
same batch and wake the coordinator ONCE with all results + a single
running/drained note computed at flush time. Race-safe via a Ref buffer +
scheduled flag cleared before draining (a mid-drain arrival re-arms; at worst one
harmless empty flush).

Crucially this does NOT slow the pipeline: dispatch is driven by `pump`, which
still runs on EVERY settle independent of the coordinator — only the coordinator's
wake frequency drops. Interrupt alerts keep their own immediate inject path.

Set the window very low to approximate per-completion behavior. Dispatch logic
(pump/selectWave) untouched: orchestrator tests 17/17, typecheck 19/19. Needs a
live run to confirm end-to-end (session-layer, like AR2/A-5).

Part of context-wave-2 (S-1).
<resolved-vectors> is injected every turn for both roles and was the single
heaviest volatile block on a real engagement (62 vectors → ~3.5k tok), driven by
verbose per-vector evidence strings and re-listing confirmed successes.

Keep the anti-re-test guarantee, bound the cost:
- Drop CONFIRMED vectors from the block — those are successes already recorded as
  vulnerabilities, not dead ends to avoid re-testing. They remain in
  state_query resolved_vectors and are counted in the "+N more" overflow.
- Clip evidence / revisit text to 90 chars (full text via state_query).
- Tighten the cap 40 -> 30, resolved/blocked first.

Measured on the real engagement (62 vectors: 29 resolved, 25 confirmed, 5
attempted, 3 blocked): block 13822 B -> 6098 B, ~1931 tok/turn saved (-56%), for
both roles every turn.

Tests: 8/8 (added confirmed-excluded, only-confirmed->undefined, evidence-clip).
typecheck 19/19.

Part of context-wave-2 (NEW-1, surfaced by the wave-1 measurement).
…formed re-testing (NEW-2)

The resolved-vectors ledger killed BLIND repetition (R6: open-redirect tested 7x)
but its "DO NOT re-test resolved" framing also risked suppressing LEGITIMATE
re-testing: a vector resolved while unauthenticated can become exploitable once
creds land — which directly conflicts with the host-exhaustion / credential-reuse
discipline (get access -> reconsider earlier dead ends). Over-eager `resolved`
marking also entrenched false negatives, re-injected every turn.

- Reframe the <resolved-vectors> header: don't repeat BLINDLY; a RESOLVED verdict
  holds only for the info known then; re-open with NEW leverage (fresh creds/
  access, new technique, changed target). BLOCKED/ATTEMPTED unchanged.
- Event-driven nudge: when credentials/access land this cycle (from the changelog
  diff) and RESOLVED vectors exist, inject a one-shot <revisit-hint> telling the
  agent to reconsider auth-gated dead ends. Reuses the existing changelog/diff
  machinery — no schema/store change, no cred-scoping ambiguity.
- Status discipline in docs (state-update.txt + pentest.txt): mark `resolved`
  ONLY when confident it's genuinely not exploitable; if you just failed, use
  `attempted`; if dead only "without creds", use `blocked` + revisit_when.

typecheck 19/19; resolved-vectors tests 8/8.

Part of context-wave-2 (NEW-2, from the "don't re-test hurts reasoning?" discussion).
# Conflicts:
#	packages/core/test/engagement-resolved-vectors.test.ts
#	packages/opencode/src/tool/state-update.txt
Both the released 0.2.2 and our build failed to weaponize a found web vuln by
brute-forcing request shapes instead of reading the route handler they had the
source for. Add a general, target-agnostic methodology rule to pentest/exploiter/
webapp: on a 4xx/5xx with source in hand, read the handler to derive the exact
contract (transport JSON vs form-encoded vs query; field names/nesting/types;
preconditions) before trying variants.

Deliberately GENERIC — no framework/plugin/CVE-specific detail — so it improves
general exploitation methodology without leaking any test CVE's answer (keeps
zero_day measurement clean).
When a versioned component has a public fixed release, diff patched vs target
version to locate the vuln from the fix, then focus exploitation on the changed
code. Target-agnostic (any versioned software) — no CVE/framework specifics, so
zero_day measurement stays clean. Depth-committing: also counters breadth-first
scatter. Complements read-the-handler (fix #1).
…fing source

A security fix in a newer release inverts the vulnerable code, so reading
'latest' source shows the opposite of what the target runs and leads to a
false 'not vulnerable' conclusion. Instruct agents to determine/pin the
target's exact version (fingerprint or fetch a plausibly-vulnerable older
release, never unpinned latest) before source-diffing, and to try
well-known/default/hardcoded secrets referenced in source. Generic, no
target or CVE specifics. Added to pentest/exploiter/webapp prompts.
…ixture

Replace the ad-hoc 172.50.2.20 test target with 192.0.2.20 (TEST-NET-1,
reserved for documentation) so no non-documentation address appears in the repo.
…en check

Extract behavior-identical pure helpers so the highest-risk information-loss
paths become deterministically testable on any platform (the multi-agent depth
gate needs Linux; these do not):

- C-4: shouldCapOutput / buildCappedOutput (small-model.ts), used by tools.ts.
  Tests lock: only large unhandled string outputs are capped, never double-cap
  an already-shaped result, head preserved verbatim, full text kept by ref.
- A-5: selectPrunableParts (compaction.ts), used by prune. Tests lock the
  depth-protection invariants: never prune the recent N turns, protected tools
  (skill), the recent protect-token budget, or across a summary/compacted boundary.
- I-2: source-invariant guard that nmap_parse synthesizes no relationship edges.
- Context-budget token check: measures the pure formatters — 33x state -> 2.1x
  context (caps hold), 200 resolved vectors (~800KB raw) -> ~4.5KB rendered,
  a 52KB scan dump -> ~16KB transcript (69% off the hot path, full text by ref).

53 tests green, typecheck 19/19. No behavior change — pure extraction only.
@s0ld13rr
s0ld13rr merged commit 5743c9e into main Jul 18, 2026
0 of 4 checks passed
@s0ld13rr
s0ld13rr deleted the perf/exploit-methodology branch July 18, 2026 05:15
KumaloWilson pushed a commit to KumaloWilson/pentestcode that referenced this pull request Jul 18, 2026
perf + methodology: token/context efficiency, reasoning-quality fixes, exploitation methodology
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant