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,22 @@
|
||||
[package]
|
||||
name = "aos-license"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
ed25519-dalek = { version = "2", features = ["rand_core"] }
|
||||
rand = "0.8"
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "aos-license"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,69 @@
|
||||
//! License issuance: builds an ed25519-signed key from a payload.
|
||||
//!
|
||||
//! The payload is the customer-facing license metadata (product, plan, domain,
|
||||
//! expiry). The key is `base64(payload_json).base64(signature)` — verifiable
|
||||
//! offline by anyone holding the public key.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The customer-facing license metadata that gets signed.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LicensePayload {
|
||||
pub product: String,
|
||||
pub plan: String,
|
||||
pub domain: String,
|
||||
/// ISO-8601 expiry, or null for perpetual.
|
||||
pub expires_at: Option<String>,
|
||||
/// Issued-at timestamp.
|
||||
pub issued_at: String,
|
||||
}
|
||||
|
||||
/// A signed license key: the URL-safe-base64 payload + signature.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LicenseKey {
|
||||
/// `base64url(payload_json).base64url(signature)`
|
||||
pub key: String,
|
||||
pub payload: LicensePayload,
|
||||
}
|
||||
|
||||
/// Issues a license key signed by `signing_key`.
|
||||
///
|
||||
/// The signing key is platform-held; agents never see it. They call this
|
||||
/// function via the MCP tool `aos-license:issue`.
|
||||
pub fn issue_license(payload: LicensePayload, signing_key: &SigningKey) -> Result<LicenseKey> {
|
||||
let payload_json = serde_json::to_vec(&payload)?;
|
||||
let payload_b64 = URL_SAFE_NO_PAD.encode(&payload_json);
|
||||
let signature = signing_key.sign(&payload_json);
|
||||
let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
|
||||
Ok(LicenseKey {
|
||||
key: format!("{payload_b64}.{sig_b64}"),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[test]
|
||||
fn issued_key_round_trips() {
|
||||
let mut csprng = OsRng;
|
||||
let signing = SigningKey::generate(&mut csprng);
|
||||
let payload = LicensePayload {
|
||||
product: "aos-cms".into(),
|
||||
plan: "pro".into(),
|
||||
domain: "example.dfmpanel.eu".into(),
|
||||
expires_at: Some("2027-01-01T00:00:00Z".into()),
|
||||
issued_at: "2026-01-01T00:00:00Z".into(),
|
||||
};
|
||||
let key = issue_license(payload.clone(), &signing).unwrap();
|
||||
// Verify with the corresponding public key.
|
||||
let verifying = signing.verifying_key();
|
||||
assert!(crate::verify::verify_license(&key.key, &verifying).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// aos-license — license key issue/verify with ed25519.
|
||||
//
|
||||
// Agents call this crate; they never invent or edit keys. The signing key is
|
||||
// held by the platform (env-configured), never shipped to agents. Keys are
|
||||
// offline-verifiable: a customer's instance can validate a license without
|
||||
// calling home.
|
||||
//
|
||||
// See `skills/license-lifecycle/SKILL.md` for the agent procedure.
|
||||
|
||||
pub mod key;
|
||||
pub mod verify;
|
||||
|
||||
pub use key::{issue_license, LicenseKey, LicensePayload};
|
||||
pub use verify::verify_license;
|
||||
@@ -0,0 +1,93 @@
|
||||
// aos-license CLI: issue + verify subcommands.
|
||||
//
|
||||
// Usage:
|
||||
// aos-license issue --product aos-cms --plan pro --domain example.com [--expires 2027-01-01]
|
||||
// aos-license verify --key <key-string> --pubkey <base64-public-key>
|
||||
//
|
||||
// The signing key for `issue` is read from the AOS_LICENSE_SIGNING_KEY env
|
||||
// var (base64 32-byte seed). Not having it set is an error — agents must not
|
||||
// issue keys from the CLI directly in production.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "aos-license", version, about = "Ed25519 license key issue/verify")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Issue a signed license key (requires AOS_LICENSE_SIGNING_KEY env).
|
||||
Issue {
|
||||
#[arg(long)]
|
||||
product: String,
|
||||
#[arg(long)]
|
||||
plan: String,
|
||||
#[arg(long)]
|
||||
domain: String,
|
||||
#[arg(long)]
|
||||
expires: Option<String>,
|
||||
},
|
||||
/// Verify a license key against a public key.
|
||||
Verify {
|
||||
#[arg(long)]
|
||||
key: String,
|
||||
#[arg(long, help = "base64 ed25519 public key")]
|
||||
pubkey: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Issue {
|
||||
product,
|
||||
plan,
|
||||
domain,
|
||||
expires,
|
||||
} => {
|
||||
let seed_b64 = std::env::var("AOS_LICENSE_SIGNING_KEY")
|
||||
.expect("AOS_LICENSE_SIGNING_KEY env var required to issue licenses");
|
||||
let seed = STANDARD.decode(seed_b64)?;
|
||||
let signing = ed25519_dalek::SigningKey::from_bytes(
|
||||
seed.as_slice().try_into().expect("seed must be 32 bytes"),
|
||||
);
|
||||
let payload = aos_license::LicensePayload {
|
||||
product,
|
||||
plan,
|
||||
domain,
|
||||
expires_at: expires,
|
||||
issued_at: chrono_now_iso(),
|
||||
};
|
||||
let key = aos_license::issue_license(payload, &signing)?;
|
||||
println!("{}", key.key);
|
||||
Ok(())
|
||||
}
|
||||
Command::Verify { key, pubkey } => {
|
||||
let pubkey_bytes = STANDARD.decode(pubkey)?;
|
||||
let verifying = ed25519_dalek::VerifyingKey::from_bytes(
|
||||
pubkey_bytes.as_slice().try_into().expect("pubkey must be 32 bytes"),
|
||||
)?;
|
||||
let payload = aos_license::verify_license(&key, &verifying)?;
|
||||
println!(
|
||||
"valid: product={}, plan={}, domain={}, expires={:?}",
|
||||
payload.product, payload.plan, payload.domain, payload.expires_at
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chrono_now_iso() -> String {
|
||||
// Avoid pulling chrono just for a timestamp — use std.
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
format!("{secs}")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Offline license verification.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
|
||||
/// Verifies a license key string against the platform's public key.
|
||||
///
|
||||
/// Returns the decoded `LicensePayload` on success. Offline-verifiable: the
|
||||
/// customer's instance needs only the public key, no network call.
|
||||
pub fn verify_license(key: &str, public_key: &VerifyingKey) -> Result<crate::LicensePayload> {
|
||||
let (payload_b64, sig_b64) = key
|
||||
.split_once('.')
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid license key format (no '.' separator)"))?;
|
||||
|
||||
let payload_bytes = URL_SAFE_NO_PAD.decode(payload_b64)?;
|
||||
let sig_bytes = URL_SAFE_NO_PAD.decode(sig_b64)?;
|
||||
let signature = Signature::from_slice(&sig_bytes)?;
|
||||
|
||||
public_key
|
||||
.verify(&payload_bytes, &signature)
|
||||
.map_err(|_| anyhow::anyhow!("license signature verification failed"))?;
|
||||
|
||||
let payload: crate::LicensePayload = serde_json::from_slice(&payload_bytes)?;
|
||||
Ok(payload)
|
||||
}
|
||||
Reference in new issue
Block a user