Skip to content

fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101) - #3160

Open
sei-ahouseholder wants to merge 4 commits into
mainfrom
plan/3101-filter-cs-dimension-stale-override
Open

fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)#3160
sei-ahouseholder wants to merge 4 commits into
mainfrom
plan/3101-filter-cs-dimension-stale-override

Conversation

@sei-ahouseholder

Copy link
Copy Markdown
Contributor

Closes #3101.

The defect

FilterCsEmDimensionNode returning FAILURE on a missing case short-circuited the memory=False add_case_status Sequence before FinalizeCsFilterNode could clear BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE. The override was left stranded on the process-global blackboard, so a subsequent execution could commit a ledger entry against another case's override payload.

The root cause was structural: "what to do when a case evaporates underneath a node" was re-decided at ~78 call sites across vultron/core/behaviors/, drifting apart on both verdict (SUCCESS / FAILURE / fall-through) and loudness (silent / debug / warning / error) with no unifying principle.

The fix — unify the decision (ADR-0087)

Rather than patch the one node, this unifies the disposition into a single, role-indexed decision:

  • Two canonical helpers in behaviors/helpers.py, exposed as _require_case / _resolve_case_replica on all four DataLayer BT base classes:
    • require_caseRegime 1 (authoritative coordination): one canonical FAILURE at error level with a canonical feedback_message. Its first tuple element is typed VulnerabilityCase (not Optional) so callers need no post-guard narrowing; the contract-unsoundness is contained to three type: ignore returns in that one helper.
    • resolve_case_replicaRegime 2 (replica-apply): an absent local case is a routine SUCCESS-skip (partial replica; SYNC-02-002 / ADR-0073).
  • Migrated every Regime-1 site to the shared helper.
  • Examined every remaining direct read_case and either migrated it or marked it with a category comment: Regime 2 replica/seed, Regime 3 case-under-construction, lenient guards, module-level resolvers, audit-best-effort ledger appends.
  • Complementary blackboard carve-out (the actual FilterCsEmDimensionNode case-not-found FAILURE leaves BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE stale on blackboard #3101 fix): BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE is execution-scoped and is now reset in BTBridge.execute_with_setup's finally block via managed_keys on every outcome — so no short-circuit path can leak it forward. The disposition policy then prevents the class of bug from recurring.

Guard against re-drift

test/architecture/test_case_resolution_uses_helpers.py is an exact ratchet over the sanctioned direct-read_case allowlist: a new site or a stale allowlist entry both fail CI. Includes synthetic detector-validation tests.

Docs

ADR-0087 (case-resolution disposition policy) + index entry + mkdocs nav; bt-pitfalls.md tick-boundary note; BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE lifetime docstring.

Verification

  • Full suite green: 8492 passed, 0 failed (27 pre-existing xfails).
  • flake8 / mypy (193 files) / black clean across the behaviors tree and the new test.

🤖 Generated with Claude Code

…verride

Closes #3101.

FilterCsEmDimensionNode returning FAILURE on a missing case short-circuited
the memory=False add_case_status Sequence before FinalizeCsFilterNode could
clear BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE, stranding it on the process-global
blackboard so a later execution could commit a ledger entry against another
case's override payload. The root cause was that "what to do when a case
evaporates underneath a node" was re-decided at ~78 call sites, drifting apart
on both verdict (SUCCESS/FAILURE/fall-through) and loudness (silent/debug/
warning/error) with no unifying principle.

Rather than patch the one node, unify the decision (ADR-0087):

- Two canonical helpers in behaviors/helpers.py — require_case (Regime 1,
  authoritative coordination: one FAILURE at error level with a canonical
  feedback_message) and resolve_case_replica (Regime 2, replica-apply: absent
  local case is a routine SUCCESS-skip). Exposed as _require_case /
  _resolve_case_replica on all four DataLayer BT base classes. require_case's
  first tuple element is typed VulnerabilityCase (not Optional) so callers need
  no post-guard narrowing; the contract-unsoundness is contained to three
  type: ignore returns in that one helper.
- Migrated every Regime-1 site across the behaviors tree to the shared helper.
- Examined every remaining direct read_case and either migrated it or marked
  it with a category comment: Regime 2 replica/seed, Regime 3 case-under-
  construction, lenient guards, module-level resolvers, audit-best-effort
  ledger appends.
- Complementary blackboard carve-out: BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE is
  execution-scoped and now reset in BTBridge.execute_with_setup's finally block
  via managed_keys on every outcome, so no short-circuit path can leak it
  forward (the actual #3101 fix; disposition policy prevents recurrence).

Guard against re-drift: test/architecture/test_case_resolution_uses_helpers.py
is an exact ratchet over the sanctioned direct-read_case allowlist (new site or
stale entry both fail CI), plus synthetic detector-validation tests.

Docs: ADR-0087 (case-resolution disposition policy), index entry, mkdocs nav,
bt-pitfalls tick-boundary note, and the BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE
lifetime docstring.

Full suite green (8492 passed, 0 failed); flake8/mypy/black clean across the
behaviors tree and the new test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two fan-out recipient collectors (CollectLogEntryRecipientsNode,
CollectNonClosedLogEntryRecipientsNode) were flipped from a silent
zero-recipient SUCCESS to a Regime-1 hard FAILURE on an unresolvable case
(ADR-0087). The other three presumptive-bug flips were already pinned by
tests (cs_invariant_guards x2, TransitionCStoFixReady); these two were not.

Add TestFanoutCollectorsRequireCase: runs each collector with a valid
log_entry but no seeded case and asserts FAILURE, the canonical
"case '<id>' not found in DataLayer" feedback, and that no fanout_recipients
list is written (the flip is a hard fail, not a silent []).

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

@sei-ahouseholder sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Triage: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)

Linked issues: #3101 (FilterCsEmDimensionNode case-not-found FAILURE leaves BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE stale)
Changed files: 55 files — core/behaviors, docs, notes
CI status: ❌ failing (single failure — see below; pre-existing, resolved by sync)
Merge state: ✅ mergeable (UNSTABLE — the failing non-required check; branch is 36 commits behind main)
Base branch: main
Needs integration tests: yes


Findings

# Phase Severity Description Outcome
phase11-ci-pytest-agents-md-ratchet-stale-base-0 linter-ci ❌ FAIL CI pytest fails only on test_root_agents_md_within_target (AGENTS.md 405 > 400). PR does not touch AGENTS.md; already fixed on origin/main (391 lines). Merge with main is clean. Sync clears it. fix-now
phase8-ratchet-scope-granularity-blindspot-0 code-review ⚠️ IMPROVE Ratchet keys by (path, scope): a 2nd read_case in an already-allowlisted scope evades detection, weakening the "exact" claim fix-now
phase8-require-case-datalayer-none-no-log-0 code-review ⚠️ IMPROVE require_case datalayer-None branch omits the logger.error its two sibling failure branches emit (helpers.py:191) fix-now
phase8-nested-exec-nonmanaged-key-leak-0 code-review 🎫 NEW-ISSUE Pre-existing/out-of-scope: execute_with_setup restores only managed_keys; nested executions clobber /activity and context keys without restore. Not introduced by this PR. defer-ask

Total: 1 FAIL · 2 IMPROVE · 1 NEW-ISSUE


Assessment: The core fix is sound. The code-review sub-agent found no hard correctness or security bug — the require_case/resolve_case_replica design and its contained type: ignore unsoundness are correct, the ledger_payload_object_override reset is correct on all exit paths (including nested RLock executions), the ~50 node migrations preserve or intentionally-and-consistently tighten control flow, and the ratchet catches both drift directions. ADR-0087 follows the template; index + nav + bt-pitfalls.md are updated. The sole CI failure is a stale-base artifact already fixed on main.

Triage artifact: .claude/pr-3160-triage.json
Next step: /pr-execute or /pr-ship applies fixes.

- code-review — require_case datalayer-None branch now logs at error level,
  matching its two sibling failure branches (canonical single-report goal)
- code-review — case-resolution ratchet counts read_case calls per scope, so a
  second unsanctioned call inside an already-allowlisted scope trips CI (closes
  the set-collapse blind spot in the test's 'exact' guarantee)

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

@sei-ahouseholder sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Execute: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)

Fixes applied: 2 commits (2 IMPROVE findings + base sync)
Excursions filed and fixed: 0
Deferred (you approved): 0
Halted (inversion, awaiting you): 0
Tests run: unit + integration
CI status: ✅ passing (22/22 checks)
Base sync: ✅ merged main @ fb2b89743 — clean, 0 conflicts


Fixed

Finding Commit Closes
phase11-ci-pytest-agents-md-ratchet-stale-base-0: CI Tests (pytest) failed only on test_root_agents_md_within_target (AGENTS.md 405 > 400); PR never touched AGENTS.md — resolved by syncing main (391 lines) fb2b89743
phase8-ratchet-scope-granularity-blindspot-0: Harden the read_case ratchet to count calls per scope so a second unsanctioned read_case in an already-allowlisted scope trips CI 25c0ad71a
phase8-require-case-datalayer-none-no-log-0: Add the missing logger.error(...) to require_case's datalayer-None failure branch (ADR-0087 canonical error-level report) 25c0ad71a

Skipped

Finding Reason
phase8-nested-exec-nonmanaged-key-leak-0: execute_with_setup restores only managed_keys; /activity + **context_data can leak across nested executions Pre-existing and out of scope of #3101 (whose ledger_payload_object_override reset is complete). Latent-only — every current nested execute_with_setup site passes only tree=/actor_id=/sync_port=, never activity=/**context_data, so nothing is clobbered today. The general fix (snapshot/restore all setup_tree-written keys) needs domain judgment beyond #3101. Filed as type:Concern #3161 (Project #24, Backlog) with evidence, per BW-07-009.

Execute artifact: .claude/pr-3160-execute.json
Next step: run /pr-verify, or /pr-ship will continue automatically.

@sei-ahouseholder sei-ahouseholder left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Verify: #3160 — fix(behaviors): unify case-resolution disposition; fix stale ledger override (#3101)

Overall verdict: ✅ READY-TO-MERGE
CI status: ✅ passing (22/22 checks)
Merge state: ✅ MERGEABLE (CLEAN) — base main
Base sync in execute: ✅ merged @ fb2b89743 (0 conflicts resolved)
Integrity check: ✅ all 4 findings accounted for


Finding Verdicts

Finding Severity Outcome Verdict
phase11-ci-pytest-agents-md-ratchet-stale-base-0 ❌ FAIL fixed @ fb2b89743 (sync main) ✅ CONFIRMED
phase8-ratchet-scope-granularity-blindspot-0 ⚠️ IMPROVE fixed @ 25c0ad71a ✅ CONFIRMED
phase8-require-case-datalayer-none-no-log-0 ⚠️ IMPROVE fixed @ 25c0ad71a ✅ CONFIRMED
phase8-nested-exec-nonmanaged-key-leak-0 🎫 NEW-ISSUE skipped — pre-existing Concern #3161 📋 NOTED

Spot-check detail (verified at HEAD, not just the commit diff):

  • AGENTS.md ratchet: the sole CI failure (test_root_agents_md_within_target, 405 > 400) was a stale-base artifact — the PR never touched AGENTS.md, and main had already condensed it. At HEAD AGENTS.md is 391 lines and Tests (pytest) is green.
  • read_case ratchet hardening: test_case_resolution_uses_helpers.py now counts read_case calls per scope (Counter, _read_case_counts, self.counts) so a second unsanctioned call in an already-allowlisted scope trips CI. Present at HEAD.
  • require_case log line: the datalayer-None failure branch now emits node.logger.error(...) (helpers.py:193), matching the other two failure branches. Present at HEAD.
  • Nested-execution key leak: pre-existing, out of scope of #3101, latent-only (no current nested execute_with_setup site passes activity=/**context_data). Tracked as type:Concern #3161 (open, Project #24) with evidence per BW-07-009 — correctly not folded into a #3101 fix.

Artifacts cleaned up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

specs-notes Docs-only PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FilterCsEmDimensionNode case-not-found FAILURE leaves BB_LEDGER_PAYLOAD_OBJECT_OVERRIDE stale on blackboard

2 participants