use std::sync::Arc;
use tracing::{error, info, instrument};
use candor_core::error::CoreError;
use candor_core::protocol::AgentAction;
use candor_core::state::AgentState;
use candor_graph::hooks::BeforeToolCallback;
use super::rules::{enforce_deterministic_rules, check_conventional_commit};
use super::slop_detector;
pub struct SentinelInterceptor {
local_classifier: Arc<candor_cognitive::CognitiveEngine>,
valid_scopes: Vec<String>,
active: bool,
}
impl SentinelInterceptor {
pub fn new(
classifier: Arc<candor_cognitive::CognitiveEngine>,
valid_scopes: Vec<String>,
) -> Self {
Self {
local_classifier: classifier,
valid_scopes,
active: true,
}
}
pub fn permissive(classifier: Arc<candor_cognitive::CognitiveEngine>) -> Self {
Self {
local_classifier: classifier,
valid_scopes: vec![],
active: true,
}
}
pub fn deactivate(&mut self) {
self.active = false;
info!("Sentinel deactivated");
}
pub fn activate(&mut self) {
self.active = true;
info!("Sentinel activated");
}
pub fn set_scopes(&mut self, scopes: Vec<String>) {
self.valid_scopes = scopes;
}
fn enforce_deterministic_rules_sync(
&self,
payload: &str,
) -> Result<(), CoreError> {
let check = enforce_deterministic_rules(payload, &self.valid_scopes);
if !check.passed {
let messages: Vec<String> = check
.violations
.iter()
.map(|v| v.description.clone())
.collect();
error!(
violations = ?messages,
"Sentinel deterministic rules violated"
);
return Err(CoreError::SentinelPolicyViolation(messages.join("; ")));
}
Ok(())
}
#[instrument(skip(self))]
pub async fn evaluate_payload(
&self,
code_payload: String,
) -> Result<(), CoreError> {
if !self.active {
info!("Sentinel inactive — skipping audit");
return Ok(());
}
info!("Sentinel initiating hybrid audit on proposed payload");
self.enforce_deterministic_rules_sync(&code_payload)?;
let cognitive = Arc::clone(&self.local_classifier);
let payload = code_payload.clone();
let evaluation = tokio::task::spawn(async move {
slop_detector::evaluate_for_slop(&cognitive, &payload).await
})
.await
.map_err(|_| {
CoreError::SentinelSemanticRejection("Tokio task panicked during audit".into())
})?;
match evaluation {
Ok(true) => {
info!("Sentinel audit passed. Resuming graph execution.");
Ok(())
}
Ok(false) => {
error!("Sentinel detected AI slop or hallucination. Graph execution halted.");
Err(CoreError::SentinelSemanticRejection(
"Payload failed semantic no-slop verification.".into(),
))
}
Err(e) => {
error!(error = %e, "Sentinel evaluation error");
Err(e)
}
}
}
pub async fn evaluate_action(
&self,
action: &AgentAction,
) -> Result<(), CoreError> {
if matches!(
action.action_type,
candor_core::protocol::ActionType::GitCommit
) {
let commit_check = check_conventional_commit(&action.payload);
if !commit_check.passed {
return Err(CoreError::SentinelPolicyViolation(
commit_check.violations[0].description.clone(),
));
}
}
self.evaluate_payload(action.payload.clone()).await
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn clone_box(&self) -> Box<dyn BeforeToolCallback> {
Box::new(Self {
local_classifier: Arc::clone(&self.local_classifier),
valid_scopes: self.valid_scopes.clone(),
active: self.active,
})
}
}
#[async_trait::async_trait]
impl BeforeToolCallback for SentinelInterceptor {
async fn before_tool(
&self,
action: &AgentAction,
_state: Arc<tokio::sync::Mutex<AgentState>>,
) -> Result<(), CoreError> {
self.evaluate_action(action).await
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_sentinel_structural() {
assert!(true); }
}