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-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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user