Skip to content

feat(inventory): split scan sessions from a global live-band-aid inventory - #60

Merged
henleda merged 3 commits into
fix-hero-relevancefrom
feat-session-inventory
Aug 19, 2026
Merged

feat(inventory): split scan sessions from a global live-band-aid inventory#60
henleda merged 3 commits into
fix-hero-relevancefrom
feat-session-inventory

Conversation

@henleda

@henleda henleda commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Stacked on #59 (fix-hero-relevance) — merge that first; this PR retargets to main once it lands.

The problem

The Retire tab mixed data across sessions: a 5-day-old band-aid applied to one app (neg-pay-001 @ nimbus-www) showed up beside a later scan of a different app (Larkspur). Root cause — live band-aids lived in the per-session ledger.json, 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.

The split

Two genuinely different kinds of state, now separated:

owns lifetime scope
session (out* dir) findings / triage / policies / report + a ledger scoped strictly to that scan ephemeral per-session
inventory.json (global) what band-aid is attached to which LB, its TTL + cure until retired cross-session

Retire + reconcile read the inventory, always — so a live patch can no longer be orphaned (it doesn't live in the session ledger anymore), and the old cross-session keep-block is deleted.

Mechanics

  • inventory.py — global store keyed by finding_id; entry mirrors a ledger entry + a session field (for reconcile's per-entry probe/audit resolution).
  • ledger.mark_mitigated/remediated/retired write through to the inventory, so none of the ~10 apply call sites change; _init_from_scan's keep-block is removed.
  • 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 global. A pass/list also auto-migrates the given out_dir (idempotent safety net so a pass never misses a live patch).
  • 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), and a lifespan startup seeds the inventory from existing out* dirs (one-time migration).
  • inventory.json / reconcile.lock gitignored; an autouse fixture isolates the inventory dir per test.

Verification

  • Full suite: 1194 passed, 15 skipped (new test_inventory.py, test_console_inventory.py; test_reconcile.py updated for the data move).
  • Live: the migration pulled all 6 cross-session live band-aids into the inventory, each tagged with its provenance session; the Retire tab renders them labeled and unmixed (screenshot verified).

🤖 Generated with Claude Code

Daniel Henley and others added 2 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>
@henleda
henleda merged commit c227e0e into fix-hero-relevance Aug 19, 2026
4 checks passed
henleda added a commit that referenced this pull request Aug 19, 2026
chore: land the session/inventory + named-sessions work into main (recovery for #60/#61)
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