Skip to content

fix(security): wire security controls into all agent entry points#6587

Merged
bug-ops merged 1 commit into
mainfrom
fix/6578-wire-entrypoint-security
Jul 20, 2026
Merged

fix(security): wire security controls into all agent entry points#6587
bug-ops merged 1 commit into
mainfrom
fix/6578-wire-entrypoint-security

Conversation

@bug-ops

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

Copy link
Copy Markdown
Owner

Summary

Three security- and fidelity-relevant controls were wired only from src/runner.rs (the CLI/TUI entry point) and silently skipped in src/daemon.rs (A2A/gateway daemon), src/acp.rs (ACP/IDE embedding), and src/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).

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, RiskChainAccumulator is constructed once per connection/server and shared (same Arc) across all concurrent sessions. Since Agent::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 on wire_risk_chain and the relevant field docs. Follow-up issue to be filed for per-session isolation.

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 (14880 passed)
  • Rustdoc gate (RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace ...)
  • New unit tests in agent_setup.rs verifying the shared helpers are exercised by production call sites (Arc strong-count based, given Agent::services is pub(crate) to zeph-core)
  • Independently verified: all 4 entry points call the same 3 shared helpers with identical argument shapes; config threading traced back to real operator config in each mode; same RiskChainAccumulator Arc shared between ShellExecutor and Agent so begin_turn() reset semantics hold; MAGE enabled flag still respected in all modes; runner.rs refactor confirmed behavior-preserving

Closes #6578
Closes #6579
Closes #6574

@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation size/XL Extra large PR (500+ lines) and removed bug Something isn't working labels Jul 20, 2026
@bug-ops
bug-ops enabled auto-merge (squash) July 20, 2026 14:55
@bug-ops
bug-ops force-pushed the fix/6578-wire-entrypoint-security branch from 985eabc to 3845997 Compare July 20, 2026 14:56
@github-actions github-actions Bot added the bug Something isn't working label Jul 20, 2026
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
bug-ops force-pushed the fix/6578-wire-entrypoint-security branch from 3845997 to 2721135 Compare July 20, 2026 15:07
@bug-ops
bug-ops merged commit f7b6b5f into main Jul 20, 2026
43 checks passed
@bug-ops
bug-ops deleted the fix/6578-wire-entrypoint-security branch July 20, 2026 15:18
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment