use std::sync::Arc;
use tracing::{Instrument as _, info_span};
use crate::SkillTrustLevel;
use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::registry::ToolDef;
use crate::trust_gate::{is_quarantine_denied, quarantine_denial_message};
pub trait ProbeGate: Send + Sync {
fn probe<'a>(
&'a self,
qualified_tool_id: &'a str,
args: &'a serde_json::Value,
turn_number: u64,
risk_level: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>;
fn record<'a>(
&'a self,
qualified_tool_id: &'a str,
turn_number: u64,
risk_level: &'a str,
context_summary: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
let _ = (qualified_tool_id, turn_number, risk_level, context_summary);
Box::pin(async {})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProbeOutcome {
Allow,
Deny {
reason: String,
},
Skip,
}
pub struct ShadowProbeExecutor<T: ToolExecutor> {
inner: T,
probe: Arc<dyn ProbeGate>,
turn_number: Arc<std::sync::atomic::AtomicU64>,
risk_level: Arc<parking_lot::RwLock<String>>,
effective_trust: std::sync::atomic::AtomicU8,
}
impl<T: ToolExecutor + std::fmt::Debug> std::fmt::Debug for ShadowProbeExecutor<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShadowProbeExecutor")
.field("inner", &self.inner)
.finish_non_exhaustive()
}
}
impl<T: ToolExecutor> ShadowProbeExecutor<T> {
#[must_use]
pub fn new(
inner: T,
probe: Arc<dyn ProbeGate>,
turn_number: Arc<std::sync::atomic::AtomicU64>,
risk_level: Arc<parking_lot::RwLock<String>>,
) -> Self {
Self {
inner,
probe,
turn_number,
risk_level,
effective_trust: std::sync::atomic::AtomicU8::new(SkillTrustLevel::Trusted.severity()),
}
}
fn current_turn(&self) -> u64 {
self.turn_number.load(std::sync::atomic::Ordering::Acquire)
}
fn current_risk_level(&self) -> String {
self.risk_level.read().clone()
}
fn effective_trust(&self) -> SkillTrustLevel {
SkillTrustLevel::from_severity(
self.effective_trust
.load(std::sync::atomic::Ordering::Relaxed),
)
}
fn quarantine_denial_reason(&self, call: &ToolCall) -> Option<String> {
if self.effective_trust() == SkillTrustLevel::Quarantined
&& is_quarantine_denied(call.tool_id.as_str())
{
let active_skills = call.skill_name.as_deref().unwrap_or(&[]);
return Some(quarantine_denial_message(
call.tool_id.as_str(),
active_skills,
));
}
None
}
fn context_summary_for_result(result: &Result<Option<ToolOutput>, ToolError>) -> String {
match result {
Ok(Some(output)) => output.summary.clone(),
Ok(None) => "tool call completed with no output".to_owned(),
Err(e) => format!("tool call failed: {e}"),
}
}
}
impl<T: ToolExecutor> ToolExecutor for ShadowProbeExecutor<T> {
async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
self.inner.execute(response).await
}
async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
self.inner.execute_confirmed(response).await
}
fn tool_definitions(&self) -> Vec<ToolDef> {
self.inner.tool_definitions()
}
async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
let turn = self.current_turn();
let risk = self.current_risk_level();
if let Some(reason) = self.quarantine_denial_reason(call) {
tracing::warn!(
tool_id = %call.tool_id,
reason = %reason,
"ShadowProbeExecutor: quarantine short-circuit denied tool call"
);
self.probe
.record(
call.tool_id.as_str(),
turn,
&risk,
&format!("quarantine short-circuit: {reason}"),
)
.await;
return Err(ToolError::SafetyDenied { reason });
}
let span = info_span!(
"security.shadow.probe_executor",
tool_id = %call.tool_id
);
let args = serde_json::Value::Object(call.params.clone());
let outcome = self
.probe
.probe(call.tool_id.as_str(), &args, turn, &risk)
.instrument(span)
.await;
match outcome {
ProbeOutcome::Allow => {
let result = self.inner.execute_tool_call(call).await;
if !matches!(result, Err(ToolError::ConfirmationRequired { .. })) {
let summary = Self::context_summary_for_result(&result);
self.probe
.record(call.tool_id.as_str(), turn, &risk, &summary)
.await;
}
result
}
ProbeOutcome::Skip => self.inner.execute_tool_call(call).await,
ProbeOutcome::Deny { reason } => {
tracing::warn!(
tool_id = %call.tool_id,
reason = %reason,
"ShadowProbeExecutor: safety probe denied tool call"
);
self.probe
.record(
call.tool_id.as_str(),
turn,
&risk,
&format!("probe denied: {reason}"),
)
.await;
Err(ToolError::SafetyDenied { reason })
}
}
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
let turn = self.current_turn();
let risk = self.current_risk_level();
if let Some(reason) = self.quarantine_denial_reason(call) {
tracing::warn!(
tool_id = %call.tool_id,
reason = %reason,
"ShadowProbeExecutor: quarantine short-circuit denied confirmed tool call"
);
self.probe
.record(
call.tool_id.as_str(),
turn,
&risk,
&format!("quarantine short-circuit: {reason}"),
)
.await;
return Err(ToolError::SafetyDenied { reason });
}
let span = info_span!(
"security.shadow.probe_executor_confirmed",
tool_id = %call.tool_id
);
let args = serde_json::Value::Object(call.params.clone());
let outcome = self
.probe
.probe(call.tool_id.as_str(), &args, turn, &risk)
.instrument(span)
.await;
match outcome {
ProbeOutcome::Allow => {
let result = self.inner.execute_tool_call_confirmed(call).await;
if !matches!(result, Err(ToolError::ConfirmationRequired { .. })) {
let summary = Self::context_summary_for_result(&result);
self.probe
.record(call.tool_id.as_str(), turn, &risk, &summary)
.await;
}
result
}
ProbeOutcome::Skip => self.inner.execute_tool_call_confirmed(call).await,
ProbeOutcome::Deny { reason } => {
tracing::warn!(
tool_id = %call.tool_id,
reason = %reason,
"ShadowProbeExecutor: safety probe denied confirmed tool call"
);
self.probe
.record(
call.tool_id.as_str(),
turn,
&risk,
&format!("probe denied: {reason}"),
)
.await;
Err(ToolError::SafetyDenied { reason })
}
}
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
self.inner.set_skill_env(env);
}
fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
self.effective_trust
.store(level.severity(), std::sync::atomic::Ordering::Relaxed);
self.inner.set_effective_trust(level);
}
fn is_tool_retryable(&self, tool_id: &str) -> bool {
self.inner.is_tool_retryable(tool_id)
}
fn is_tool_speculatable(&self, tool_id: &str) -> bool {
let _ = tool_id;
false
}
fn requires_confirmation(&self, call: &ToolCall) -> bool {
self.inner.requires_confirmation(call)
}
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()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::executor::{ToolError, ToolOutput};
use crate::{ToolCall, ToolExecutor};
use zeph_common::ToolName;
struct AllowProbe;
impl ProbeGate for AllowProbe {
fn probe<'a>(
&'a self,
_: &'a str,
_: &'a serde_json::Value,
_: u64,
_: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>
{
Box::pin(async { ProbeOutcome::Allow })
}
}
struct DenyProbe;
impl ProbeGate for DenyProbe {
fn probe<'a>(
&'a self,
_: &'a str,
_: &'a serde_json::Value,
_: u64,
_: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>
{
Box::pin(async {
ProbeOutcome::Deny {
reason: "test denial".to_owned(),
}
})
}
}
struct SkipProbe;
impl ProbeGate for SkipProbe {
fn probe<'a>(
&'a self,
_: &'a str,
_: &'a serde_json::Value,
_: u64,
_: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>
{
Box::pin(async { ProbeOutcome::Skip })
}
}
struct PanicProbe;
impl ProbeGate for PanicProbe {
fn probe<'a>(
&'a self,
_: &'a str,
_: &'a serde_json::Value,
_: u64,
_: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>
{
panic!("probe() must not be invoked when the quarantine short-circuit applies")
}
}
struct RecordingProbe {
outcome: ProbeOutcome,
recorded: std::sync::Mutex<Vec<(String, u64, String, String)>>,
}
impl RecordingProbe {
fn new(outcome: ProbeOutcome) -> Self {
Self {
outcome,
recorded: std::sync::Mutex::new(Vec::new()),
}
}
}
impl ProbeGate for RecordingProbe {
fn probe<'a>(
&'a self,
_: &'a str,
_: &'a serde_json::Value,
_: u64,
_: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ProbeOutcome> + Send + 'a>>
{
let outcome = self.outcome.clone();
Box::pin(async move { outcome })
}
fn record<'a>(
&'a self,
qualified_tool_id: &'a str,
turn_number: u64,
risk_level: &'a str,
context_summary: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
self.recorded.lock().unwrap().push((
qualified_tool_id.to_owned(),
turn_number,
risk_level.to_owned(),
context_summary.to_owned(),
));
})
}
}
struct OkInner;
impl ToolExecutor for OkInner {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
Ok(Some(ToolOutput {
tool_name: call.tool_id.clone(),
summary: "ok".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
crate::tool_executor_no_inner_defaults!();
}
struct ConfirmationRequiredInner;
impl ToolExecutor for ConfirmationRequiredInner {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
Err(ToolError::ConfirmationRequired {
command: call.tool_id.to_string(),
})
}
crate::tool_executor_no_inner_defaults!();
}
fn make_call(tool: &str) -> ToolCall {
ToolCall {
tool_id: ToolName::new(tool),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
}
}
fn make_call_with_skills(tool: &str, skills: &[&str]) -> ToolCall {
ToolCall {
tool_id: ToolName::new(tool),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: Some(skills.iter().map(ToString::to_string).collect()),
}
}
fn make_executor<P: ProbeGate + 'static>(probe: P) -> ShadowProbeExecutor<OkInner> {
ShadowProbeExecutor::new(
OkInner,
Arc::new(probe),
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
)
}
#[tokio::test]
async fn allow_probe_delegates_to_inner() {
let exec = make_executor(AllowProbe);
let result = exec.execute_tool_call(&make_call("builtin:shell")).await;
assert!(result.unwrap().is_some());
}
#[tokio::test]
async fn deny_probe_returns_safety_denied() {
let exec = make_executor(DenyProbe);
let result = exec.execute_tool_call(&make_call("builtin:shell")).await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert_eq!(reason, "test denial");
}
other => panic!("expected SafetyDenied, got {other:?}"),
}
}
#[tokio::test]
async fn skip_probe_delegates_to_inner() {
let exec = make_executor(SkipProbe);
let result = exec.execute_tool_call(&make_call("builtin:read")).await;
assert!(result.unwrap().is_some());
}
#[tokio::test]
async fn legacy_execute_bypasses_probe() {
let exec = make_executor(DenyProbe);
let result = exec.execute("some text").await;
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn deny_probe_blocks_confirmed_call() {
let exec = make_executor(DenyProbe);
let result = exec
.execute_tool_call_confirmed(&make_call("builtin:shell"))
.await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert_eq!(reason, "test denial");
}
other => panic!("expected SafetyDenied on confirmed call, got {other:?}"),
}
}
#[tokio::test]
async fn quarantined_short_circuits_before_probe_runs() {
let exec = make_executor(PanicProbe);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let call = make_call_with_skills("bash", &["disk-usage"]);
let result = exec.execute_tool_call(&call).await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert!(
reason.contains("disk-usage"),
"expected quarantine_denial_message naming active skills, got: {reason}"
);
}
other => panic!("expected SafetyDenied, got {other:?}"),
}
}
#[tokio::test]
async fn quarantined_short_circuits_confirmed_path() {
let exec = make_executor(PanicProbe);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let call = make_call_with_skills("bash", &["disk-usage"]);
let result = exec.execute_tool_call_confirmed(&call).await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert!(reason.contains("disk-usage"));
}
other => panic!("expected SafetyDenied on confirmed call, got {other:?}"),
}
}
#[tokio::test]
async fn quarantined_non_denied_tool_still_runs_probe() {
let exec = make_executor(AllowProbe);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let result = exec.execute_tool_call(&make_call("read")).await;
assert!(result.unwrap().is_some());
}
#[tokio::test]
async fn non_quarantined_trust_still_runs_probe_for_denied_tool_name() {
let exec = make_executor(DenyProbe);
exec.set_effective_trust(SkillTrustLevel::Trusted);
let result = exec.execute_tool_call(&make_call("bash")).await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert_eq!(
reason, "test denial",
"probe must still run at Trusted level"
);
}
other => panic!("expected SafetyDenied from probe, got {other:?}"),
}
}
#[tokio::test]
async fn quarantined_non_denied_tool_still_runs_probe_confirmed_path() {
let exec = make_executor(AllowProbe);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let result = exec.execute_tool_call_confirmed(&make_call("read")).await;
assert!(result.unwrap().is_some());
}
#[tokio::test]
async fn non_quarantined_trust_still_runs_probe_for_denied_tool_name_confirmed_path() {
let exec = make_executor(DenyProbe);
exec.set_effective_trust(SkillTrustLevel::Trusted);
let result = exec.execute_tool_call_confirmed(&make_call("bash")).await;
match result {
Err(ToolError::SafetyDenied { reason }) => {
assert_eq!(
reason, "test denial",
"probe must still run at Trusted level"
);
}
other => panic!("expected SafetyDenied from probe, got {other:?}"),
}
}
#[tokio::test]
async fn quarantine_short_circuit_still_records_event() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Allow));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(7)),
Arc::new(parking_lot::RwLock::new("elevated".to_owned())),
);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let call = make_call_with_skills("bash", &["disk-usage"]);
let result = exec.execute_tool_call(&call).await;
assert!(matches!(result, Err(ToolError::SafetyDenied { .. })));
let recorded = probe.recorded.lock().unwrap();
assert_eq!(
recorded.len(),
1,
"quarantine short-circuit must record exactly one event"
);
let (tool_id, turn, risk, summary) = &recorded[0];
assert_eq!(tool_id, "bash");
assert_eq!(*turn, 7);
assert_eq!(risk, "elevated");
assert!(summary.starts_with("quarantine short-circuit:"));
assert!(summary.contains("disk-usage"));
}
#[tokio::test]
async fn quarantine_short_circuit_confirmed_path_still_records_event() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Allow));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
exec.set_effective_trust(SkillTrustLevel::Quarantined);
let call = make_call_with_skills("bash", &["disk-usage"]);
let result = exec.execute_tool_call_confirmed(&call).await;
assert!(matches!(result, Err(ToolError::SafetyDenied { .. })));
assert_eq!(probe.recorded.lock().unwrap().len(), 1);
}
struct CheckpointingInner;
impl ToolExecutor for CheckpointingInner {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
crate::executor::CheckpointActionResult {
supported: true,
message: "stub".into(),
reverted_commands: n,
..Default::default()
}
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
crate::executor::CheckpointActionResult {
supported: true,
message: "stub".into(),
..Default::default()
}
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
crate::executor::CheckpointListResult {
supported: true,
..Default::default()
}
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
self.execute_tool_call(call).await
}
fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
false
}
fn requires_confirmation(&self, _call: &ToolCall) -> bool {
false
}
}
#[test]
fn checkpoint_methods_delegated_to_inner() {
let exec = ShadowProbeExecutor::new(
CheckpointingInner,
Arc::new(AllowProbe),
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
let undo_result = exec.checkpoint_undo(7);
assert!(undo_result.supported);
assert_eq!(
undo_result.reverted_commands, 7,
"n must be forwarded, not hardcoded"
);
assert!(exec.checkpoint_redo().supported);
assert!(exec.checkpoint_list().supported);
}
#[test]
fn is_tool_speculatable_always_false() {
let exec = make_executor(AllowProbe);
assert!(!exec.is_tool_speculatable("builtin:read"));
assert!(!exec.is_tool_speculatable("builtin:shell"));
}
#[tokio::test]
async fn allow_outcome_records_after_execution() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Allow));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(3)),
Arc::new(parking_lot::RwLock::new("elevated".to_owned())),
);
let result = exec.execute_tool_call(&make_call("builtin:shell")).await;
assert!(result.unwrap().is_some());
let recorded = probe.recorded.lock().unwrap();
assert_eq!(
recorded.len(),
1,
"Allow outcome must record exactly one event"
);
let (tool_id, turn, risk, summary) = &recorded[0];
assert_eq!(tool_id, "builtin:shell");
assert_eq!(*turn, 3);
assert_eq!(risk, "elevated");
assert_eq!(summary, "ok");
}
#[tokio::test]
async fn allow_outcome_does_not_record_on_confirmation_required() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Allow));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
ConfirmationRequiredInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
let result = exec.execute_tool_call(&make_call("builtin:shell")).await;
assert!(matches!(
result,
Err(ToolError::ConfirmationRequired { .. })
));
assert!(
probe.recorded.lock().unwrap().is_empty(),
"ConfirmationRequired must not be recorded — the confirmed re-run records instead"
);
}
#[tokio::test]
async fn deny_outcome_records_denial_reason() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Deny {
reason: "risky pattern".to_owned(),
}));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
let result = exec.execute_tool_call(&make_call("builtin:shell")).await;
assert!(result.is_err(), "Deny outcome must still return an error");
let recorded = probe.recorded.lock().unwrap();
assert_eq!(
recorded.len(),
1,
"Deny outcome must be recorded even though the tool never executed"
);
assert!(recorded[0].3.contains("risky pattern"));
}
#[tokio::test]
async fn skip_outcome_does_not_record() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Skip));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
let _ = exec.execute_tool_call(&make_call("builtin:read")).await;
assert!(
probe.recorded.lock().unwrap().is_empty(),
"Skip outcome must never record — it covers both disabled-feature and \
low-risk-tool cases and would flood the store with noise"
);
}
#[tokio::test]
async fn allow_outcome_records_on_confirmed_path_too() {
let probe = Arc::new(RecordingProbe::new(ProbeOutcome::Allow));
let gate: Arc<dyn ProbeGate> = probe.clone();
let exec = ShadowProbeExecutor::new(
OkInner,
gate,
Arc::new(std::sync::atomic::AtomicU64::new(1)),
Arc::new(parking_lot::RwLock::new("calm".to_owned())),
);
let _ = exec
.execute_tool_call_confirmed(&make_call("builtin:shell"))
.await;
assert_eq!(
probe.recorded.lock().unwrap().len(),
1,
"confirmed path must also record on Allow"
);
}
}