Skip to main content

auths_cli/commands/
treasury.rs

1//! `auths treasury …` — an aggregate cap across a manager's sub-delegated agents.
2//!
3//! `auths credential issue --cap calls:<N>` bounds **one** delegation. A fund-of-agents
4//! needs the *aggregate* dimension: a manager holds a parent cap and allots bounded
5//! **slices** to sub-agents, with `Σ slices ≤ parent_cap` enforced at every allotment,
6//! and the budget **reallocatable** between sub-agents without ever exceeding the
7//! parent. This is the thin presentation layer; all bookkeeping lives in
8//! `auths_sdk::domains::treasury` (the aggregate analogue of the usage ledger).
9
10use std::path::PathBuf;
11
12use anyhow::Result;
13use clap::{Parser, Subcommand};
14
15use auths_sdk::storage_layout::layout;
16
17use crate::commands::executable::ExecutableCommand;
18use crate::config::CliConfig;
19use crate::ux::format::{JsonResponse, is_json_mode};
20
21/// Manage an aggregate treasury cap across a manager's sub-delegated agents.
22#[derive(Parser, Debug, Clone)]
23#[command(
24    about = "Aggregate treasury cap: allot bounded slices across sub-agents, reallocatable, with Σ ≤ cap.",
25    after_help = "Examples:
26  auths treasury open       --manager my-key --cap calls:10
27  auths treasury allot      --manager my-key --to did:keri:E… --amount 4
28  auths treasury reallocate --manager my-key --from did:keri:E… --to did:keri:E… --amount 2
29  auths treasury status     --manager my-key"
30)]
31pub struct TreasuryCommand {
32    #[clap(subcommand)]
33    pub subcommand: TreasurySubcommand,
34}
35
36/// Treasury subcommands.
37#[derive(Subcommand, Debug, Clone)]
38pub enum TreasurySubcommand {
39    /// Establish a manager's aggregate cap.
40    Open {
41        #[arg(long, help = "Manager keychain alias (the ledger key).")]
42        manager: String,
43        #[arg(long, help = "Aggregate cap (calls:<N> or a plain integer).")]
44        cap: String,
45    },
46    /// Commit a slice of the cap to a sub-agent (refused if Σ + amount > cap).
47    Allot {
48        #[arg(long)]
49        manager: String,
50        #[arg(long = "to", help = "Sub-agent did:keri: to allot to.")]
51        to: String,
52        #[arg(long)]
53        amount: u64,
54    },
55    /// Move a slice from one sub-agent to another (constant-sum; refused on underflow).
56    Reallocate {
57        #[arg(long)]
58        manager: String,
59        #[arg(long, help = "Source sub-agent did:keri:.")]
60        from: String,
61        #[arg(long = "to", help = "Target sub-agent did:keri:.")]
62        to: String,
63        #[arg(long)]
64        amount: u64,
65    },
66    /// Report parent cap, committed, free pool, slices, and the aggregate invariant.
67    Status {
68        #[arg(long)]
69        manager: String,
70    },
71    /// Credit a recorded x402 inbound settlement to a sub-agent's verifiable P&L.
72    Credit {
73        #[arg(long = "to", help = "Sub-agent did:keri: to credit.")]
74        to: String,
75        #[arg(long, help = "Path to a recorded x402 SettlementResponse JSON.")]
76        settlement: PathBuf,
77        #[arg(
78            long = "claim-cents",
79            help = "Assert the credited amount (rejected if ≠ the recorded settlement)."
80        )]
81        claim_cents: Option<u64>,
82    },
83    /// Sub-delegate a child slice from a sub-agent (Σ children ≤ the parent's slice).
84    Subdelegate {
85        #[arg(long)]
86        manager: String,
87        #[arg(
88            long,
89            help = "The sub-agent (parent) did:keri: doing the sub-delegation."
90        )]
91        parent: String,
92        #[arg(long, help = "The child worker did:keri:.")]
93        child: String,
94        #[arg(long)]
95        amount: u64,
96    },
97    /// Reclaim a revoked sub-agent's slice back to the free pool (idempotent).
98    Reclaim {
99        #[arg(long)]
100        manager: String,
101        #[arg(long = "agent", help = "The revoked sub-agent did:keri: to reclaim.")]
102        agent: String,
103    },
104}
105
106impl ExecutableCommand for TreasuryCommand {
107    fn execute(&self, ctx: &CliConfig) -> Result<()> {
108        let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
109        handle_treasury(self.clone(), repo_path)
110    }
111}
112
113/// Parse a cap predicate: `calls:<N>` (the AGT-4 grammar) or a plain integer.
114fn parse_cap(s: &str) -> Result<u64> {
115    let raw = s.strip_prefix("calls:").unwrap_or(s).trim();
116    raw.parse::<u64>().map_err(|_| {
117        anyhow::anyhow!("invalid cap {s:?}: expected calls:<N> or a non-negative integer")
118    })
119}
120
121/// Dispatch an `auths treasury …` subcommand against the repo-rooted ledger.
122pub fn handle_treasury(cmd: TreasuryCommand, repo_path: PathBuf) -> Result<()> {
123    use auths_sdk::domains::treasury;
124    match cmd.subcommand {
125        TreasurySubcommand::Open { manager, cap } => {
126            let parent_cap = parse_cap(&cap)?;
127            let v = treasury::open(&repo_path, &manager, parent_cap).map_err(anyhow::Error::new)?;
128            emit(
129                "treasury open",
130                serde_json::json!({ "status": v.status(), "manager": manager, "parent_cap": parent_cap }),
131                &format!("treasury cap established for {manager}: {parent_cap}"),
132            )
133        }
134        TreasurySubcommand::Allot {
135            manager,
136            to,
137            amount,
138        } => {
139            let v =
140                treasury::allot(&repo_path, &manager, &to, amount).map_err(anyhow::Error::new)?;
141            emit(
142                "treasury allot",
143                serde_json::json!({ "status": v.status(), "manager": manager, "to": to, "amount": amount }),
144                &format!("allot {amount} → {to}: {}", v.status()),
145            )
146        }
147        TreasurySubcommand::Reallocate {
148            manager,
149            from,
150            to,
151            amount,
152        } => {
153            let v = treasury::reallocate(&repo_path, &manager, &from, &to, amount)
154                .map_err(anyhow::Error::new)?;
155            emit(
156                "treasury reallocate",
157                serde_json::json!({ "status": v.status(), "manager": manager, "from": from, "to": to, "amount": amount }),
158                &format!("reallocate {amount} {from}→{to}: {}", v.status()),
159            )
160        }
161        TreasurySubcommand::Status { manager } => {
162            let st = treasury::status(&repo_path, &manager).map_err(anyhow::Error::new)?;
163            if is_json_mode() {
164                JsonResponse::success("treasury status", &st).print()?;
165            } else {
166                println!(
167                    "treasury {manager}: cap {} · committed {} · free {} · {}",
168                    st.parent_cap, st.committed, st.free_pool, st.status
169                );
170                for s in &st.slices {
171                    println!("  slice {} = {}", s.agent_did, s.amount);
172                }
173            }
174            Ok(())
175        }
176        TreasurySubcommand::Credit {
177            to,
178            settlement,
179            claim_cents,
180        } => {
181            let v = treasury::credit(&repo_path, &to, &settlement, claim_cents)
182                .map_err(anyhow::Error::new)?;
183            emit(
184                "treasury credit",
185                serde_json::json!({ "status": v.status(), "to": to, "credited_cents": v.credited_cents(), "rail": "x402", "direction": "inbound" }),
186                &format!(
187                    "credit {to}: {} ({}c inbound)",
188                    v.status(),
189                    v.credited_cents()
190                ),
191            )
192        }
193        TreasurySubcommand::Subdelegate {
194            manager,
195            parent,
196            child,
197            amount,
198        } => {
199            let v = treasury::subdelegate(&repo_path, &manager, &parent, &child, amount)
200                .map_err(anyhow::Error::new)?;
201            emit(
202                "treasury subdelegate",
203                serde_json::json!({ "status": v.status(), "manager": manager, "parent": parent, "child": child, "amount": amount }),
204                &format!("subdelegate {amount} {parent}→{child}: {}", v.status()),
205            )
206        }
207        TreasurySubcommand::Reclaim { manager, agent } => {
208            let v = treasury::reclaim(&repo_path, &manager, &agent).map_err(anyhow::Error::new)?;
209            emit(
210                "treasury reclaim",
211                serde_json::json!({ "status": v.status(), "manager": manager, "agent": agent }),
212                &format!("reclaim {agent}: {}", v.status()),
213            )
214        }
215    }
216}
217
218/// Print a mutation outcome — the verdict JSON under `--json`, else a human line.
219fn emit(command: &str, data: serde_json::Value, human: &str) -> Result<()> {
220    if is_json_mode() {
221        JsonResponse::success(command, data).print()?;
222    } else {
223        println!("✓ {human}");
224    }
225    Ok(())
226}