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:
Z User committed 2026-10-30 13:01:03 +00:00
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;