feat: zeroclaw agentic agency architecture — agents, skills, Rust crates
- 18 agent specs across Software, Support, Marketing, Cross-cutting (orchestrator + 7 software + 5 support + 3 cross-cutting + leads-finder) - 8 reusable skills (handoff, pr-review, dokploy-deploy, campaign-structure, content-calendar, ticket-triage, license-lifecycle, invoice-draft) - 3 JSON schemas: handoff, deliverable, approval - Policies: autonomy levels, tool risk classification, compliance (DSGVO/GoBD/UWG) - routing.toml: model tiers T1/T2/T3 + provider config (PII gating) - Rust workspace (5 crates, all compile + 16 tests pass): - aos-mcp: tool registry + risk tagging - aos-policy: autonomy levels, budget enforcement, approval requests - aos-license: ed25519 license issue/verify (real crypto + CLI binary) - aos-ledger: integer-cents money math, gapless numbering, VAT, audit chain - aos-sync: frontmatter validator (18/18 specs pass), CLI binary - CLAUDE.md entry point with architecture overview + roadmap
This commit is contained in:
1 parent
d18c463ffd
commit
136379c666
60 files changed
+4776
No files matched your search
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "aos-policy"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Approval request generation for high-risk actions.
|
||||
//!
|
||||
//! Mirrors `schemas/approval.json`. A tool call that `can_execute()` rejects
|
||||
//! produces one of these, which lands in the AOS approval inbox + notifies the
|
||||
//! human approver.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The category of irreversible/external action. Each maps to a hard rule in
|
||||
/// `policies/autonomy.md`. Must stay in sync with `schemas/approval.json`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApprovalAction {
|
||||
Spend,
|
||||
PublishPublic,
|
||||
SendInvoice,
|
||||
SendClientCommunication,
|
||||
SendOutreach,
|
||||
RevokeLicense,
|
||||
DeleteData,
|
||||
DeleteInstance,
|
||||
DeleteDns,
|
||||
ProductionDeploy,
|
||||
DbMigrationLive,
|
||||
FileWithAuthorities,
|
||||
ContractChange,
|
||||
BudgetChange,
|
||||
CampaignActivation,
|
||||
}
|
||||
|
||||
/// A request that pauses a task at a human gate.
|
||||
///
|
||||
/// See `schemas/approval.json` for the authoritative shape. This struct is
|
||||
/// the Rust mirror; the policy crate constructs it, AOS persists it.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApprovalRequest {
|
||||
pub approval_id: String,
|
||||
pub task_id: String,
|
||||
pub agent: String,
|
||||
pub action: ApprovalAction,
|
||||
pub risk: ApprovalRisk,
|
||||
pub rationale: String,
|
||||
pub diff_summary: String,
|
||||
/// Structured details for the approver to inspect. Validated per-action.
|
||||
#[serde(default)]
|
||||
pub payload: serde_json::Value,
|
||||
pub expires_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub estimated_cost_eur: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ApprovalRisk {
|
||||
Medium,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
/// Constructs an `ApprovalRequest` for the given action. The caller fills in
|
||||
/// `rationale`, `diff_summary`, and `payload`; this provides the boilerplate.
|
||||
///
|
||||
/// `approval_id` + `task_id` + `agent` are required; `risk` defaults to High.
|
||||
pub fn require_approval(
|
||||
approval_id: impl Into<String>,
|
||||
task_id: impl Into<String>,
|
||||
agent: impl Into<String>,
|
||||
action: ApprovalAction,
|
||||
) -> ApprovalRequest {
|
||||
ApprovalRequest {
|
||||
approval_id: approval_id.into(),
|
||||
task_id: task_id.into(),
|
||||
agent: agent.into(),
|
||||
action,
|
||||
risk: ApprovalRisk::High,
|
||||
rationale: String::new(),
|
||||
diff_summary: String::new(),
|
||||
payload: serde_json::Value::Null,
|
||||
expires_at: None,
|
||||
estimated_cost_eur: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Autonomy levels and the rule that decides whether a given (level, risk)
|
||||
//! combination may execute without an approval.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Agent autonomy level. L3 (full autonomy) is intentionally absent — not
|
||||
/// used initially. See `policies/autonomy.md`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AutonomyLevel {
|
||||
/// Read/analyze only. Produces a report/draft, changes no state.
|
||||
L0,
|
||||
/// Drafts inside the system; a human approves before anything leaves.
|
||||
L1,
|
||||
/// Acts alone within caps and reversible scope. Internal-only side effects.
|
||||
L2,
|
||||
}
|
||||
|
||||
/// The risk classification mirrored from `aos-mcp::Risk` to avoid a circular
|
||||
/// dependency. Kept in sync manually (a test asserts the string forms match).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ToolRisk {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// Returns whether an agent at `level` may execute a tool of `risk` without
|
||||
/// producing an approval request.
|
||||
///
|
||||
/// - Low risk: always executable (any level).
|
||||
/// - Medium risk: L1/L2 may execute (internal reversible writes). L0 may not.
|
||||
/// - High risk: **never** auto-executed. Always produces an `ApprovalRequest`
|
||||
/// regardless of level. Returns `false`.
|
||||
pub fn can_execute(level: AutonomyLevel, risk: ToolRisk) -> bool {
|
||||
match risk {
|
||||
ToolRisk::Low => true,
|
||||
ToolRisk::Medium => matches!(level, AutonomyLevel::L1 | AutonomyLevel::L2),
|
||||
ToolRisk::High => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn low_risk_always_executable() {
|
||||
for level in [AutonomyLevel::L0, AutonomyLevel::L1, AutonomyLevel::L2] {
|
||||
assert!(can_execute(level, ToolRisk::Low));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_risk_never_auto_executed() {
|
||||
for level in [AutonomyLevel::L0, AutonomyLevel::L1, AutonomyLevel::L2] {
|
||||
assert!(!can_execute(level, ToolRisk::High));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_risk_blocked_for_l0() {
|
||||
assert!(!can_execute(AutonomyLevel::L0, ToolRisk::Medium));
|
||||
assert!(can_execute(AutonomyLevel::L1, ToolRisk::Medium));
|
||||
assert!(can_execute(AutonomyLevel::L2, ToolRisk::Medium));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Per-task budget tracking (EUR + tokens). Hard-stops when the cap is reached
|
||||
//! — no agent can spend past its declared ceiling, even with approval, without
|
||||
//! an explicit budget-increase approval.
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
/// Tracks cumulative spend for a single task. The policy crate owns one per
|
||||
/// active `task_id`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BudgetTracker {
|
||||
task_id: String,
|
||||
spent_eur: f64,
|
||||
spent_tokens: u64,
|
||||
cap_eur: f64,
|
||||
cap_tokens: u64,
|
||||
}
|
||||
|
||||
/// Result of checking spend against the cap.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum SpendCheck {
|
||||
/// Within budget.
|
||||
Ok,
|
||||
/// Would exceed the cap — the call is rejected.
|
||||
OverBudget,
|
||||
}
|
||||
|
||||
impl BudgetTracker {
|
||||
pub fn new(task_id: impl Into<String>, cap_eur: f64, cap_tokens: u64) -> Self {
|
||||
Self {
|
||||
task_id: task_id.into(),
|
||||
spent_eur: 0.0,
|
||||
spent_tokens: 0,
|
||||
cap_eur,
|
||||
cap_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether spending `eur` + `tokens` more is allowed. Does NOT
|
||||
/// record the spend — call `record` after the action succeeds.
|
||||
pub fn check_spend(&self, eur: f64, tokens: u64) -> SpendCheck {
|
||||
if self.spent_eur + eur > self.cap_eur + f64::EPSILON {
|
||||
return SpendCheck::OverBudget;
|
||||
}
|
||||
if self.spent_tokens.saturating_add(tokens) > self.cap_tokens {
|
||||
return SpendCheck::OverBudget;
|
||||
}
|
||||
SpendCheck::Ok
|
||||
}
|
||||
|
||||
/// Records actual spend. Returns an error if it would exceed the cap
|
||||
/// (defensive — `check_spend` should have caught it).
|
||||
pub fn record(&mut self, eur: f64, tokens: u64) -> Result<()> {
|
||||
if self.check_spend(eur, tokens) == SpendCheck::OverBudget {
|
||||
bail!(
|
||||
"budget cap exceeded for task {}: {:.2} EUR / {} tokens (cap {:.2} / {})",
|
||||
self.task_id,
|
||||
self.spent_eur + eur,
|
||||
self.spent_tokens + tokens,
|
||||
self.cap_eur,
|
||||
self.cap_tokens
|
||||
);
|
||||
}
|
||||
self.spent_eur += eur;
|
||||
self.spent_tokens = self.spent_tokens.saturating_add(tokens);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn spent_eur(&self) -> f64 {
|
||||
self.spent_eur
|
||||
}
|
||||
pub fn spent_tokens(&self) -> u64 {
|
||||
self.spent_tokens
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn within_budget_ok() {
|
||||
let t = BudgetTracker::new("AOS-1", 10.0, 1000);
|
||||
assert_eq!(t.check_spend(5.0, 500), SpendCheck::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_eur_budget_rejected() {
|
||||
let t = BudgetTracker::new("AOS-1", 10.0, 1000);
|
||||
assert_eq!(t.check_spend(15.0, 100), SpendCheck::OverBudget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_token_budget_rejected() {
|
||||
let t = BudgetTracker::new("AOS-1", 10.0, 1000);
|
||||
assert_eq!(t.check_spend(1.0, 2000), SpendCheck::OverBudget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_accumulates() {
|
||||
let mut t = BudgetTracker::new("AOS-1", 10.0, 1000);
|
||||
t.record(3.0, 200).unwrap();
|
||||
t.record(3.0, 200).unwrap();
|
||||
assert_eq!(t.spent_eur(), 6.0);
|
||||
assert_eq!(t.spent_tokens(), 400);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// aos-policy — approval gate + budget enforcement.
|
||||
//
|
||||
// Every high-risk tool call passes through this crate. It enforces:
|
||||
// - autonomy levels (L0/L1/L2) vs tool risk
|
||||
// - budget caps per task (EUR + tokens)
|
||||
// - the hard approval list (spend, publish, send, revoke, delete, deploy,
|
||||
// file-with-authorities, contract changes)
|
||||
//
|
||||
// See `policies/autonomy.md` and `policies/tool-risk.md` for the rules.
|
||||
|
||||
pub mod approval;
|
||||
pub mod autonomy;
|
||||
pub mod budget;
|
||||
|
||||
pub use approval::{ApprovalAction, ApprovalRequest};
|
||||
pub use autonomy::{can_execute, AutonomyLevel};
|
||||
pub use budget::BudgetTracker;
|
||||
Reference in new issue
Block a user