Description
Same defect class as #5899 (checkpoint methods silently dropped at a wrapper boundary), but on the two outermost wrapper layers, which means fixing #5899 alone will not restore /undo//redo whenever capability_scopes or shadow_sentinel is enabled.
ScopedToolExecutor (crates/zeph-tools/src/scope.rs:435-614) and ShadowProbeExecutor (crates/zeph-tools/src/shadow_probe.rs:205-391) both implement ToolExecutor for a generic inner executor E/T and forward most cross-cutting methods (set_skill_env, set_effective_trust, is_tool_retryable, is_tool_speculatable) to self.inner. Neither overrides checkpoint_undo, checkpoint_redo, or checkpoint_list — grepping both files for checkpoint returns zero matches outside doc/comment text.
Per scope.rs's own module doc, the production wiring order is:
ScopedToolExecutor ← outermost (this crate)
→ ShadowProbeExecutor ← wired when shadow_sentinel.enabled = true
→ PolicyGateExecutor
→ TrustGateExecutor
→ CompositeExecutor
→ ToolFilter, AuditedExecutor, ...
confirmed in src/runner.rs:2680-2797, where tool_executor is progressively rewrapped: build_scoped_executor(tool_executor, ...) (line 2705) produces the ScopedToolExecutor, then (if shadow_sentinel.enabled) ShadowProbeExecutor::new(tool_executor, ...) (line 2783) wraps that again. The resulting tool_executor is the top-level Arc<dyn ErasedToolExecutor> that /undo, /redo, /undo list call via checkpoint_undo_erased/checkpoint_redo_erased/checkpoint_list_erased (crates/zeph-core/src/agent/agent_access_impl.rs:1692,1727,1746).
Because neither wrapper overrides the three checkpoint methods, any checkpoint call on the fully composed executor falls through to ToolExecutor's default (CheckpointActionResult::unsupported() / CheckpointListResult { supported: false, .. }) as soon as capability_scopes (any scopes configured) or shadow_sentinel.enabled = true is set — even after #5899's fix lands for TrustGateExecutor/PolicyGateExecutor/AdversarialPolicyGateExecutor/CompositeExecutor, because those inner gates are never reached; the call is swallowed one or two layers before it gets there.
Both config knobs are opt-in (capability_scopes.scopes empty and shadow_sentinel.enabled = false by default), so this is dormant in the default config, but both are documented, spec-driven security features (spec 050) intended for production use, not edge cases.
Reproduction Steps
- Configure
[tools.shell] checkpoints_enabled = true.
- Configure at least one
[security.capability_scopes.<name>] scope (or set [security.shadow_sentinel] enabled = true).
- Run a shell command that mutates a file (checkpoint capture occurs inside
ShellExecutor).
- Run
/undo list.
- Observe: "Checkpoints are not enabled..." is returned, even though
checkpoints_enabled = true and a checkpoint was captured — because ScopedToolExecutor/ShadowProbeExecutor sit in front of the chain and never forward the call.
Expected Behavior
/undo, /redo, /undo list should operate on the checkpoint stack regardless of which optional security wrappers are active — these wrappers should be transparent to checkpoint operations, exactly like CompositeExecutor already is.
Actual Behavior
Checkpoint commands report "unsupported" whenever ScopedToolExecutor or ShadowProbeExecutor wraps the executor chain, independent of the inner gates' own forwarding correctness.
Environment
- HEAD: 2616f7b (zeph-tools rotation sweep, CI cycle 1285)
- Crate:
zeph-tools — scope.rs, shadow_probe.rs
Suggested Fix
Add to impl<E: ToolExecutor> ToolExecutor for ScopedToolExecutor<E> (scope.rs) and impl<T: ToolExecutor> ToolExecutor for ShadowProbeExecutor<T> (shadow_probe.rs):
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
self.inner.checkpoint_undo(n)
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
self.inner.checkpoint_redo()
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
self.inner.checkpoint_list()
}
Add a regression test in each file mirroring composite.rs's state_forwarding test module, asserting checkpoint_list().supported == true propagates through both wrappers when the inner executor supports checkpoints.
Related: #5899 (same class, inner gate trio), #5900 (requires_confirmation/is_tool_speculatable forwarding gaps), #3869 (original defect class — set_skill_env/set_effective_trust across CompositeExecutor).
Description
Same defect class as #5899 (checkpoint methods silently dropped at a wrapper boundary), but on the two outermost wrapper layers, which means fixing #5899 alone will not restore
/undo//redowhenevercapability_scopesorshadow_sentinelis enabled.ScopedToolExecutor(crates/zeph-tools/src/scope.rs:435-614) andShadowProbeExecutor(crates/zeph-tools/src/shadow_probe.rs:205-391) both implementToolExecutorfor a generic inner executorE/Tand forward most cross-cutting methods (set_skill_env,set_effective_trust,is_tool_retryable,is_tool_speculatable) toself.inner. Neither overridescheckpoint_undo,checkpoint_redo, orcheckpoint_list— grepping both files forcheckpointreturns zero matches outside doc/comment text.Per
scope.rs's own module doc, the production wiring order is:confirmed in
src/runner.rs:2680-2797, wheretool_executoris progressively rewrapped:build_scoped_executor(tool_executor, ...)(line 2705) produces theScopedToolExecutor, then (ifshadow_sentinel.enabled)ShadowProbeExecutor::new(tool_executor, ...)(line 2783) wraps that again. The resultingtool_executoris the top-levelArc<dyn ErasedToolExecutor>that/undo,/redo,/undo listcall viacheckpoint_undo_erased/checkpoint_redo_erased/checkpoint_list_erased(crates/zeph-core/src/agent/agent_access_impl.rs:1692,1727,1746).Because neither wrapper overrides the three checkpoint methods, any checkpoint call on the fully composed executor falls through to
ToolExecutor's default (CheckpointActionResult::unsupported()/CheckpointListResult { supported: false, .. }) as soon ascapability_scopes(any scopes configured) orshadow_sentinel.enabled = trueis set — even after #5899's fix lands forTrustGateExecutor/PolicyGateExecutor/AdversarialPolicyGateExecutor/CompositeExecutor, because those inner gates are never reached; the call is swallowed one or two layers before it gets there.Both config knobs are opt-in (
capability_scopes.scopesempty andshadow_sentinel.enabled = falseby default), so this is dormant in the default config, but both are documented, spec-driven security features (spec 050) intended for production use, not edge cases.Reproduction Steps
[tools.shell] checkpoints_enabled = true.[security.capability_scopes.<name>]scope (or set[security.shadow_sentinel] enabled = true).ShellExecutor)./undo list.checkpoints_enabled = trueand a checkpoint was captured — becauseScopedToolExecutor/ShadowProbeExecutorsit in front of the chain and never forward the call.Expected Behavior
/undo,/redo,/undo listshould operate on the checkpoint stack regardless of which optional security wrappers are active — these wrappers should be transparent to checkpoint operations, exactly likeCompositeExecutoralready is.Actual Behavior
Checkpoint commands report "unsupported" whenever
ScopedToolExecutororShadowProbeExecutorwraps the executor chain, independent of the inner gates' own forwarding correctness.Environment
zeph-tools—scope.rs,shadow_probe.rsSuggested Fix
Add to
impl<E: ToolExecutor> ToolExecutor for ScopedToolExecutor<E>(scope.rs) andimpl<T: ToolExecutor> ToolExecutor for ShadowProbeExecutor<T>(shadow_probe.rs):Add a regression test in each file mirroring
composite.rs'sstate_forwardingtest module, assertingcheckpoint_list().supported == truepropagates through both wrappers when the inner executor supports checkpoints.Related: #5899 (same class, inner gate trio), #5900 (
requires_confirmation/is_tool_speculatableforwarding gaps), #3869 (original defect class —set_skill_env/set_effective_trustacrossCompositeExecutor).