use super::{
AgentMission, AtelierAction, GuardCapability, GuardDecision, GuardRefusal, guard_action,
};
use sim_kernel::{Expr, Result, Symbol};
use sim_lib_stream_core::{DevCassette, DevEvent, LatencyClass};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AtelierToolDescriptor {
pub id: String,
pub title: String,
pub command: String,
pub required_capability: GuardCapability,
pub evidence_kind: String,
pub repo: Option<String>,
}
impl AtelierToolDescriptor {
fn new(
id: impl Into<String>,
title: impl Into<String>,
command: impl Into<String>,
required_capability: GuardCapability,
evidence_kind: impl Into<String>,
repo: Option<String>,
) -> Self {
Self {
id: id.into(),
title: title.into(),
command: command.into(),
required_capability,
evidence_kind: evidence_kind.into(),
repo,
}
}
pub fn as_expr(&self) -> Expr {
let mut entries = vec![
key("id", Expr::String(self.id.clone())),
key("title", Expr::String(self.title.clone())),
key("command", Expr::String(self.command.clone())),
key(
"capability",
Expr::String(capability_label(&self.required_capability)),
),
key("evidence-kind", Expr::String(self.evidence_kind.clone())),
];
if let Some(repo) = &self.repo {
entries.push(key("repo", Expr::String(repo.clone())));
}
Expr::Map(entries)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PinUpdateRequest {
pub repo: String,
pub current_commit: String,
pub new_commit: String,
pub pushed_commit_exists: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DocsOperation {
Regenerate,
HandEdit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DocsRegenerationRequest {
pub repo: String,
pub path: String,
pub docs_command: String,
pub generated_public_doc: bool,
pub operation: DocsOperation,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolRunEvidence {
pub descriptor: AtelierToolDescriptor,
pub exit_status: i32,
pub log_path: String,
}
impl ToolRunEvidence {
pub fn new(
descriptor: AtelierToolDescriptor,
exit_status: i32,
log_path: impl Into<String>,
) -> Self {
Self {
descriptor,
exit_status,
log_path: log_path.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AtelierToolAction {
Simctl(AtelierToolDescriptor),
Validation(ToolRunEvidence),
Docs(ToolRunEvidence),
PinUpdate(PinUpdateRequest),
DocsRegeneration(DocsRegenerationRequest),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AtelierToolEvaluation {
pub descriptor: AtelierToolDescriptor,
pub decision: GuardDecision,
pub cassette: DevCassette,
}
pub fn simctl_tool_descriptors(
control_repo: impl Into<String>,
docs_repo: impl Into<String>,
) -> Vec<AtelierToolDescriptor> {
let control_repo = control_repo.into();
let docs_repo = docs_repo.into();
[
("clone", "Clone or update sibling repos"),
("meta-build", "Regenerate the validation meta workspace"),
("audit", "Run the private-data audit"),
("no-github-check", "Assert no GitHub work is enabled"),
("site", "Regenerate the public front page"),
("repos", "List the constellation manifest"),
("atelier-site", "Emit the Atelier Site graph"),
("atelier-index", "Refresh the Atelier index"),
("atelier-radar", "Query ranked Atelier hints"),
("atelier-guard", "Run the Guideline Firewall"),
]
.into_iter()
.map(|(command, title)| {
let capability = if command == "site" {
GuardCapability::RegenDocs(docs_repo.clone())
} else {
GuardCapability::RunValidation(control_repo.clone())
};
AtelierToolDescriptor::new(
format!("simctl/{command}"),
title,
format!("sh bin/simctl {command}"),
capability,
"control",
Some(control_repo.clone()),
)
})
.collect()
}
pub fn repo_validation_descriptor(
repo: impl Into<String>,
validation_command: impl Into<String>,
) -> AtelierToolDescriptor {
let repo = repo.into();
AtelierToolDescriptor::new(
format!("validation/{repo}"),
format!("Validate {repo}"),
validation_command,
GuardCapability::RunValidation(repo.clone()),
"validate",
Some(repo),
)
}
pub fn repo_docs_descriptor(
repo: impl Into<String>,
docs_command: impl Into<String>,
) -> AtelierToolDescriptor {
let repo = repo.into();
AtelierToolDescriptor::new(
format!("docs/{repo}"),
format!("Regenerate docs for {repo}"),
docs_command,
GuardCapability::RegenDocs(repo.clone()),
"docs",
Some(repo),
)
}
pub fn evaluate_atelier_tool(
mission: &AgentMission,
action: AtelierToolAction,
atelier_node: Symbol,
stream_id: Symbol,
) -> Result<AtelierToolEvaluation> {
let descriptor = action.descriptor();
let guard = action.guard_action();
let decision = match guard_action(mission, guard.clone()) {
GuardDecision::Granted => action.post_guard_decision(guard),
refused @ GuardDecision::Refused(_) => refused,
};
let event = match &decision {
GuardDecision::Granted => DevEvent::new(
&descriptor.evidence_kind,
atelier_node,
LatencyClass::OfflineRender,
action.payload_expr(&descriptor),
)?,
GuardDecision::Refused(refusal) => refusal.dev_event(atelier_node)?,
};
Ok(AtelierToolEvaluation {
descriptor,
decision,
cassette: DevCassette::from_events(stream_id, vec![event])?,
})
}
impl AtelierToolAction {
fn descriptor(&self) -> AtelierToolDescriptor {
match self {
Self::Simctl(descriptor) => descriptor.clone(),
Self::Validation(evidence) | Self::Docs(evidence) => evidence.descriptor.clone(),
Self::PinUpdate(request) => AtelierToolDescriptor::new(
format!("pin/{}", request.repo),
format!("Update {} pin", request.repo),
format!(
"set repos.toml {} commit {}",
request.repo, request.new_commit
),
GuardCapability::PlanPin,
"pin",
Some(request.repo.clone()),
),
Self::DocsRegeneration(request) => {
repo_docs_descriptor(request.repo.clone(), request.docs_command.clone())
}
}
}
fn guard_action(&self) -> AtelierAction {
match self {
Self::Simctl(descriptor) => guard_action_for_descriptor(descriptor),
Self::Validation(evidence) | Self::Docs(evidence) => {
guard_action_for_descriptor(&evidence.descriptor)
}
Self::PinUpdate(request) => AtelierAction::PlanPin {
repo: request.repo.clone(),
},
Self::DocsRegeneration(request) => AtelierAction::RegenDocs {
repo: request.repo.clone(),
},
}
}
fn post_guard_decision(&self, guard: AtelierAction) -> GuardDecision {
match self {
Self::PinUpdate(request) if !request.pushed_commit_exists => refused(
guard,
"pin update requires an existing pushed upstream commit",
),
Self::DocsRegeneration(request)
if request.generated_public_doc && request.operation == DocsOperation::HandEdit =>
{
refused(
guard,
"generated public docs must be regenerated, not hand-edited",
)
}
_ => GuardDecision::Granted,
}
}
fn payload_expr(&self, descriptor: &AtelierToolDescriptor) -> Expr {
let mut entries = vec![
key("tool", Expr::String(descriptor.id.clone())),
key("command", Expr::String(descriptor.command.clone())),
key(
"capability",
Expr::String(capability_label(&descriptor.required_capability)),
),
];
match self {
Self::Validation(evidence) | Self::Docs(evidence) => {
entries.push(key(
"exit-status",
Expr::String(evidence.exit_status.to_string()),
));
entries.push(key("log-path", Expr::String(evidence.log_path.clone())));
}
Self::PinUpdate(request) => {
entries.push(key(
"current-commit",
Expr::String(request.current_commit.clone()),
));
entries.push(key("new-commit", Expr::String(request.new_commit.clone())));
}
Self::DocsRegeneration(request) => {
entries.push(key("path", Expr::String(request.path.clone())));
entries.push(key(
"generated-public-doc",
Expr::Bool(request.generated_public_doc),
));
}
Self::Simctl(_) => {}
}
Expr::Map(entries)
}
}
fn guard_action_for_descriptor(descriptor: &AtelierToolDescriptor) -> AtelierAction {
match &descriptor.required_capability {
GuardCapability::RegenDocs(repo) => AtelierAction::RegenDocs { repo: repo.clone() },
GuardCapability::RunValidation(repo) => AtelierAction::RunValidation { repo: repo.clone() },
GuardCapability::PlanPin => AtelierAction::PlanPin {
repo: descriptor.repo.clone().unwrap_or_default(),
},
GuardCapability::EvalGated => AtelierAction::Eval {
label: descriptor.id.clone(),
},
GuardCapability::EditRepo(repo) => AtelierAction::EditFile {
repo: repo.clone(),
path: descriptor.command.clone(),
},
}
}
fn refused(action: AtelierAction, reason: impl Into<String>) -> GuardDecision {
GuardDecision::Refused(GuardRefusal::new(action, reason))
}
fn capability_label(capability: &GuardCapability) -> String {
match capability {
GuardCapability::EditRepo(repo) => format!("EditRepo({repo})"),
GuardCapability::RegenDocs(repo) => format!("RegenDocs({repo})"),
GuardCapability::PlanPin => "PlanPin".to_owned(),
GuardCapability::RunValidation(repo) => format!("RunValidation({repo})"),
GuardCapability::EvalGated => "EvalGated".to_owned(),
}
}
fn key(name: &str, value: Expr) -> (Expr, Expr) {
(Expr::Symbol(Symbol::new(name)), value)
}