chore: land the session/inventory + named-sessions work into main (recovery for #60/#61) - #62
Merged
Merged
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this exists: #60 and #61 were merged, but into their stacked base branches (
#60 → fix-hero-relevance,#61 → feat-session-inventory) rather thanmain— the stacked-PR bases weren't retargeted tomainafter #59 merged. Net result:mainhas #59 (hero) but notinventory.py/sessions.pyor any of the split. This PR lands that already-reviewed, CI-green work intomain.Contents (identical to merged #60 + #61):
inventory.jsonkeyed by(lb, finding_id); the cross-session leak removed frominit_from_scan; reconcile/retire/Retire-tab read the inventory; the two-section Retire tab; migration.GET/POST /api/sessions(list/open/new), sharedsessions.py, agent-native MCPsessions+session_newtools + retirelbselector.Merges into
mainwith zero conflicts (verified locally). Full suite 1209 passed, 15 skipped; ruff clean on 3.10/3.11/3.12.🤖 Generated with Claude Code