Skip to content

cyber: draw the breach path in the dashboard's Evolution view#356

Open
larstalian wants to merge 5 commits into
mainfrom
cyber/breach-path-evo
Open

cyber: draw the breach path in the dashboard's Evolution view#356
larstalian wants to merge 5 commits into
mainfrom
cyber/breach-path-evo

Conversation

@larstalian

@larstalian larstalian commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Why

You asked me to make the dashboard's world scene really good — and to focus on the Evolution view rather than the Office (the office metaphor doesn't fit network/graph worlds).

I dug into the Evolution view and found it's well-built (stable layout, a strong add/remove/modify scrubber) but it laid nodes out by kind — so a cyber world read as a kind-sorted graph, not a breachable network. The one thing a reader actually wants from a cyber world — how is the flag won? — was invisible.

This draws the breach path: the reference solver's public-entry → flag chain, glowing end-to-end over dimmed decoys.

What

  • breach_path(graph) (cyber pack) returns the winning path as ordered on-path node ids + a summary (entry, flag, hops, exploit classes), reusing the same spine the difficulty metric reads — so the drawn path and the measured difficulty can't disagree. The credential chain follows the real enables walk (true kill-chain order), and _credential_walk now shares that one traversal.
  • It rides on lineage like world_difficulty: the builder stamps seeds; the pool's optional path_fn re-stamps evolved children. Core stays domain-agnostic — the path is computed pack-side and surfaced opaquely; _lineage_node passes it through.
  • The Evolution view glows on-path nodes and the edges between them, dims the decoys, marks the flag with a ★, and shows a per-step badge (2-hop chain · ssrf → flag · diff 36.9). New legend entry.

Verification

  • Live browser render (Playwright headless Chromium), driven + screenshotted across every state, asserting on the real DOM with zero console errors:
    • Evolved lineage, stepping the scrubber through 3 evolution steps: the path redraws each step and difficulty climbs 36.9 → 49.9 → 62.9, the badge tracks it (2-hop → 3-hop → 4-hop), and 2 newly-added nodes land on the path each step (the added + on-path overlay).
    • Flat world: short path, badge reads "direct read · broken authz → flag · diff 1.8".
    • Degraded (old run with no breach_path on lineage): no glow, no badge, flag star intact, no crash — graceful fallback.
  • A 4-dimension adversarial review (correctness · cross-layer wiring · .rules · render): 0 blockers, 0 majors. Its minor (chain ordered by id-suffix, not the solve walk) and two nits (loose PathFn type, docstring narrating WHAT) are fixed in this PR. One flagged issue ("flag star on every secret") was verified false — a cyber world has exactly one secret, always the flag.
  • New tests: breach-path connectivity, kill-chain ordering, lineage surfacing, the flagless-world None case, and the evolved-child re-stamp. Impacted suites green (cyber + dashboard + curriculum, 117 tests).

Follow-ups (deferred, not in this PR)

  • World-shaped layout — lay the Evolution graph out by zone (dmz → corp → data) with each service clustered with its endpoints/vuln, so the path reads straight instead of zig-zagging across kind-columns. (Mockup'd; the bigger change.)
  • An unrelated pre-existing src/openrange/agent.py edit (broadening shell-fence parsing for cold models) is in my working tree but deliberately excluded from this PR — flagging it so it isn't lost.

🤖 Generated with Claude Code

larstalian and others added 5 commits June 25, 2026 19:19
The Evolution graph laid nodes out by kind, so a cyber world read as a kind-sorted
graph, not a breachable network -- the one thing a reader wants (how is the flag won)
was invisible. This draws the reference solver's public-entry -> flag chain.

- breach_path(graph) (cyber pack) returns the winning path as ordered on-path node
  ids + a summary (entry, flag, hops, exploit classes), reusing the same spine the
  difficulty metric reads. The credential chain follows the real enables walk, not an
  id-suffix sort, so the kill-chain order is true; _credential_walk now shares that
  one ordered traversal (_credential_chain_vulns).
- It rides on lineage like world_difficulty: the builder stamps seeds, and the pool's
  optional path_fn re-stamps evolved children (core stays pack-agnostic -- the path is
  computed pack-side and surfaced opaquely). _lineage_node passes it through.
- The Evolution view glows the on-path nodes and the edges between them, dims the
  decoys, marks the flag with a star, and shows a per-step badge (hops, entry class,
  difficulty). New legend entry. Verified by a real headless render.

A 4-dimension adversarial review (correctness, cross-layer wiring, .rules, render)
came back with 0 blockers/majors; its minor (chain ordered by id-suffix) and nits
(loose PathFn type, docstring WHAT-narration) are fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An adversarial audit (reader + skeptic per file) checked every comment and
docstring this PR added against .rules. Verdict applied:

- breach_path: drop the docstring's second paragraph; the contract paragraph
  is the irreducible public-API WHY, the "stays in sync / off-path is decoy"
  note restated the module docstring and asserted no actionable invariant.
- _restamp_lineage: keep only the non-obvious invariant (snapshot_id is the
  graph hash, not derived from lineage, so stamping leaves a world's identity
  stable); the rest narrated WHAT the name + module docstring already say.
- two test comments deleted: one narrated the _spine_reaches assert and
  referenced the dashboard feature; the other restated the < comparison.
- the dashboard.css block comment deleted: it restated what an opacity drop
  obviously does and carried provenance owned by the JS class assignment.

Comment-only; breach_path tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Evolution view unions every step's nodes into one layout, keeping each node's
FIRST appearance. A chain hop's terminal flag-read becomes a relay when the chain
deepens (the harden mutation changes its kind), but the union froze the original
"credential_gated_flag" label -- so a 4-hop chain rendered as four flag-reads,
reading like multiple breaches instead of one chain to a single flag.

Capture the per-step label (the projection already sends the correct kind for each
step) and refresh each node's label on scrub. Now every step shows exactly one
credential_gated_flag -- the real terminal -- and the earlier hops read as relays.

Verified live: one flag-read per step (the correct terminal for that step), across
the full evolved lineage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Office (3D office sim) reads as an empty room for cyber/network worlds -- no NPCs,
no department geography -- so landing there hid all the world structure behind a tab
click. Default to the Evolution graph instead (the node-link view of the world + its
breach path); Office stays one click away for packs where it fits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The graph columned nodes by KIND (services | endpoints | vulns | data), so a breach
hop -- service -> endpoint -> vuln -> next service -- ran right then snapped back to
the far-left service column for every hop. A 4-hop chain made three full-width backward
jumps, so the path rendered as a scribble of crossing diagonals.

Column by position in the breach instead: each hop is one column, the loot sits past
the last hop, and off-path decoys hang off their nearest on-path column. The path now
flows strictly left->right. Measured on the seed chain world: backward jumps 3 -> 0,
on-path edge crossings 2 -> 0. Worlds with no recorded breach path keep the kind bands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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