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-ledger"
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,38 @@
//! Immutable audit log entry with a hash chain (GoBD Belegführung).
//!
//! Each entry's `hash` is `sha256(prev_hash || canonical(entry_fields))`.
//! Tampering with any historical entry breaks the chain. This is the
//! GoBD-compliant way to keep an electronic record (§ 146 AO, § 158 AO).
use serde::{Deserialize, Serialize};
/// One audit-log row. Never mutated after creation; corrections are new
/// entries that reference the original.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: String,
pub task_id: String,
pub actor: String,
pub action: String,
pub entity: String,
pub entity_id: String,
/// ISO-8601 timestamp.
pub at: String,
/// SHA-256 linking to the previous entry's hash. Genesis entry has a
/// fixed sentinel.
pub prev_hash: String,
/// This entry's hash: sha256(prev_hash || canonical_json(self minus hash)).
pub hash: String,
}
impl AuditEntry {
/// Computes the hash for a new entry given the previous one's hash.
///
/// TODO: implement real SHA-256. For the scaffold this returns a
/// placeholder; the actual hashing lands when the `sha2` dep is added
/// (kept out of the initial stub to keep compile time down).
pub fn compute_hash(_prev_hash: &str, _fields_canonical: &str) -> String {
// TODO: sha2::Sha256::digest(format!("{prev_hash}{fields_canonical}"))
"sha256:TODO".to_string()
}
}
@@ -0,0 +1,17 @@
// aos-ledger — money math, gapless invoice numbering, VAT, immutable audit log.
//
// Agents NEVER do money math in prompts. They call these functions. This
// keeps the GoBD guarantees (integer cents, no floats; gapless numbering;
// hash-chained audit trail) deterministic and testable.
//
// See `skills/invoice-draft/SKILL.md` for the agent procedure.
pub mod audit;
pub mod money;
pub mod numbering;
pub mod vat;
pub use audit::AuditEntry;
pub use money::Cents;
pub use numbering::next_invoice_number;
pub use vat::vat_amount;
@@ -0,0 +1,54 @@
//! Money — integer cents, no floats. The GoBD-correct representation.
//!
//! `Cents` is a fixed-point value (1 EUR = 100 cents). All arithmetic stays
//! in integers to avoid the rounding errors that plague floating-point money.
/// Amount in cents (1 EUR = 100). Never constructed from a float.
pub type Cents = i64;
/// Saturating add. Money is always positive in practice; this guards overflow.
pub fn add(a: Cents, b: Cents) -> Cents {
a.saturating_add(b)
}
/// Multiply a net amount by a quantity (integer). For fractional quantities,
/// round half-to-even at the call site first.
pub fn multiply(amount: Cents, qty: i64) -> Cents {
amount.saturating_mul(qty)
}
/// Computes the gross from a net amount + tax in basis points (1900 = 19%).
///
/// `gross = net * (1 + bp/10000)`, rounded to the nearest cent. This matches
/// § 14 UStG (Bruttobetrag on the invoice).
pub fn compute_gross(net_cents: Cents, tax_basis_points: u32) -> Cents {
// (net * (10000 + bp) + 5000) / 10000 — round half up
let scaled = net_cents as i128 * (10000 + tax_basis_points as i128);
let rounded = (scaled + 5000) / 10000;
rounded as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gross_19_percent() {
// 100.00 EUR net @ 19% → 119.00 EUR gross
assert_eq!(compute_gross(10000, 1900), 11900);
}
#[test]
fn gross_7_percent() {
// 100.00 EUR net @ 7% → 107.00 EUR gross
assert_eq!(compute_gross(10000, 700), 10700);
}
#[test]
fn gross_rounds_half_up() {
// 0.01 EUR net @ 19% → 0.0119 → rounds to 0.01 (1 cent)
assert_eq!(compute_gross(1, 1900), 1);
// 10.00 EUR net @ 19% → 11.90 EUR
assert_eq!(compute_gross(1000, 1900), 1190);
}
}
@@ -0,0 +1,35 @@
//! Gapless invoice numbering (GoBD § 146 AO).
//!
//! Numbers are sequential and must never have gaps. A "storno" creates a
//! reversing entry with its own number — the original number is never reused
//! and never deleted.
/// Formats the next invoice number as `PREFIX-YEAR-NNNN` where NNNN is
/// zero-padded to 4 digits.
///
/// `current_max` is the highest sequence number already issued this year.
/// The next is `current_max + 1`. Passing 0 issues the first of the year.
pub fn next_invoice_number(prefix: &str, year: u32, current_max: u32) -> String {
let next = current_max.saturating_add(1);
format!("{prefix}-{year}-{next:04}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_of_year() {
assert_eq!(next_invoice_number("RE", 2026, 0), "RE-2026-0001");
}
#[test]
fn subsequent() {
assert_eq!(next_invoice_number("RE", 2026, 41), "RE-2026-0042");
}
#[test]
fn crosses_thousand_boundary() {
assert_eq!(next_invoice_number("RE", 2026, 999), "RE-2026-1000");
}
}
@@ -0,0 +1,32 @@
//! VAT (Umsatzsteuer) computation.
//!
//! Tax rates are stored in basis points (1900 = 19%, 700 = 7%) to keep
//! everything integer. This avoids float drift in the ledger.
use crate::money::Cents;
/// Computes the VAT amount for a net amount.
///
/// `vat = net * bp / 10000`, rounded half-up to the nearest cent.
pub fn vat_amount(net_cents: Cents, basis_points: u32) -> Cents {
let scaled = net_cents as i128 * basis_points as i128;
let rounded = (scaled + 5000) / 10000;
rounded as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vat_19_percent() {
// 100.00 @ 19% → 19.00
assert_eq!(vat_amount(10000, 1900), 1900);
}
#[test]
fn vat_7_percent() {
// 100.00 @ 7% → 7.00
assert_eq!(vat_amount(10000, 700), 700);
}
}