Skip to content

chore: land the session/inventory + named-sessions work into main (recovery for #60/#61) - #62

Merged
henleda merged 6 commits into
mainfrom
feat-session-inventory
Aug 19, 2026
Merged

chore: land the session/inventory + named-sessions work into main (recovery for #60/#61)#62
henleda merged 6 commits into
mainfrom
feat-session-inventory

Conversation

@henleda

@henleda henleda commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Why this exists: #60 and #61 were merged, but into their stacked base branches (#60 → fix-hero-relevance, #61 → feat-session-inventory) rather than main — the stacked-PR bases weren't retargeted to main after #59 merged. Net result: main has #59 (hero) but not inventory.py / sessions.py or any of the split. This PR lands that already-reviewed, CI-green work into main.

Contents (identical to merged #60 + #61):

Merges into main with zero conflicts (verified locally). Full suite 1209 passed, 15 skipped; ruff clean on 3.10/3.11/3.12.

🤖 Generated with Claude Code

Daniel Henley and others added 6 commits August 18, 2026 18:58
… from infrastructure state

The Retire tab mixed data across sessions: a 5-day-old band-aid applied to one app showed up
beside a later scan of a different app. Root cause — live band-aids lived in the per-session
ledger, and init_from_scan had to LEAK them across scans (the old "never orphan a live patch"
keep-block) so reconcile could still find them. That leak is the mixing.

Split the two kinds of state:
- a SESSION owns ephemeral scan artifacts (findings/triage/policies/report) + a ledger that is
  now scoped STRICTLY to that scan's findings (leak removed).
- a global inventory.json is the session-independent record of what band-aid is attached to which
  LB/appliance, its TTL + cure — written on apply, cure attached on remediate, retired on retire.
  Retire + reconcile read it, always; a live patch can no longer be orphaned because it lives here.

Mechanics:
- new inventory.py (keyed by finding_id; entry mirrors a ledger entry + `session` provenance).
- ledger.mark_mitigated/remediated/retired WRITE THROUGH to inventory, so none of the ~10 apply
  call sites change; init_from_scan's cross-session keep-block is deleted.
- reconcile + retire + list_patches read the global inventory; reconcile resolves each entry's own
  `session` for its probe spec + audit trail, and its pass-lock is now global. A pass/list also
  auto-migrates the given out_dir's live patches (idempotent safety net).
- console: Retire tab is now two labeled sections — a cross-session "Live band-aids on your
  infrastructure" list (control @ target · applied date · TTL, with Retire) over a read-only
  "This session's findings" track; do_retire routes by the inventory entry (cross-session); a
  lifespan startup seeds the inventory from existing out* dirs (one-time migration).
- inventory.json / reconcile.lock gitignored (runtime state); autouse test fixture isolates the
  inventory dir per test.

Full suite: 1194 passed, 15 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… a live band-aid

Adversarial review caught the split re-introducing the exact orphaning it set out to kill:
the inventory was keyed by finding_id alone, but finding_ids are per-scan LLM slugs that recur
across apps (the repo's own ledgers show neg-pay-001 in out/, out-dgx/, out-claude/). Two live
band-aids sharing a slug on different LBs collapsed into one row — the other stayed physically
attached but vanished from Retire + reconcile.

- key the inventory by lb + finding_id (`_key`); record_mitigated / record_reconcile / mark_retired
  address one band-aid, and migrate_from_dirs keys the same way (fixing the first-dir-wins mirror).
- attach_cure (was record_remediated) attaches a merged cure to EVERY LB the finding was patched on
  — a code fix cures it everywhere — and still mints nothing for a code-cure-only finding.
- retire gains an `lb` selector end-to-end (retire_finding / console do_retire+RetireReq / CLI --lb /
  MCP): given only a finding_id it retires the single live match, and REFUSES (naming the LBs) when
  a slug is live on two — detaching the wrong control would re-expose an app. The Retire tab passes
  each row's lb; its retire-status id is now keyed by finding_id+lb (two rows can share a slug).
- appliance retires (bigip/nginx) update the inventory explicitly with their reconstructed lb, since
  ledger.mark_retired no longer writes through (it lacks the lb).

Also from the review:
- reconcile now migrates out_dir AND its sibling session dirs, so a headless CLI/cron pass picks up
  a pre-split band-aid recorded in another session (was: console-only).
- reconcile's "already detached" retire branch advances the session ledger too, so impact()'s
  per-session hero stops counting a detached band-aid as live.

Full suite: 1198 passed, 15 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI runs `ruff check src tests` (I'd only run pytest locally). Three errors:
- reconcile.py: the composite-key change left the loop var `key` unused (B007) in
  the reconcile driver and list_patches — renamed to _k (still sorts by the key).
- test_console_inventory.py: unused `import time` (F401).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…space

The final piece of the session/inventory split: make the active scan session a first-class,
visible thing so the session-scoped tabs can never silently show two runs.

- A session is an out* dir. GET /api/sessions lists every workspace (findings count, friendly name,
  active flag); POST /api/session opens an existing one or creates a fresh named one (out-<slug> +
  session.json), setting the OUT the read/scan endpoints use.
- Header gains a session selector + "+ New"; switching hard-reloads so every tab repopulates from
  the chosen session, and a scan defaults its output dir to the active session.
- open is guarded to an allowlist of discovered dirs (no path traversal); new slugifies the name.

Agent-native parity: a read-only MCP `sessions` tool lists the same workspaces (an agent targets a
session via the `out` param every tool already takes), and the MCP `retire` tool now exposes the
`lb` selector the CLI/console gained, so an agent can disambiguate a finding live on two LBs.

Full suite: 1206 passed, 15 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… create-session gap

Adversarial review of the named-sessions feature (2 finders found nothing — traversal guard and
XSS escaping held):

- A summary.json/session.json that is valid JSON but NOT an object (null / 42 / a list) made
  {}.update(loaded) raise TypeError/ValueError, uncaught — 500-ing all of /api/sessions and the MCP
  `sessions` tool over ONE damaged file. New shared `sessions.read_meta` returns {} for a missing,
  unparseable, OR non-object sidecar; both surfaces use it.
- Agent-native parity gap: the console's 'New session' writes a friendly session.json name, but no
  MCP tool could create a named session — an agent could read the name it can't set. New WRITES_OUT
  `session_new` tool creates out-<slug> + session.json via the SAME shared helper the console now
  uses (`sessions.create_session` / `slugify`), so the slug rules and metadata shape can't drift.

Full suite: 1209 passed, 15 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(console): named sessions — explicit, switchable active scan workspace
@henleda
henleda merged commit 54bc607 into main Aug 19, 2026
4 checks passed
@henleda
henleda deleted the feat-session-inventory branch August 19, 2026 02:06
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