fix(security): wire security controls into all agent entry points#6587
Merged
Conversation
bug-ops
enabled auto-merge (squash)
July 20, 2026 14:55
bug-ops
force-pushed
the
fix/6578-wire-entrypoint-security
branch
from
July 20, 2026 14:56
985eabc to
3845997
Compare
RiskChainAccumulator, the MAGE shadow-memory trajectory risk gate, and typed-page CAM fidelity enforcement were each constructed only in src/runner.rs (CLI/TUI) and silently absent in src/daemon.rs, src/acp.rs, and src/serve, leaving those network-facing deployment modes without controls that were assumed active. Extract shared helpers in src/agent_setup.rs (wire_risk_chain, build_typed_pages_state, apply_entrypoint_security_controls) and call them identically from all four entry points. Closes #6578 Closes #6579 Closes #6574
bug-ops
force-pushed
the
fix/6578-wire-entrypoint-security
branch
from
July 20, 2026 15:07
3845997 to
2721135
Compare
10 tasks
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…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
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…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
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…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
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…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
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…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
bug-ops
added a commit
that referenced
this pull request
Jul 20, 2026
…n gaps (#6602) 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
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.
Summary
Three security- and fidelity-relevant controls were wired only from
src/runner.rs(the CLI/TUI entry point) and silently skipped insrc/daemon.rs(A2A/gateway daemon),src/acp.rs(ACP/IDE embedding), andsrc/serve/agent_factory.rs/src/serve/deps.rs(Telegram/Discord/Slack multi-channel serving) — the latest instances of a recurring "wire X into ACP/serve/daemon" defect class tracked in #6581 (23+ confirmed instances).RiskChainAccumulator(RiskChainAccumulator shell attack-chain detector is never constructed in daemon/ACP/serve — only wired in the CLI/TUI runner path #6578) — the shell executor's multi-step attack-chain detector (SensitiveRead->NetworkEgress, e.g.exfil_read_then_send/cred_then_egress). Never constructed outside the CLI path, soShellExecutor::execute's risk-chain check was a silent no-op in 3 of 4 deployment modes.mage_accumulator, MAGE shadow-memory trajectory risk gate (mage_accumulator) wired only in CLI/TUI runner path — inert in daemon/ACP/serve #6579) — stayed onTrajectoryRiskAccumulator::new_noop()outside the CLI path, regressing previously-closed mage_accumulator always initialized as noop — shadow_memory config not wired in agent builder #4417.TypedPagesState, typed-page CAM fidelity enforcement (memory.compression.typed_pages) is wired only in the CLI/TUI runner path, silently inert in daemon/ACP/serve #6574) — never built outside the CLI path, so invariant enforcement/audit for compaction was unconditionally disabled elsewhere.All three are now extracted into shared helpers in
src/agent_setup.rs(wire_risk_chain,build_typed_pages_state,apply_entrypoint_security_controls) called identically from all four entry points, so a future dropped call site is a one-line diff against a shared, tested helper rather than a silently-incomplete duplicated wiring block.Known limitation (documented, not fixed here)
In ACP and serve modes,
RiskChainAccumulatoris constructed once per connection/server and shared (sameArc) across all concurrent sessions. SinceAgent::begin_turn()unconditionally resets its per-turn state, concurrent sessions can interfere with each other's detection state (false negative/positive, worst for multi-tenant serve). This is strictly better than the pre-fix state (no detection at all in these modes); true per-session isolation is out of scope for this wiring-consistency fix and is documented inline onwire_risk_chainand the relevant field docs. Follow-up issue to be filed for per-session isolation.Test plan
cargo +nightly fmt --checkcargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warningscargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins(14880 passed)RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace ...)agent_setup.rsverifying the shared helpers are exercised by production call sites (Arc strong-count based, givenAgent::servicesispub(crate)tozeph-core)RiskChainAccumulatorArc shared betweenShellExecutorandAgentsobegin_turn()reset semantics hold; MAGEenabledflag still respected in all modes;runner.rsrefactor confirmed behavior-preservingCloses #6578
Closes #6579
Closes #6574