Skip to content

fix(security): close RiskChainAccumulator cross-turn and cross-session gaps#6602

Merged
bug-ops merged 2 commits into
mainfrom
fix/6561-risk-chain-lifecycle
Jul 20, 2026
Merged

fix(security): close RiskChainAccumulator cross-turn and cross-session gaps#6602
bug-ops merged 2 commits into
mainfrom
fix/6561-risk-chain-lifecycle

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

RiskChainAccumulator (crates/zeph-tools/src/risk_chain.rs) is the shell executor's multi-step exfiltration-chain detector — it blocks a command when it observes an ordered pattern like a sensitive-file read followed by network egress. This PR closes three related gaps in that control, all surfacing after PR #6587's entry-point wiring landed:

  • RiskChainAccumulator (exfil_read_then_send / cred_then_egress) is turn-scoped and its cross-turn fallback is unwired #6561 — the accumulator was fully cleared at every turn boundary, so a chain deliberately split across turns (read in turn N, exfiltrate in turn N+1) went completely undetected, despite the module doc claiming a cross-turn fallback existed. reset() is replaced by advance_turn(), which prunes only calls older than a bounded 3-turn window and recomputes the cumulative score from what remains, so a split chain within the window is still caught. A dedup marker prevents one live chain from re-pushing its signal on every subsequent call while still matched, which would otherwise let a single detection flood TrajectorySentinel into a false session-wide block. Signal codes for the two chain patterns are now also explicitly mapped in RiskSignal::from_code at an appropriate weight — previously they fell through to the lowest fallback tier, making a confirmed chain-fire near-invisible to TrajectorySentinel/MAGE.
  • RiskChainAccumulator is shared across concurrent sessions in ACP/serve, not per-session #6588 — ACP and serve built one RiskChainAccumulator per connection/server and shared it via Arc across every concurrent session, so one session's turn reset could wipe another session's chain state, or unrelated sessions' activity could combine into a spurious block. Each session now gets its own ShellExecutor/RiskChainAccumulator built at session-construction time, composed over the shared connection/server base (sandbox, audit logger, permission policy).
  • add end-to-end behavioral test for RiskChainAccumulator wiring across entry points #6589 — added end-to-end tests per entry point (runner, daemon, acp, serve) dispatching a real chained tool-call sequence through each entry point's production executor construction path and asserting the block, plus dedicated regression tests for the cross-turn split and session isolation.

A follow-up P3 issue will be filed separately for making the 3-turn cross-turn window configurable (currently a documented, bounded constant) rather than blocking this PR on it.

Test plan

  • cargo +nightly fmt --check
  • cargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warnings
  • cargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins — 14962 passed, 0 failed, 36 skipped
  • rustdoc gate (RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links")
  • cargo test --doc for touched crates
  • gitleaks protect --staged
  • New unit tests: cross-turn split detection, window aging-out, signal dedup, signal-weight mapping
  • New e2e tests per entry point (runner/daemon/acp/serve) through real production executor construction
  • .local/testing/playbooks/risk-chain-accumulator.md and .local/testing/coverage-status.md updated
  • Live multi-session testing (two concurrent ACP/serve sessions via a real client) — not yet performed, flagged Untested in coverage-status.md pending a live-tester CI cycle

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust Rust code changes core zeph-core crate bug Something isn't working size/XL Extra large PR (500+ lines) labels Jul 20, 2026
@bug-ops
bug-ops enabled auto-merge (squash) July 20, 2026 18:50
@bug-ops
bug-ops force-pushed the fix/6561-risk-chain-lifecycle branch from 9938dae to 45ea85b Compare July 20, 2026 19:02
@bug-ops
bug-ops force-pushed the fix/6561-risk-chain-lifecycle branch 3 times, most recently from ea78e8d to c2ba570 Compare July 20, 2026 19:55
bug-ops added 2 commits July 20, 2026 22:02
…n gaps

RiskChainAccumulator (crates/zeph-tools/src/risk_chain.rs) detects
multi-step exfiltration chains (sensitive read followed by network
egress) and blocks the shell command when a chain fires. Three related
gaps in this control, all following PR #6587's entry-point wiring:

- The accumulator was fully cleared at every turn boundary, so a chain
  deliberately split across turns (read in turn N, exfiltrate in turn
  N+1) went completely undetected despite the module doc claiming a
  cross-turn fallback existed. `reset()` is replaced by `advance_turn()`,
  which prunes only calls older than a bounded window (3 turns) and
  recomputes the cumulative score from what remains, so a split chain
  within the window is still caught. A dedup marker prevents a single
  live chain from re-pushing its signal on every subsequent call while
  it stays matched, which would otherwise let one detection flood
  TrajectorySentinel into a false session-wide block.

- Signal codes for the two chain patterns were never mapped in
  RiskSignal::from_code, so a confirmed chain-fire signal reaching
  TrajectorySentinel fell through to the lowest-weight fallback tier.
  Added explicit ExfilReadThenSend/CredThenEgress variants weighted
  in line with the other high-confidence signals.

- ACP and serve built one RiskChainAccumulator per connection/server
  and shared it via Arc across every concurrent session, so one
  session's turn reset could wipe another session's chain state or
  combine unrelated sessions' activity into a spurious block. Each
  session now gets its own ShellExecutor/RiskChainAccumulator built at
  session-construction time, composed over the shared connection/server
  base (sandbox, audit logger, permission policy).

Added end-to-end tests per entry point (runner, daemon, acp, serve)
that dispatch a real chained tool-call sequence through each entry
point's production executor construction path and assert the block,
plus dedicated regression tests for the cross-turn split and the
session-isolation behavior.

Closes #6561
Closes #6588
Closes #6589
… feature

Both call sites of this helper (introduced by the #6588 per-session
isolation fix) live in acp.rs behind #[cfg(feature = "acp")], but the
function's own definition in agent_setup.rs had no matching gate. Any
feature bundle without acp (bench, chat, desktop, server) compiled it
with zero callers, tripping dead_code under this project's
build.warnings = deny.
@bug-ops
bug-ops force-pushed the fix/6561-risk-chain-lifecycle branch from c2ba570 to 0f84d43 Compare July 20, 2026 20:02
@bug-ops
bug-ops merged commit 0c99f2e into main Jul 20, 2026
43 checks passed
@bug-ops
bug-ops deleted the fix/6561-risk-chain-lifecycle branch July 20, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working core zeph-core crate documentation Improvements or additions to documentation rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant