use super::r#trait::ToolHints;
use crate::utils::plan_files::{self, PlanModeState};
use serde_json::Value;
use std::path::{Path, PathBuf};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum GateDecision {
Allow,
Deny(String),
RequireApproval(String),
}
#[allow(dead_code)]
impl GateDecision {
pub(crate) fn is_allowed(&self) -> bool {
matches!(self, GateDecision::Allow)
}
pub(crate) fn is_denied(&self) -> bool {
matches!(self, GateDecision::Deny(_))
}
pub(crate) fn needs_approval(&self) -> bool {
matches!(self, GateDecision::RequireApproval(_))
}
}
pub(crate) async fn check_plan_gate(
session_id: Uuid,
tool_name: &str,
hints: &ToolHints,
input: &Value,
) -> GateDecision {
let state = plan_files::plan_mode_state(session_id).await;
match state {
PlanModeState::NoPlan => GateDecision::Allow,
PlanModeState::Active => {
if crate::utils::plan_mode::in_seed_window(session_id).await {
if hints.read_only {
return GateDecision::Allow;
}
return GateDecision::Deny(format!(
"Plan gate: '{tool_name}' is blocked until the approved plan's \
checklist is seeded. Call `plan` add_tasks with the steps from \
the session .md, then `plan` start; project work begins after \
start succeeds."
));
}
if !hints.read_only && write_targets_session_md(session_id, input).await {
GateDecision::Deny(
"Plan gate: the session plan .md is frozen while the checklist is \
Active. Execute tasks with the plan tool (start/complete); the \
design document is no longer editable."
.to_string(),
)
} else {
GateDecision::Allow
}
}
PlanModeState::PreInitEditing => {
if hints.read_only {
return GateDecision::Allow;
}
GateDecision::RequireApproval(format!(
"Plan gate: '{tool_name}' has side effects. The session is in Plan \
mode (pre-init Editing). Approve to allow this action during design."
))
}
PlanModeState::PostInitEditing => {
if hints.read_only {
return GateDecision::Allow;
}
if write_targets_session_md(session_id, input).await {
return GateDecision::Allow;
}
GateDecision::RequireApproval(format!(
"Plan gate: '{tool_name}' has side effects and requires approval \
while the plan is being designed (Editing). Refine the session \
plan .md; project work begins after the user approves the plan."
))
}
}
}
pub(crate) async fn write_targets_session_md(session_id: Uuid, input: &Value) -> bool {
let Some(raw) = input
.get("path")
.or_else(|| input.get("file_path"))
.and_then(|v| v.as_str())
else {
return false;
};
let md = plan_files::plan_md_path(session_id).await;
let candidate = PathBuf::from(raw);
if candidate.is_absolute() {
paths_match(&candidate, &md)
} else {
paths_match(&crate::config::opencrabs_home().join(&candidate), &md)
}
}
fn paths_match(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(ca), Ok(cb)) => ca == cb,
_ => a == b,
}
}
pub(crate) fn restrict_registry_to_read_only(registry: &super::ToolRegistry) {
for name in registry.list_tools() {
let dominated = registry.get(&name).is_some_and(|t| !t.hints().read_only);
if dominated {
registry.unregister(&name);
}
}
}