use crate::tool::{ApprovalLevel, ToolArgs, ToolCtx};
pub enum ApprovalOutcome {
Approve,
Deny { reason: String },
}
pub fn level_str(level: ApprovalLevel) -> &'static str {
match level {
ApprovalLevel::Auto => "auto",
ApprovalLevel::Approve => "approve",
ApprovalLevel::Dangerous => "dangerous",
}
}
fn is_outside_workspace(_ctx: &ToolCtx, tool_name: &str, args: &ToolArgs) -> bool {
if !matches!(tool_name, "fs.write" | "fs.edit" | "fs.grep") {
return false;
}
let path = match args.named("path").or_else(|| args.positional(0).ok()) {
Some(crate::value::Value::Path(p)) => p.clone(),
Some(crate::value::Value::Str(s)) => std::path::PathBuf::from(s),
_ => return false,
};
let abs = if path.is_absolute() {
path
} else {
match std::env::current_dir() {
Ok(cwd) => cwd.join(&path),
Err(_) => return true,
}
};
match std::env::current_dir() {
Ok(cwd) => !abs.starts_with(&cwd),
Err(_) => true,
}
}
pub async fn request_approval(
ctx: &ToolCtx,
id: &str,
name: &str,
call_args: &ToolArgs,
level: ApprovalLevel,
tool: Option<&dyn crate::tool::Tool>,
) -> ApprovalOutcome {
use crate::trust::{OutsideBehavior, TrustMode};
let outside_workspace = is_outside_workspace(ctx, name, call_args);
if outside_workspace {
if let Some(trust) = &ctx.trust {
if trust.mode != TrustMode::Reckless {
match trust.outside {
OutsideBehavior::Deny => {
return ApprovalOutcome::Deny {
reason: format!(
"{name}: blocked — path outside workspace and outside=deny"
),
};
}
OutsideBehavior::Allow => {}
OutsideBehavior::Approve => {}
}
}
}
}
let Some(approval) = &ctx.approval else {
return ApprovalOutcome::Approve;
};
let Some(run_id) = ctx.flow_run_id.clone() else {
return ApprovalOutcome::Approve;
};
let force_manual = outside_workspace
&& ctx
.trust
.as_ref()
.map(|t| t.mode != TrustMode::Reckless && t.outside == OutsideBehavior::Approve)
.unwrap_or(true);
let effective_level = if force_manual {
ApprovalLevel::Dangerous
} else {
level
};
let args_preview: String = format!("{:?}", call_args.named)
.chars()
.take(4000)
.collect();
let preview = if level == ApprovalLevel::Auto {
None
} else {
match tool {
Some(t) => t.preview_call(call_args, ctx).await,
None => None,
}
};
let pending = crate::session::PendingApproval {
tool_use_id: id.to_string(),
tool_name: name.to_string(),
args_preview: args_preview.clone(),
preview: preview.clone(),
level: effective_level,
run_id: run_id.clone(),
emitted_at: chrono::Utc::now(),
bypass_auto_ceiling: force_manual,
};
let rx = approval.request(pending);
if let Some(sink) = ctx.events.as_ref() {
sink.emit(crate::event::Event::ToolPendingApproval {
run_id: run_id.clone(),
tool_use_id: id.to_string(),
tool_name: name.to_string(),
args_preview: args_preview.clone(),
level: level_str(level).into(),
preview: preview.clone(),
});
}
if let Some(tx) = &ctx.stream_tx {
let _ = tx.send(crate::stream::StreamFrame::ToolPendingApproval {
run_id: run_id.0.to_string(),
tool_use_id: id.to_string(),
tool_name: name.to_string(),
args_preview,
level: level_str(level).into(),
preview: preview.clone(),
});
}
let decision = rx.await.unwrap_or(crate::session::ApprovalDecision::Deny {
reason: "approval channel dropped".into(),
});
match decision {
crate::session::ApprovalDecision::Approve => {
if let Some(sink) = ctx.events.as_ref() {
sink.emit(crate::event::Event::ToolApproved {
run_id: run_id.clone(),
tool_use_id: id.to_string(),
decided_by: "user".into(),
});
}
if let Some(tx) = &ctx.stream_tx {
let _ = tx.send(crate::stream::StreamFrame::ToolApproved {
run_id: run_id.0.to_string(),
tool_use_id: id.to_string(),
decided_by: "user".into(),
});
}
ApprovalOutcome::Approve
}
crate::session::ApprovalDecision::Deny { reason } => {
if let Some(sink) = ctx.events.as_ref() {
sink.emit(crate::event::Event::ToolDenied {
run_id: run_id.clone(),
tool_use_id: id.to_string(),
reason: reason.clone(),
});
}
if let Some(tx) = &ctx.stream_tx {
let _ = tx.send(crate::stream::StreamFrame::ToolDenied {
run_id: run_id.0.to_string(),
tool_use_id: id.to_string(),
reason: reason.clone(),
});
}
ApprovalOutcome::Deny { reason }
}
}
}