Scaffold dd0c/run: Rust agent (classifier, executor, audit) + TypeScript SaaS

- Rust agent: clap CLI, command classifier (read-only/modifying/destructive), executor with approval gates, audit log entries
- Classifier: pattern-based safety classification for shell, AWS, kubectl, terraform/tofu commands
- 6 Rust tests: read-only, destructive, modifying, empty, terraform apply, tofu destroy
- SaaS backend: Fastify server, runbook CRUD API, approval API, Slack interactive handler
- Slack integration: signature verification, block_actions for approve/reject buttons
- PostgreSQL schema with RLS: runbooks, executions, audit_entries (append-only), agents
- Dual Dockerfiles: Rust multi-stage (agent), Node multi-stage (SaaS)
- Gitea Actions CI: Rust test+clippy, Node typecheck+test
- Fly.io config for SaaS
This commit is contained in:
2026-03-01 03:03:29 +00:00
parent 6f692fc5ef
commit 57e7083986
18 changed files with 1046 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
/// Parsed runbook step.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunbookStep {
pub index: usize,
pub description: String,
pub command: String,
pub timeout_seconds: u64,
pub on_failure: FailureAction,
pub condition: Option<String>, // Optional: only run if previous step output matches
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FailureAction {
Abort,
Continue,
Retry { max_attempts: u32 },
}
/// Parsed runbook.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Runbook {
pub name: String,
pub description: String,
pub version: String,
pub steps: Vec<RunbookStep>,
}
/// Parse a YAML runbook into structured steps.
pub fn parse_yaml(content: &str) -> anyhow::Result<Runbook> {
// TODO: Full YAML parsing with serde_yaml
// For now, return a placeholder
Ok(Runbook {
name: "placeholder".into(),
description: "TODO: implement YAML parser".into(),
version: "0.1.0".into(),
steps: vec![],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty_returns_placeholder() {
let result = parse_yaml("").unwrap();
assert_eq!(result.name, "placeholder");
assert!(result.steps.is_empty());
}
}