use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Constraint {
pub id: String,
pub description: String,
pub enabled: bool,
pub filter_type: FilterType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FilterType {
Allow,
Deny,
RequestApproval,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintMatrix {
pub identity: String,
pub room: String,
pub constraints: Vec<Constraint>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConstraintResult {
Allow,
Deny(ConstraintViolation),
RequestApproval(ApprovalRequest),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstraintViolation {
pub constraint: String,
pub attempted_action: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalRequest {
pub constraint: String,
pub attempted_action: String,
pub approvers: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Command {
pub verb: String,
pub target: String,
pub args: Vec<String>,
}
impl Command {
pub fn new(verb: &str, target: &str, args: Vec<&str>) -> Self {
Self {
verb: verb.to_string(),
target: target.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
}
}
pub fn from_string(input: &str) -> Self {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.is_empty() {
return Self {
verb: String::new(),
target: String::new(),
args: vec![],
};
}
let verb = parts[0].to_string();
let target = parts.get(1).map(|s| s.to_string()).unwrap_or_default();
let args = parts[2..].iter().map(|s| s.to_string()).collect();
Self { verb, target, args }
}
}
pub struct ConstraintEngine {
matrices: HashMap<(String, String), ConstraintMatrix>, }
impl ConstraintEngine {
pub fn new() -> Self {
Self {
matrices: HashMap::new(),
}
}
pub fn load_constraints(&self, room_name: &str, identity: &str) -> Result<ConstraintMatrix, anyhow::Error> {
Ok(ConstraintMatrix {
identity: identity.to_string(),
room: room_name.to_string(),
constraints: vec![
Constraint {
id: "view_room".to_string(),
description: "Can view room description".to_string(),
enabled: true,
filter_type: FilterType::Allow,
},
Constraint {
id: "send_tell".to_string(),
description: "Can send tells to other entities".to_string(),
enabled: true,
filter_type: FilterType::Allow,
},
Constraint {
id: "admin_commands".to_string(),
description: "Can execute admin commands".to_string(),
enabled: false,
filter_type: FilterType::Deny,
},
],
})
}
pub fn check(&self, matrix: &ConstraintMatrix, command: &Command) -> ConstraintResult {
for constraint in &matrix.constraints {
if !constraint.enabled {
continue;
}
let matches = match constraint.id.as_str() {
"view_room" => command.verb == "look" || command.verb == "examine",
"send_tell" => command.verb == "tell" || command.verb == "page",
"admin_commands" => command.verb.starts_with("@") || command.verb == "delete" || command.verb == "create",
_ => false,
};
if matches {
match constraint.filter_type {
FilterType::Allow => return ConstraintResult::Allow,
FilterType::Deny => {
return ConstraintResult::Deny(ConstraintViolation {
constraint: constraint.id.clone(),
attempted_action: format!("{} {}", command.verb, command.target),
reason: constraint.description.clone(),
});
}
FilterType::RequestApproval => {
return ConstraintResult::RequestApproval(ApprovalRequest {
constraint: constraint.id.clone(),
attempted_action: format!("{} {}", command.verb, command.target),
approvers: vec!["@admin".to_string()],
});
}
}
}
}
ConstraintResult::Allow
}
pub fn add_constraint(&mut self, mut matrix: ConstraintMatrix, constraint: Constraint) -> ConstraintMatrix {
matrix.constraints.push(constraint);
matrix
}
}
impl Default for ConstraintEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssertiveConstraint {
pub id: String,
pub source_text: String,
pub kind: AssertionKind,
pub subject: Option<String>,
pub predicate: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AssertionKind {
Must,
Should,
MustNot,
}
impl AssertiveConstraint {
fn parse_kind(text: &str) -> AssertionKind {
let lower = text.to_lowercase();
if lower.contains("must not") || lower.contains("cannot") || lower.contains("never") {
AssertionKind::MustNot
} else if lower.contains("must") || lower.contains("shall") || lower.contains("always") {
AssertionKind::Must
} else {
AssertionKind::Should
}
}
}
pub fn parse_markdown_constraints(content: &str) -> Vec<AssertiveConstraint> {
let modal_verbs = ["must", "shall", "cannot", "must not", "never", "always", "should"];
content
.lines()
.enumerate()
.filter_map(|(i, line)| {
let trimmed = line.trim();
let text = if let Some(rest) = trimmed.strip_prefix("- ") {
rest
} else if let Some(rest) = trimmed.strip_prefix("* ") {
rest
} else {
return None;
};
let lower = text.to_lowercase();
if !modal_verbs.iter().any(|v| lower.contains(v)) {
return None;
}
let kind = AssertiveConstraint::parse_kind(text);
Some(AssertiveConstraint {
id: format!("md-assert-{}", i),
source_text: text.trim_end_matches('.').to_string(),
kind,
subject: None,
predicate: text.to_string(),
})
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuditOutcome {
Pass,
RetryRequired(Vec<String>),
Warned(Vec<String>),
}
pub struct ConstraintAuditor {
constraints: Vec<AssertiveConstraint>,
}
impl ConstraintAuditor {
pub fn new(constraints: Vec<AssertiveConstraint>) -> Self {
Self { constraints }
}
pub fn from_markdown(content: &str) -> Self {
Self::new(parse_markdown_constraints(content))
}
pub fn audit(&self, agent_output: &str) -> AuditOutcome {
let mut hard_failures: Vec<String> = Vec::new();
let mut soft_warnings: Vec<String> = Vec::new();
for c in &self.constraints {
let violated = self.check_violated(c, agent_output);
if violated {
match c.kind {
AssertionKind::Must | AssertionKind::MustNot => {
hard_failures.push(c.source_text.clone());
}
AssertionKind::Should => {
soft_warnings.push(c.source_text.clone());
}
}
}
}
if !hard_failures.is_empty() {
AuditOutcome::RetryRequired(hard_failures)
} else if !soft_warnings.is_empty() {
AuditOutcome::Warned(soft_warnings)
} else {
AuditOutcome::Pass
}
}
fn check_violated(&self, constraint: &AssertiveConstraint, output: &str) -> bool {
let lower_pred = constraint.predicate.to_lowercase();
let lower_out = output.to_lowercase();
match constraint.kind {
AssertionKind::MustNot => {
for marker in ["must not ", "cannot ", "never "] {
if let Some(pos) = lower_pred.find(marker) {
let prohibited: String = lower_pred[pos + marker.len()..]
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join(" ");
if lower_out.contains(&prohibited) {
return true;
}
}
}
false
}
AssertionKind::Must | AssertionKind::Should => {
for marker in ["must be ", "must ", "shall ", "should ", "always "] {
if let Some(pos) = lower_pred.find(marker) {
let required: String = lower_pred[pos + marker.len()..]
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.join(" ");
let violated = !required.is_empty() && !lower_out.contains(&required);
return violated; }
}
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_constraint_check() {
let engine = ConstraintEngine::new();
let matrix = ConstraintMatrix {
identity: "@test".to_string(),
room: "test-room".to_string(),
constraints: vec![
Constraint {
id: "send_tell".to_string(),
description: "Can send tells".to_string(),
enabled: true,
filter_type: FilterType::Allow,
},
],
};
let cmd = Command::new("tell", "@other", vec!["Hello"]);
let result = engine.check(&matrix, &cmd);
assert_eq!(result, ConstraintResult::Allow);
}
#[test]
fn test_parse_markdown_constraints() {
let md = "## Rules\n- The user's name must be capitalized.\n- Output cannot contain profanity.\n- Links should be https.\n";
let constraints = parse_markdown_constraints(md);
assert_eq!(constraints.len(), 3);
assert_eq!(constraints[0].kind, AssertionKind::Must);
assert_eq!(constraints[1].kind, AssertionKind::MustNot);
assert_eq!(constraints[2].kind, AssertionKind::Should);
}
#[test]
fn test_auditor_retry_on_hard_failure() {
let md = "- Output must be capitalized.\n";
let auditor = ConstraintAuditor::from_markdown(md);
let result = auditor.audit("hello world");
assert_eq!(result, AuditOutcome::RetryRequired(vec!["Output must be capitalized".to_string()]));
}
#[test]
fn test_auditor_pass() {
let md = "- Output must be capitalized.\n";
let auditor = ConstraintAuditor::from_markdown(md);
let result = auditor.audit("Hello world — capitalized.");
assert_eq!(result, AuditOutcome::Pass);
}
}