use crate::approval::{AuditEvent, AuditEventType};
use crate::audit::log_event;
use crate::constraints::{Constraint, ConstraintSet, ConstraintValue};
use crate::crypto::{PublicKey, Signature, SigningKey};
use crate::diff::ClearanceDiff;
use crate::error::{Error, Result};
use crate::wire::{
KEY_DISPLAY_TRUNCATION, MAX_CONSTRAINTS_PER_TOOL, MAX_EXTENSION_KEYS, MAX_EXTENSION_KEY_SIZE,
MAX_EXTENSION_VALUE_SIZE, MAX_TOOLS_PER_WARRANT,
};
use crate::MAX_DELEGATION_DEPTH;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::time::Duration;
use uuid::Uuid;
pub const WARRANT_ID_PREFIX: &str = "tnu_wrt_";
pub const WARRANT_VERSION: u32 = 1;
pub const CLOCK_SKEW_TOLERANCE_SECS: u64 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum WarrantType {
Execution = 0,
Issuer = 1,
}
impl Serialize for WarrantType {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(*self as u8)
}
}
impl<'de> Deserialize<'de> for WarrantType {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct WarrantTypeVisitor;
impl<'de> serde::de::Visitor<'de> for WarrantTypeVisitor {
type Value = WarrantType;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("integer 0 (Execution) or 1 (Issuer)")
}
fn visit_u8<E>(self, value: u8) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
0 => Ok(WarrantType::Execution),
1 => Ok(WarrantType::Issuer),
v => Err(E::custom(format!("invalid warrant type: {}", v))),
}
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_u8(value as u8)
}
}
deserializer.deserialize_u8(WarrantTypeVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct Clearance(pub u8);
impl Clearance {
pub const UNTRUSTED: Self = Self(0);
pub const EXTERNAL: Self = Self(10);
pub const PARTNER: Self = Self(20);
pub const INTERNAL: Self = Self(30);
pub const PRIVILEGED: Self = Self(40);
pub const SYSTEM: Self = Self(50);
pub const fn custom(level: u8) -> Self {
Self(level)
}
pub const fn new(level: u8) -> Self {
Self(level)
}
pub const fn level(&self) -> u8 {
self.0
}
pub fn meets(&self, required: Clearance) -> bool {
self.0 >= required.0
}
}
impl std::str::FromStr for Clearance {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"untrusted" => Ok(Clearance::UNTRUSTED),
"external" => Ok(Clearance::EXTERNAL),
"partner" => Ok(Clearance::PARTNER),
"internal" => Ok(Clearance::INTERNAL),
"privileged" => Ok(Clearance::PRIVILEGED),
"system" => Ok(Clearance::SYSTEM),
s => s.parse::<u8>().map(Clearance).map_err(|_| {
format!(
"Invalid clearance: {}. Must be a named level or integer (0-255).",
s
)
}),
}
}
}
impl std::fmt::Display for Clearance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
0 => write!(f, "Untrusted"),
10 => write!(f, "External"),
20 => write!(f, "Partner"),
30 => write!(f, "Internal"),
40 => write!(f, "Privileged"),
50 => write!(f, "System"),
n => write!(f, "Level({})", n),
}
}
}
pub const POP_TIMESTAMP_WINDOW_SECS: i64 = 30;
pub const POP_MAX_WINDOWS: u32 = 5;
use crate::domain::POP_CONTEXT;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WarrantId([u8; 16]);
impl Serialize for WarrantId {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.0)
}
}
impl<'de> Deserialize<'de> for WarrantId {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct BytesVisitor;
impl<'de> serde::de::Visitor<'de> for BytesVisitor {
type Value = WarrantId;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("16 bytes for warrant ID")
}
fn visit_bytes<E>(self, v: &[u8]) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
if v.len() != 16 {
return Err(E::custom(format!(
"warrant ID must be 16 bytes, got {}",
v.len()
)));
}
let mut arr = [0u8; 16];
arr.copy_from_slice(v);
Ok(WarrantId(arr))
}
fn visit_byte_buf<E>(self, v: Vec<u8>) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_bytes(&v)
}
}
deserializer.deserialize_bytes(BytesVisitor)
}
}
impl WarrantId {
pub fn new() -> Self {
Self(*Uuid::now_v7().as_bytes())
}
pub fn new_random() -> Self {
Self(*Uuid::new_v4().as_bytes())
}
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn from_string(s: impl AsRef<str>) -> Result<Self> {
let s = s.as_ref();
let hex_str = if let Some(stripped) = s.strip_prefix(WARRANT_ID_PREFIX) {
stripped
} else if s.contains('-') {
return Uuid::parse_str(s)
.map(|u| Self(*u.as_bytes()))
.map_err(|e| Error::InvalidWarrantId(format!("invalid UUID: {}", e)));
} else {
s
};
if hex_str.len() != 32 {
return Err(Error::InvalidWarrantId(format!(
"expected 32 hex chars, got {}",
hex_str.len()
)));
}
let mut bytes = [0u8; 16];
for (i, chunk) in hex_str.as_bytes().chunks(2).enumerate() {
let hex = std::str::from_utf8(chunk)
.map_err(|_| Error::InvalidWarrantId("invalid UTF-8".to_string()))?;
bytes[i] = u8::from_str_radix(hex, 16)
.map_err(|_| Error::InvalidWarrantId(format!("invalid hex: {}", hex)))?;
}
Ok(Self(bytes))
}
pub fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn to_prefixed_string(&self) -> String {
format!("{}{}", WARRANT_ID_PREFIX, hex::encode(self.0))
}
}
impl Default for WarrantId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for WarrantId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", WARRANT_ID_PREFIX, hex::encode(self.0))
}
}
pub use crate::payload::WarrantPayload;
#[derive(Debug, Clone)]
pub struct Warrant {
pub payload: WarrantPayload,
pub signature: Signature,
pub payload_bytes: Vec<u8>,
pub envelope_version: u8,
}
impl Serialize for Warrant {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeTuple;
let mut tup = serializer.serialize_tuple(3)?;
tup.serialize_element(&self.envelope_version)?;
tup.serialize_element(&serde_bytes::Bytes::new(&self.payload_bytes))?;
tup.serialize_element(&self.signature)?;
tup.end()
}
}
impl<'de> Deserialize<'de> for Warrant {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct WarrantVisitor;
impl<'de> serde::de::Visitor<'de> for WarrantVisitor {
type Value = Warrant;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a signed warrant array [ver, payload_bytes, sig]")
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let envelope_version: u8 = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
if envelope_version != 1 {
return Err(serde::de::Error::custom(format!(
"unsupported envelope_version: {}",
envelope_version
)));
}
let payload_bytes: Vec<u8> = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
let signature: Signature = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
let payload: WarrantPayload = ciborium::de::from_reader(&payload_bytes[..])
.map_err(|e| {
serde::de::Error::custom(format!("invalid payload CBOR: {}", e))
})?;
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(envelope_version);
preimage.extend_from_slice(&payload_bytes);
if payload.issuer.verify(&preimage, &signature).is_err() {
return Err(serde::de::Error::custom("invalid warrant signature"));
}
Ok(Warrant {
envelope_version,
payload,
signature,
payload_bytes,
})
}
}
deserializer.deserialize_tuple(3, WarrantVisitor)
}
}
impl Warrant {
pub fn builder() -> WarrantBuilder {
WarrantBuilder::new()
}
pub fn id(&self) -> &WarrantId {
&self.payload.id
}
pub fn r#type(&self) -> WarrantType {
self.payload.warrant_type
}
pub fn version(&self) -> u8 {
self.payload.version
}
pub fn capabilities(&self) -> Option<&BTreeMap<String, ConstraintSet>> {
if self.payload.tools.is_empty() {
None
} else {
Some(&self.payload.tools)
}
}
pub fn retain_capabilities(&mut self, tools: &[String]) {
let tools_set: std::collections::HashSet<_> = tools.iter().map(|s| s.as_str()).collect();
self.payload
.tools
.retain(|k, _| tools_set.contains(k.as_str()));
}
pub fn tools(&self) -> Vec<String> {
let mut tools: Vec<String> = self.payload.tools.keys().cloned().collect();
tools.sort(); tools
}
pub fn issuable_tools(&self) -> Option<&[String]> {
self.payload.issuable_tools.as_deref()
}
pub fn max_issue_depth(&self) -> Option<u32> {
self.payload.max_issue_depth
}
pub fn constraint_bounds(&self) -> Option<&ConstraintSet> {
self.payload.constraint_bounds.as_ref()
}
pub fn clearance(&self) -> Option<Clearance> {
self.payload.clearance
}
pub fn issued_at(&self) -> DateTime<Utc> {
DateTime::from_timestamp(self.payload.issued_at as i64, 0).unwrap_or_default()
}
pub fn expires_at(&self) -> DateTime<Utc> {
DateTime::from_timestamp(self.payload.expires_at as i64, 0).unwrap_or_default()
}
pub fn validate_constraint_depth(&self) -> Result<()> {
if self.payload.tools.len() > MAX_TOOLS_PER_WARRANT {
return Err(Error::Validation(format!(
"tools count {} exceeds limit {}",
self.payload.tools.len(),
MAX_TOOLS_PER_WARRANT
)));
}
for constraints in self.payload.tools.values() {
if constraints.len() > MAX_CONSTRAINTS_PER_TOOL {
return Err(Error::Validation(format!(
"constraints count {} exceeds limit {}",
constraints.len(),
MAX_CONSTRAINTS_PER_TOOL
)));
}
constraints.validate_depth()?;
}
if let Some(constraint_bounds) = &self.payload.constraint_bounds {
constraint_bounds.validate_depth()?;
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
if self.payload.version != WARRANT_VERSION as u8 {
return Err(Error::Validation(format!(
"unsupported warrant version: {} (expected {})",
self.payload.version, WARRANT_VERSION
)));
}
let now = Utc::now().timestamp() as u64;
if self.payload.issued_at > now.saturating_add(CLOCK_SKEW_TOLERANCE_SECS) {
return Err(Error::IssuedInFuture);
}
if self.payload.expires_at <= self.payload.issued_at {
return Err(Error::Validation(
"warrant expires_at must be strictly greater than issued_at".to_string(),
));
}
if self.is_expired() {
}
match self.payload.warrant_type {
WarrantType::Execution => {
if self.payload.issuable_tools.is_some() {
return Err(Error::InvalidWarrantType {
message: "execution warrant cannot have issuable_tools".to_string(),
});
}
if self.payload.max_issue_depth.is_some() {
return Err(Error::InvalidWarrantType {
message: "execution warrant cannot have max_issue_depth".to_string(),
});
}
}
WarrantType::Issuer => {
if !self.payload.tools.is_empty() {
return Err(Error::InvalidWarrantType {
message: "issuer warrant cannot have tools".to_string(),
});
}
if self
.payload
.issuable_tools
.as_ref()
.is_none_or(|t| t.is_empty())
{
return Err(Error::InvalidWarrantType {
message: "issuer warrant requires at least one issuable_tool".to_string(),
});
}
}
}
if let Some(max_issue) = self.payload.max_issue_depth {
let effective_max = self.effective_max_depth();
if max_issue > effective_max {
return Err(Error::IssueDepthExceeded {
depth: max_issue,
max: effective_max,
});
}
}
self.validate_constraint_depth()?;
if self.payload.extensions.len() > MAX_EXTENSION_KEYS {
return Err(Error::Validation(format!(
"extensions count {} exceeds limit {}",
self.payload.extensions.len(),
MAX_EXTENSION_KEYS
)));
}
for (key, val) in &self.payload.extensions {
if val.len() > MAX_EXTENSION_VALUE_SIZE {
return Err(Error::Validation(format!(
"extension '{}' value size {} exceeds limit {}",
key,
val.len(),
MAX_EXTENSION_VALUE_SIZE
)));
}
}
if self.is_expired() {
}
Ok(())
}
pub fn depth(&self) -> u32 {
self.payload.depth
}
pub fn max_depth(&self) -> Option<u32> {
Some(self.payload.max_depth as u32)
}
pub fn effective_max_depth(&self) -> u32 {
self.payload.max_depth as u32
}
pub fn parent_hash(&self) -> Option<&[u8; 32]> {
self.payload.parent_hash.as_ref()
}
pub fn is_root(&self) -> bool {
self.payload.parent_hash.is_none()
}
pub fn session_id(&self) -> Option<&str> {
self.payload.session_id.as_deref()
}
pub fn agent_id(&self) -> Option<&str> {
self.payload.agent_id.as_deref()
}
pub fn issuer(&self) -> &PublicKey {
&self.payload.issuer
}
pub fn authorized_holder(&self) -> &PublicKey {
&self.payload.holder
}
pub fn extensions(&self) -> &BTreeMap<String, Vec<u8>> {
&self.payload.extensions
}
pub fn extension(&self, key: &str) -> Option<&Vec<u8>> {
self.payload.extensions.get(key)
}
pub fn requires_pop(&self) -> bool {
true
}
pub fn required_approvers(&self) -> Option<&Vec<PublicKey>> {
self.payload.required_approvers.as_ref()
}
pub fn min_approvals(&self) -> Option<u32> {
self.payload.min_approvals
}
pub fn requires_multisig(&self) -> bool {
self.payload.required_approvers.is_some()
}
pub fn approval_threshold(&self) -> u32 {
use std::convert::TryInto;
match (&self.payload.required_approvers, self.payload.min_approvals) {
(Some(approvers), Some(min)) => {
let len: u32 = approvers.len().try_into().unwrap_or(u32::MAX);
min.min(len)
}
(Some(approvers), None) => approvers.len().try_into().unwrap_or(u32::MAX),
(None, _) => 0,
}
}
pub fn payload_bytes(&self) -> &[u8] {
&self.payload_bytes
}
pub fn signature_preimage(&self) -> Vec<u8> {
let mut preimage = Vec::with_capacity(1 + self.payload_bytes.len());
preimage.push(self.envelope_version);
preimage.extend_from_slice(&self.payload_bytes);
preimage
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub fn verify_holder(&self, challenge: &[u8], signature: &Signature) -> Result<()> {
self.payload
.holder
.verify(challenge, signature)
.map_err(|_| Error::SignatureInvalid("holder proof-of-possession failed".to_string()))
}
pub fn is_expired(&self) -> bool {
let now = Utc::now().timestamp() as u64;
now >= self.payload.expires_at
}
pub fn is_terminal(&self) -> bool {
self.depth() >= self.effective_max_depth()
}
pub fn is_expired_with_tolerance(&self, tolerance: chrono::Duration) -> bool {
let now = Utc::now().timestamp() as u64;
let tol_secs = tolerance.num_seconds();
if tol_secs < 0 {
now > self.payload.expires_at.saturating_sub((-tol_secs) as u64)
} else {
now > self.payload.expires_at.saturating_add(tol_secs as u64)
}
}
pub fn verify(&self, expected_issuer: &PublicKey) -> Result<()> {
if &self.payload.issuer != expected_issuer {
return Err(Error::SignatureInvalid(
"issuer public key does not match".to_string(),
));
}
self.verify_signature()
}
pub fn verify_signature(&self) -> Result<()> {
self.payload
.issuer
.verify(&self.signature_preimage(), &self.signature)
}
pub fn authorize(
&self,
tool: &str,
args: &HashMap<String, ConstraintValue>,
signature: Option<&Signature>,
) -> Result<()> {
if self.is_expired() {
return Err(Error::WarrantExpired(self.expires_at()));
}
if self.payload.warrant_type != WarrantType::Execution {
return Err(Error::InvalidWarrantType {
message: "only execution warrants can authorize actions".to_string(),
});
}
let constraints = if let Some(c) = self.payload.tools.get(tool) {
c
} else if let Some(c) = self.payload.tools.get("*") {
c
} else {
return Err(Error::ConstraintNotSatisfied {
field: "tool".to_string(),
reason: format!("warrant does not authorize tool '{}'", tool),
});
};
self.verify_pop(
tool,
args,
signature,
POP_TIMESTAMP_WINDOW_SECS,
POP_MAX_WINDOWS,
)?;
constraints.matches(args)
}
pub fn authorize_with_pop_config(
&self,
tool: &str,
args: &HashMap<String, ConstraintValue>,
signature: Option<&Signature>,
pop_window_secs: i64,
pop_max_windows: u32,
) -> Result<()> {
if self.is_expired() {
return Err(Error::WarrantExpired(self.expires_at()));
}
if self.payload.warrant_type != WarrantType::Execution {
return Err(Error::InvalidWarrantType {
message: "only execution warrants can authorize actions".to_string(),
});
}
let constraints = if let Some(c) = self.payload.tools.get(tool) {
c
} else if let Some(c) = self.payload.tools.get("*") {
c
} else {
return Err(Error::ConstraintNotSatisfied {
field: "tool".to_string(),
reason: format!("warrant does not authorize tool '{}'", tool),
});
};
self.verify_pop(tool, args, signature, pop_window_secs, pop_max_windows)?;
constraints.matches(args)
}
pub fn check_constraints(
&self,
tool: &str,
args: &HashMap<String, ConstraintValue>,
) -> Result<()> {
let constraints = if let Some(c) = self.payload.tools.get(tool) {
c
} else if let Some(c) = self.payload.tools.get("*") {
c
} else {
return Err(Error::ConstraintNotSatisfied {
field: "tool".to_string(),
reason: format!("warrant does not authorize tool '{}'", tool),
});
};
constraints.matches(args)
}
pub fn verify_pop(
&self,
tool: &str,
args: &HashMap<String, ConstraintValue>,
signature: Option<&Signature>,
window_secs: i64,
max_windows: u32,
) -> Result<()> {
let signature = signature
.ok_or_else(|| Error::MissingSignature("Proof-of-Possession required".to_string()))?;
let now = Utc::now().timestamp();
let mut sorted_args: Vec<(&String, &ConstraintValue)> = args.iter().collect();
sorted_args.sort_by_key(|(k, _)| *k);
let mut verified = false;
let id_hex = self.payload.id.to_hex();
let mut challenge_bytes = Vec::with_capacity(256);
let mut preimage = Vec::with_capacity(256);
let half = (max_windows / 2) as i64;
for i in 0..max_windows {
let offset = if i == 0 {
0
} else {
let abs_offset = i.div_ceil(2) as i64;
if i % 2 == 1 {
-abs_offset } else {
abs_offset }
};
if offset.abs() > half {
continue;
}
let window_ts = (now / window_secs + offset) * window_secs;
let challenge_data = (&id_hex, tool, &sorted_args, window_ts);
challenge_bytes.clear();
if ciborium::ser::into_writer(&challenge_data, &mut challenge_bytes).is_err() {
continue;
}
preimage.clear();
preimage.extend_from_slice(POP_CONTEXT);
preimage.extend_from_slice(&challenge_bytes);
if self.payload.holder.verify(&preimage, signature).is_ok() {
verified = true;
break;
}
}
if !verified {
return Err(Error::SignatureInvalid(
"Proof-of-Possession verification failed".to_string(),
));
}
Ok(())
}
pub fn sign(
&self,
keypair: &SigningKey,
tool: &str,
args: &HashMap<String, ConstraintValue>,
) -> Result<Signature> {
self.sign_with_timestamp(keypair, tool, args, None)
}
pub fn sign_with_timestamp(
&self,
keypair: &SigningKey,
tool: &str,
args: &HashMap<String, ConstraintValue>,
timestamp: Option<i64>,
) -> Result<Signature> {
let mut sorted_args: Vec<(&String, &ConstraintValue)> = args.iter().collect();
sorted_args.sort_by_key(|(k, _)| *k);
let now = timestamp.unwrap_or_else(|| Utc::now().timestamp());
let window_ts = (now / POP_TIMESTAMP_WINDOW_SECS) * POP_TIMESTAMP_WINDOW_SECS;
let challenge_data = (self.payload.id.to_hex(), tool, sorted_args, window_ts);
let mut challenge_bytes = Vec::new();
ciborium::ser::into_writer(&challenge_data, &mut challenge_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let mut preimage = POP_CONTEXT.to_vec();
preimage.extend_from_slice(&challenge_bytes);
Ok(keypair.sign(&preimage))
}
pub fn dedup_key(&self, tool: &str, args: &HashMap<String, ConstraintValue>) -> String {
use sha2::{Digest, Sha256};
let mut sorted_args: Vec<(&String, &ConstraintValue)> = args.iter().collect();
sorted_args.sort_by_key(|(k, _)| *k);
let payload = (self.payload.id.to_hex(), tool, &sorted_args);
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.expect("dedup payload serialization should never fail");
let mut hasher = Sha256::new();
hasher.update(&payload_bytes);
let hash = hasher.finalize();
hex::encode(hash)
}
pub const fn dedup_ttl_secs() -> i64 {
POP_TIMESTAMP_WINDOW_SECS * 4
}
pub fn attenuate(&self) -> AttenuationBuilder<'_> {
AttenuationBuilder::new(self)
}
pub fn issue_execution_warrant(&self) -> Result<IssuanceBuilder<'_>> {
if self.payload.warrant_type != WarrantType::Issuer {
return Err(Error::InvalidWarrantType {
message: "can only issue execution warrants from issuer warrants".to_string(),
});
}
Ok(IssuanceBuilder::new(self))
}
}
#[derive(Debug, Clone)]
pub struct WarrantBuilder {
warrant_type: Option<WarrantType>,
tools: BTreeMap<String, ConstraintSet>,
issuable_tools: Option<Vec<String>>,
max_issue_depth: Option<u32>,
constraint_bounds: ConstraintSet,
clearance: Option<Clearance>,
ttl: Option<Duration>,
max_depth: Option<u32>,
session_id: Option<String>,
agent_id: Option<String>,
holder: Option<PublicKey>,
required_approvers: Option<Vec<PublicKey>>,
min_approvals: Option<u32>,
id: Option<WarrantId>,
extensions: BTreeMap<String, Vec<u8>>,
error: Option<Error>,
}
impl WarrantBuilder {
pub fn new() -> Self {
Self {
warrant_type: None,
tools: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: ConstraintSet::new(),
clearance: None,
ttl: None,
max_depth: None,
session_id: None,
agent_id: None,
holder: None,
required_approvers: None,
min_approvals: None,
id: None,
extensions: BTreeMap::new(),
error: None,
}
}
pub fn r#type(mut self, warrant_type: WarrantType) -> Self {
self.warrant_type = Some(warrant_type);
self
}
pub fn clearance(mut self, level: Clearance) -> Self {
self.clearance = Some(level);
self
}
pub fn issuable_tools(mut self, tools: Vec<String>) -> Self {
self.issuable_tools = Some(tools);
self
}
pub fn max_issue_depth(mut self, depth: u32) -> Self {
self.max_issue_depth = Some(depth);
self
}
pub fn constraint_bound(
mut self,
field: impl Into<String>,
constraint: impl Into<Constraint>,
) -> Self {
self.constraint_bounds.insert(field, constraint);
self
}
pub fn extension(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
self.extensions.insert(key.into(), value);
self
}
pub fn tool(mut self, tool: impl Into<String>, constraints: ConstraintSet) -> Self {
let tool_name: String = tool.into();
if tool_name.starts_with("tenuo:") {
self.error = Some(Error::Validation(
"Reserved tool namespace: tools starting with 'tenuo:' are reserved for framework use"
.to_string(),
));
return self;
}
self.tools.insert(tool_name, constraints);
self
}
pub fn capability(self, tool: impl Into<String>, constraints: ConstraintSet) -> Self {
self.tool(tool, constraints)
}
pub fn holder(mut self, holder: PublicKey) -> Self {
self.holder = Some(holder);
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.session_id = Some(id.into());
self
}
pub fn agent_id(mut self, id: impl Into<String>) -> Self {
self.agent_id = Some(id.into());
self
}
pub fn required_approvers(mut self, approvers: Vec<PublicKey>) -> Self {
self.required_approvers = Some(approvers);
self
}
pub fn min_approvals(mut self, min: u32) -> Self {
self.min_approvals = Some(min);
self
}
pub fn max_depth(mut self, depth: u32) -> Self {
self.max_depth = Some(depth);
self
}
pub fn id(mut self, id: WarrantId) -> Self {
self.id = Some(id);
self
}
pub fn build(mut self, signing_key: &SigningKey) -> Result<Warrant> {
if let Some(err) = self.error {
return Err(err);
}
if self.warrant_type.is_none() {
if !self.tools.is_empty() {
self.warrant_type = Some(WarrantType::Execution);
} else if self.issuable_tools.is_some() || self.max_issue_depth.is_some() {
self.warrant_type = Some(WarrantType::Issuer);
}
}
if let Some(tools) = &mut self.issuable_tools {
tools.sort();
}
if let Some(approvers) = &mut self.required_approvers {
approvers.sort_by_key(|a| a.to_bytes());
}
let warrant_type = self
.warrant_type
.ok_or_else(|| Error::Validation("warrant type required".to_string()))?;
match warrant_type {
WarrantType::Execution => {
if self.tools.is_empty() {
return Err(Error::InvalidWarrantType {
message: "execution warrant must have tools".to_string(),
});
}
if self.issuable_tools.is_some() {
return Err(Error::InvalidWarrantType {
message: "execution warrant cannot have issuable_tools".to_string(),
});
}
if self.max_issue_depth.is_some() {
return Err(Error::InvalidWarrantType {
message: "execution warrant cannot have max_issue_depth".to_string(),
});
}
}
WarrantType::Issuer => {
if !self.tools.is_empty() {
return Err(Error::InvalidWarrantType {
message: "issuer warrant cannot have tools".to_string(),
});
}
if self.issuable_tools.as_ref().is_none_or(|t| t.is_empty()) {
return Err(Error::InvalidWarrantType {
message: "issuer warrant requires at least one issuable_tool".to_string(),
});
}
}
}
let ttl = self
.ttl
.unwrap_or_else(|| Duration::from_secs(crate::DEFAULT_WARRANT_TTL_SECS));
if ttl.as_secs() > crate::MAX_WARRANT_TTL_SECS {
return Err(Error::InvalidTtl(format!(
"TTL {} seconds exceeds protocol maximum of {} seconds ({} days)",
ttl.as_secs(),
crate::MAX_WARRANT_TTL_SECS,
crate::MAX_WARRANT_TTL_SECS / 86400
)));
}
let holder = self.holder.unwrap_or_else(|| signing_key.public_key());
let max_depth_val = self.max_depth.unwrap_or(crate::MAX_DELEGATION_DEPTH);
let max_depth_u8 = max_depth_val as u8;
if max_depth_val > crate::MAX_DELEGATION_DEPTH {
return Err(Error::DepthExceeded(
max_depth_val,
crate::MAX_DELEGATION_DEPTH,
));
}
if let Some(max_issue) = self.max_issue_depth {
if max_issue > max_depth_val {
return Err(Error::IssueDepthExceeded {
depth: max_issue,
max: max_depth_val,
});
}
}
if let (Some(approvers), Some(min)) = (&self.required_approvers, self.min_approvals) {
if min as usize > approvers.len() {
return Err(Error::MonotonicityViolation(format!(
"min_approvals ({}) cannot exceed required_approvers count ({})",
min,
approvers.len()
)));
}
}
if self
.required_approvers
.as_ref()
.map(|a| !a.is_empty())
.unwrap_or(false)
&& !self
.extensions
.contains_key(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
return Err(Error::Validation(
"required_approvers requires an approval gate map: add a tenuo.approval_gates extension \
specifying which tools need approval"
.to_string(),
));
}
if self.tools.len() > MAX_TOOLS_PER_WARRANT {
return Err(Error::Validation(format!(
"tools count {} exceeds limit {}",
self.tools.len(),
MAX_TOOLS_PER_WARRANT
)));
}
for constraints in self.tools.values() {
if constraints.len() > MAX_CONSTRAINTS_PER_TOOL {
return Err(Error::Validation(format!(
"constraints count {} exceeds limit {}",
constraints.len(),
MAX_CONSTRAINTS_PER_TOOL
)));
}
constraints.validate_depth()?;
}
if self.extensions.len() > MAX_EXTENSION_KEYS {
return Err(Error::Validation(format!(
"extensions count {} exceeds limit {}",
self.extensions.len(),
MAX_EXTENSION_KEYS
)));
}
for (key, val) in &self.extensions {
if key.len() > MAX_EXTENSION_KEY_SIZE {
return Err(Error::Validation(format!(
"extension key '{}...' length {} exceeds limit {}",
key.chars().take(KEY_DISPLAY_TRUNCATION).collect::<String>(),
key.len(),
MAX_EXTENSION_KEY_SIZE
)));
}
if val.len() > MAX_EXTENSION_VALUE_SIZE {
return Err(Error::Validation(format!(
"extension '{}' value size {} exceeds limit {}",
key,
val.len(),
MAX_EXTENSION_VALUE_SIZE
)));
}
}
if !self.constraint_bounds.is_empty() {
self.constraint_bounds.validate_depth()?;
}
let id = self.id.unwrap_or_default();
let issued_at = Utc::now().timestamp() as u64;
let expires_at = issued_at + ttl.as_secs();
let payload = WarrantPayload {
version: WARRANT_VERSION as u8,
warrant_type,
id,
tools: self.tools,
holder,
issuer: signing_key.public_key(),
issued_at,
expires_at,
max_depth: max_depth_u8,
depth: 0, parent_hash: None, extensions: self.extensions,
issuable_tools: self.issuable_tools,
max_issue_depth: self.max_issue_depth,
constraint_bounds: if self.constraint_bounds.is_empty() {
None
} else {
Some(self.constraint_bounds)
},
clearance: self.clearance,
session_id: self.session_id,
agent_id: self.agent_id,
required_approvers: self.required_approvers,
min_approvals: self.min_approvals,
};
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(1); preimage.extend_from_slice(&payload_bytes);
let signature = signing_key.sign(&preimage);
let warrant = Warrant {
payload,
signature,
payload_bytes,
envelope_version: 1,
};
log_event(AuditEvent {
id: Uuid::new_v4().to_string(),
event_type: AuditEventType::WarrantIssued,
timestamp: Utc::now(),
provider: "tenuo".to_string(),
external_id: None,
public_key_hex: Some(hex::encode(signing_key.public_key().to_bytes())),
actor: format!(
"issuer:{}",
hex::encode(&signing_key.public_key().to_bytes()[..8])
),
details: Some(format!(
"root warrant created: type={:?}, tools={:?}, depth=0",
warrant.payload.warrant_type,
warrant.tools()
)),
related_ids: Some(vec![warrant.id().to_string()]),
});
Ok(warrant)
}
}
impl Default for WarrantBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct AttenuationBuilder<'a> {
parent: &'a Warrant,
tools: BTreeMap<String, ConstraintSet>,
issuable_tools: Option<Vec<String>>,
max_issue_depth: Option<u32>,
constraint_bounds: ConstraintSet,
clearance: Option<Clearance>,
ttl: Option<Duration>,
max_depth: Option<u32>,
session_id: Option<String>,
agent_id: Option<String>,
holder: Option<PublicKey>,
required_approvers: Option<Vec<PublicKey>>,
min_approvals: Option<u32>,
extensions: BTreeMap<String, Vec<u8>>,
}
impl<'a> AttenuationBuilder<'a> {
fn new(parent: &'a Warrant) -> Self {
let (tools, issuable_tools, max_issue_depth, constraint_bounds) =
match parent.payload.warrant_type {
WarrantType::Execution => (
BTreeMap::new(),
None,
None,
ConstraintSet::new(),
),
WarrantType::Issuer => (
BTreeMap::new(),
None,
parent.payload.max_issue_depth,
parent.payload.constraint_bounds.clone().unwrap_or_default(),
),
};
let mut extensions = BTreeMap::new();
if let Some(gate_bytes) = parent
.payload
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
gate_bytes.clone(),
);
}
Self {
parent,
tools,
issuable_tools,
max_issue_depth,
constraint_bounds,
clearance: parent.payload.clearance,
ttl: None,
max_depth: None, session_id: parent.payload.session_id.clone(),
agent_id: parent.payload.agent_id.clone(),
holder: Some(parent.payload.holder.clone()),
required_approvers: parent.payload.required_approvers.clone(),
min_approvals: parent.payload.min_approvals,
extensions,
}
}
pub fn inherit_all(mut self) -> Self {
match self.parent.payload.warrant_type {
WarrantType::Execution => {
self.tools = self.parent.payload.tools.clone();
}
WarrantType::Issuer => {
self.issuable_tools = self.parent.payload.issuable_tools.clone();
}
}
self
}
pub fn tool(mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) -> Self {
self.tools.insert(tool.into(), constraints.into());
self
}
pub fn capability(
self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) -> Self {
self.tool(tool, constraints)
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn max_depth(mut self, max_depth: u32) -> Self {
self.max_depth = Some(max_depth);
self
}
pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = Some(agent_id.into());
self
}
pub fn holder(mut self, public_key: PublicKey) -> Self {
self.holder = Some(public_key);
self
}
pub fn add_approvers(mut self, approvers: Vec<PublicKey>) -> Self {
let mut current = self.required_approvers.unwrap_or_default();
for approver in approvers {
if !current.contains(&approver) {
current.push(approver);
}
}
self.required_approvers = Some(current);
self
}
pub fn raise_min_approvals(mut self, min: u32) -> Self {
let current = self.min_approvals.unwrap_or(0);
self.min_approvals = Some(min.max(current));
self
}
pub fn clearance(mut self, clearance: Clearance) -> Self {
self.clearance = Some(clearance);
self
}
pub fn extension(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
let key = key.into();
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return self;
}
self.extensions.insert(key, value);
self
}
fn validate_multisig_monotonicity(&self) -> Result<()> {
if let Some(parent_approvers) = &self.parent.payload.required_approvers {
if let Some(child_approvers) = &self.required_approvers {
for parent_key in parent_approvers {
if !child_approvers.contains(parent_key) {
return Err(Error::MonotonicityViolation(format!(
"cannot remove approver {} from multi-sig set",
hex::encode(parent_key.to_bytes())
)));
}
}
} else {
return Err(Error::MonotonicityViolation(
"cannot remove multi-sig requirement from parent".to_string(),
));
}
}
if let Some(parent_min) = self.parent.payload.min_approvals {
if let Some(child_min) = self.min_approvals {
if child_min < parent_min {
return Err(Error::MonotonicityViolation(format!(
"cannot lower min_approvals from {} to {}",
parent_min, child_min
)));
}
}
}
if let (Some(approvers), Some(min)) = (&self.required_approvers, self.min_approvals) {
if min as usize > approvers.len() {
return Err(Error::MonotonicityViolation(format!(
"min_approvals ({}) cannot exceed required_approvers count ({})",
min,
approvers.len()
)));
}
}
Ok(())
}
pub fn build(mut self, signing_key: &SigningKey) -> Result<Warrant> {
if signing_key.public_key() != *self.parent.authorized_holder() {
return Err(Error::DelegationAuthorityError {
expected: self.parent.authorized_holder().fingerprint(),
actual: signing_key.public_key().fingerprint(),
});
}
let new_depth = self.parent.depth() + 1;
if new_depth > MAX_DELEGATION_DEPTH {
return Err(Error::DepthExceeded(new_depth, MAX_DELEGATION_DEPTH));
}
let parent_max_depth = self.parent.max_depth();
let effective_max = match (parent_max_depth, self.max_depth) {
(Some(parent_max), Some(child_max)) => {
if child_max > parent_max {
return Err(Error::MonotonicityViolation(format!(
"max_depth {} exceeds parent's max_depth {}",
child_max, parent_max
)));
}
Some(child_max)
}
(Some(parent_max), None) => Some(parent_max),
(None, Some(child_max)) => {
if child_max > MAX_DELEGATION_DEPTH {
return Err(Error::DepthExceeded(child_max, MAX_DELEGATION_DEPTH));
}
Some(child_max)
}
(None, None) => None,
};
let depth_limit = effective_max.unwrap_or(MAX_DELEGATION_DEPTH);
if new_depth > depth_limit {
return Err(Error::DepthExceeded(new_depth, depth_limit));
}
if self.parent.is_expired() {
return Err(Error::WarrantExpired(self.parent.expires_at()));
}
match self.parent.payload.warrant_type {
WarrantType::Execution => {
let parent_tools = &self.parent.payload.tools;
for (tool, constraints) in &self.tools {
if let Some(parent_constraints) = parent_tools.get(tool) {
parent_constraints.validate_attenuation(constraints)?;
} else if let Some(parent_wildcard) = parent_tools.get("*") {
parent_wildcard.validate_attenuation(constraints)?;
} else {
return Err(Error::MonotonicityViolation(format!(
"tool '{}' not in parent's tools",
tool
)));
}
}
if self.tools.is_empty() {
return Err(Error::Validation(
"execution warrant must have at least one tool".to_string(),
));
}
if let Some(child_clearance) = self.clearance {
match self.parent.payload.clearance {
Some(parent_clearance) => {
if child_clearance > parent_clearance {
return Err(Error::MonotonicityViolation(format!(
"clearance cannot increase: parent {:?}, child {:?}",
parent_clearance, child_clearance
)));
}
}
None => {
return Err(Error::MonotonicityViolation(format!(
"cannot introduce clearance {:?} in child when parent has none",
child_clearance
)));
}
}
}
}
WarrantType::Issuer => {
if let Some(parent_issuable) = &self.parent.payload.issuable_tools {
if let Some(ref child_issuable) = self.issuable_tools {
for tool in child_issuable {
if !parent_issuable.contains(tool) {
return Err(Error::MonotonicityViolation(format!(
"issuable_tool '{}' not in parent's issuable_tools",
tool
)));
}
}
}
}
if let Some(parent_bounds) = &self.parent.payload.constraint_bounds {
parent_bounds.validate_attenuation(&self.constraint_bounds)?;
}
}
}
self.validate_multisig_monotonicity()?;
if let Some(gate_bytes) = self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
.cloned()
{
let parent_gates = crate::approval_gate::parse_approval_gate_map(Some(&gate_bytes))?;
if let Some(parent_gates) = parent_gates {
match crate::approval_gate::propagate_approval_gates(&parent_gates, &self.tools) {
Some(scoped) => {
let encoded = crate::approval_gate::encode_approval_gate_map(&scoped)?;
self.extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
encoded,
);
}
None => {
self.extensions
.remove(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY);
}
}
}
}
if self.extensions.len() > MAX_EXTENSION_KEYS {
return Err(Error::Validation(format!(
"extensions count {} exceeds limit {}",
self.extensions.len(),
MAX_EXTENSION_KEYS
)));
}
for (key, val) in &self.extensions {
if key.len() > MAX_EXTENSION_KEY_SIZE {
return Err(Error::Validation(format!(
"extension key '{}...' length {} exceeds limit {}",
key.chars().take(KEY_DISPLAY_TRUNCATION).collect::<String>(),
key.len(),
MAX_EXTENSION_KEY_SIZE
)));
}
if val.len() > MAX_EXTENSION_VALUE_SIZE {
return Err(Error::Validation(format!(
"extension '{}' value size {} exceeds limit {}",
key,
val.len(),
MAX_EXTENSION_VALUE_SIZE
)));
}
}
let holder = self
.holder
.ok_or_else(|| Error::Validation("holder is required".to_string()))?;
let now_sec = Utc::now().timestamp() as u64;
let expires_at = if let Some(ttl) = self.ttl {
let ttl_secs = ttl.as_secs();
let proposed = now_sec + ttl_secs;
if proposed > self.parent.payload.expires_at {
self.parent.payload.expires_at
} else {
proposed
}
} else {
self.parent.payload.expires_at
};
let effective_min = self.min_approvals.or(self.parent.payload.min_approvals);
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&self.parent.payload_bytes);
let parent_hash: [u8; 32] = hasher.finalize().into();
let payload = WarrantPayload {
version: WARRANT_VERSION as u8,
warrant_type: self.parent.payload.warrant_type,
id: WarrantId::new(),
holder,
tools: self.tools,
issuable_tools: match self.parent.payload.warrant_type {
WarrantType::Issuer => self.issuable_tools.clone(),
WarrantType::Execution => None,
},
max_issue_depth: match self.parent.payload.warrant_type {
WarrantType::Issuer => self.max_issue_depth,
WarrantType::Execution => None,
},
constraint_bounds: match self.parent.payload.warrant_type {
WarrantType::Issuer => {
if self.constraint_bounds.is_empty() {
None
} else {
Some(self.constraint_bounds)
}
}
WarrantType::Execution => None,
},
clearance: self.clearance,
issued_at: now_sec,
expires_at,
max_depth: effective_max
.map(|d| d as u8)
.unwrap_or(MAX_DELEGATION_DEPTH as u8),
depth: self.parent.depth() + 1, session_id: self.session_id,
agent_id: self.agent_id,
issuer: signing_key.public_key(),
parent_hash: Some(parent_hash),
required_approvers: self.required_approvers,
min_approvals: effective_min,
extensions: self.extensions,
};
if payload.warrant_type == WarrantType::Issuer && !payload.tools.is_empty() {
return Err(Error::InvalidWarrantType {
message: "issuer warrant cannot have tools".to_string(),
});
}
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(1); preimage.extend_from_slice(&payload_bytes);
let signature = signing_key.sign(&preimage);
let warrant = Warrant {
payload,
signature,
payload_bytes,
envelope_version: 1,
};
log_event(AuditEvent {
id: Uuid::new_v4().to_string(),
event_type: AuditEventType::WarrantIssued,
timestamp: Utc::now(),
provider: "tenuo".to_string(),
external_id: None,
public_key_hex: Some(hex::encode(signing_key.public_key().to_bytes())),
actor: format!(
"delegator:{}",
hex::encode(&signing_key.public_key().to_bytes()[..8])
),
details: Some(format!(
"warrant attenuated: type={:?}, depth={}, parent={}",
warrant.payload.warrant_type,
warrant.depth(),
self.parent.id()
)),
related_ids: Some(vec![warrant.id().to_string(), self.parent.id().to_string()]),
});
Ok(warrant)
}
}
#[derive(Debug, Clone)]
pub struct OwnedAttenuationBuilder {
parent: Warrant,
tools: BTreeMap<String, ConstraintSet>,
issuable_tools: Option<Vec<String>>,
max_issue_depth: Option<u32>,
constraint_bounds: ConstraintSet,
clearance: Option<Clearance>,
ttl: Option<Duration>,
max_depth: Option<u32>,
session_id: Option<String>,
agent_id: Option<String>,
holder: Option<PublicKey>,
required_approvers: Option<Vec<PublicKey>>,
min_approvals: Option<u32>,
intent: Option<String>,
extensions: BTreeMap<String, Vec<u8>>,
}
impl OwnedAttenuationBuilder {
pub fn new(parent: Warrant) -> Self {
let (tools, issuable_tools, max_issue_depth, constraint_bounds) =
match parent.payload.warrant_type {
WarrantType::Execution => (
BTreeMap::new(),
None,
None,
ConstraintSet::new(),
),
WarrantType::Issuer => (
BTreeMap::new(),
None,
parent.payload.max_issue_depth,
parent.payload.constraint_bounds.clone().unwrap_or_default(),
),
};
let mut extensions = BTreeMap::new();
if let Some(gate_bytes) = parent
.payload
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
gate_bytes.clone(),
);
}
Self {
clearance: parent.payload.clearance,
session_id: parent.payload.session_id.clone(),
agent_id: parent.payload.agent_id.clone(),
holder: Some(parent.payload.holder.clone()),
required_approvers: parent.payload.required_approvers.clone(),
min_approvals: parent.payload.min_approvals,
parent,
tools,
issuable_tools,
max_issue_depth,
constraint_bounds,
ttl: None,
max_depth: None,
intent: None,
extensions,
}
}
pub fn inherit_all(&mut self) {
match self.parent.payload.warrant_type {
WarrantType::Execution => {
self.tools = self.parent.payload.tools.clone();
}
WarrantType::Issuer => {
self.issuable_tools = self.parent.payload.issuable_tools.clone();
}
}
}
pub fn tools(&self) -> &BTreeMap<String, ConstraintSet> {
&self.tools
}
pub fn parent(&self) -> &Warrant {
&self.parent
}
pub fn retain_tool(&mut self, tool: &str) {
self.tools.retain(|k, _| k == tool);
}
pub fn retain_capability(&mut self, tool: &str) {
self.retain_tool(tool)
}
pub fn retain_tools(&mut self, tools: &[String]) {
self.tools.retain(|k, _| tools.contains(k));
}
pub fn retain_capabilities(&mut self, tools: &[String]) {
self.retain_tools(tools)
}
pub fn ttl_seconds(&self) -> Option<u64> {
self.ttl.map(|d| d.as_secs())
}
pub fn get_holder(&self) -> Option<&PublicKey> {
self.holder.as_ref()
}
pub fn clearance(&self) -> Option<Clearance> {
self.clearance
}
pub fn intent(&self) -> Option<&str> {
self.intent.as_deref()
}
pub fn tool(mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) -> Self {
self.tools.insert(tool.into(), constraints.into());
self
}
pub fn capability(
self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) -> Self {
self.tool(tool, constraints)
}
pub fn set_tool(&mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) {
self.tools.insert(tool.into(), constraints.into());
}
pub fn set_capability(
&mut self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) {
self.set_tool(tool, constraints)
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn set_ttl(&mut self, ttl: Duration) {
self.ttl = Some(ttl);
}
pub fn max_depth(mut self, max_depth: u32) -> Self {
self.max_depth = Some(max_depth);
self
}
pub fn set_max_depth(&mut self, max_depth: u32) {
self.max_depth = Some(max_depth);
}
pub fn terminal(mut self) -> Self {
self.max_depth = Some(self.parent.depth() + 1);
self
}
pub fn set_terminal(&mut self) {
self.max_depth = Some(self.parent.depth() + 1);
}
pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = Some(agent_id.into());
self
}
pub fn holder(mut self, public_key: PublicKey) -> Self {
self.holder = Some(public_key);
self
}
pub fn set_holder(&mut self, public_key: PublicKey) {
self.holder = Some(public_key);
}
pub fn set_clearance(&mut self, level: Clearance) {
self.clearance = Some(level);
}
pub fn set_intent(&mut self, intent: impl Into<String>) {
self.intent = Some(intent.into());
}
pub fn issuable_tool(mut self, tool: impl Into<String>) -> Self {
self.issuable_tools = Some(vec![tool.into()]);
self
}
pub fn set_issuable_tool(&mut self, tool: impl Into<String>) {
self.issuable_tools = Some(vec![tool.into()]);
}
pub fn issuable_tools(mut self, tools: Vec<String>) -> Self {
self.issuable_tools = Some(tools);
self
}
pub fn set_issuable_tools(&mut self, tools: Vec<String>) {
self.issuable_tools = Some(tools);
}
pub fn drop_issuable_tools(&mut self, tools_to_drop: Vec<String>) {
if let Some(current) = &mut self.issuable_tools {
current.retain(|t| !tools_to_drop.contains(t));
} else if let Some(parent_tools) = &self.parent.payload.issuable_tools {
let mut current = parent_tools.clone();
current.retain(|t| !tools_to_drop.contains(t));
self.issuable_tools = Some(current);
}
}
pub fn extension(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
let key = key.into();
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return self;
}
self.extensions.insert(key, value);
self
}
pub fn add_extension(&mut self, key: String, value: Vec<u8>) {
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return;
}
self.extensions.insert(key, value);
}
pub fn set_approval_gates_extension(&mut self, bytes: Vec<u8>) -> Result<()> {
let explicit = match crate::approval_gate::parse_approval_gate_map(Some(&bytes))? {
Some(g) => g,
None => return Ok(()),
};
let merged = match self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
Some(existing) => {
match crate::approval_gate::parse_approval_gate_map(Some(existing))? {
Some(inherited) => {
crate::approval_gate::merge_approval_gate_maps(&inherited, &explicit)
}
None => explicit,
}
}
None => explicit,
};
let encoded = crate::approval_gate::encode_approval_gate_map(&merged)?;
self.extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
encoded,
);
Ok(())
}
pub fn add_approvers(mut self, approvers: Vec<PublicKey>) -> Self {
let mut current = self.required_approvers.unwrap_or_default();
for approver in approvers {
if !current.contains(&approver) {
current.push(approver);
}
}
self.required_approvers = Some(current);
self
}
pub fn raise_min_approvals(mut self, min: u32) -> Self {
let current = self.min_approvals.unwrap_or(0);
self.min_approvals = Some(min.max(current));
self
}
pub fn diff(&self) -> crate::diff::DelegationDiff {
use crate::diff::{ConstraintDiff, DelegationDiff, DepthDiff, ToolsDiff, TtlDiff};
use chrono::Utc;
use std::collections::HashMap;
let parent_tools = self.parent.payload.tools.keys().cloned().collect();
let mut child_tools: Vec<String> = self.tools.keys().cloned().collect();
child_tools.sort();
let tools = ToolsDiff::new(parent_tools, child_tools.clone());
let mut capabilities: HashMap<String, HashMap<String, ConstraintDiff>> = HashMap::new();
let mut all_tools = child_tools.clone();
for tool in self.parent.payload.tools.keys() {
if !all_tools.contains(tool) {
all_tools.push(tool.clone());
}
}
all_tools.sort();
for tool in all_tools {
let parent_constraints = self
.parent
.payload
.tools
.get(&tool)
.cloned()
.unwrap_or_default();
let child_constraints = self.tools.get(&tool).cloned().unwrap_or_default();
let mut tool_diffs = HashMap::new();
let mut all_fields: Vec<String> = Vec::new();
for (field, _) in parent_constraints.iter() {
all_fields.push(field.clone());
}
for (field, _) in child_constraints.iter() {
if !all_fields.contains(field) {
all_fields.push(field.clone());
}
}
for field in all_fields {
let pc = parent_constraints.get(&field).cloned();
let cc = child_constraints.get(&field).cloned();
tool_diffs.insert(field.clone(), ConstraintDiff::new(field, pc, cc));
}
if !tool_diffs.is_empty() {
capabilities.insert(tool, tool_diffs);
}
}
let now = Utc::now();
let parent_remaining = (self.parent.expires_at() - now).num_seconds().max(0);
let child_ttl = self.ttl.map(|d| d.as_secs() as i64);
let ttl = TtlDiff::new(Some(parent_remaining), child_ttl);
let clearance = ClearanceDiff::new(self.parent.clearance(), self.clearance());
let depth = DepthDiff::new(
self.parent.depth(),
self.parent.depth() + 1,
self.parent.max_depth(),
);
DelegationDiff {
parent_warrant_id: self.parent.id().to_string(),
child_warrant_id: None, timestamp: Utc::now(),
tools,
capabilities,
ttl,
clearance,
depth,
intent: self.intent.clone(),
}
}
fn validate_multisig_monotonicity(&self) -> Result<()> {
if let Some(parent_approvers) = &self.parent.payload.required_approvers {
if let Some(child_approvers) = &self.required_approvers {
for parent_key in parent_approvers {
if !child_approvers.contains(parent_key) {
return Err(Error::MonotonicityViolation(format!(
"cannot remove approver {} from multi-sig set",
hex::encode(parent_key.to_bytes())
)));
}
}
} else {
return Err(Error::MonotonicityViolation(
"cannot remove multi-sig requirement from parent".to_string(),
));
}
}
if let Some(parent_min) = self.parent.payload.min_approvals {
if let Some(child_min) = self.min_approvals {
if child_min < parent_min {
return Err(Error::MonotonicityViolation(format!(
"cannot lower min_approvals from {} to {}",
parent_min, child_min
)));
}
}
}
if let (Some(approvers), Some(min)) = (&self.required_approvers, self.min_approvals) {
if min as usize > approvers.len() {
return Err(Error::MonotonicityViolation(format!(
"min_approvals ({}) cannot exceed required_approvers count ({})",
min,
approvers.len()
)));
}
}
Ok(())
}
pub fn build(mut self, signing_key: &SigningKey) -> Result<Warrant> {
if let Some(tools) = &mut self.issuable_tools {
tools.sort();
}
if let Some(approvers) = &mut self.required_approvers {
approvers.sort_by_key(|a| a.to_bytes());
}
if signing_key.public_key() != *self.parent.authorized_holder() {
return Err(Error::DelegationAuthorityError {
expected: self.parent.authorized_holder().fingerprint(),
actual: signing_key.public_key().fingerprint(),
});
}
let new_depth = self.parent.depth() + 1;
let parent_max = self.parent.payload.max_depth as u32;
let effective_max = match self.max_depth {
Some(child_max) => {
if child_max > parent_max {
return Err(Error::MonotonicityViolation(format!(
"max_depth {} exceeds parent's max_depth {}",
child_max, parent_max
)));
}
Some(child_max)
}
None => Some(parent_max),
};
let depth_limit = effective_max.unwrap_or(MAX_DELEGATION_DEPTH);
if new_depth > depth_limit {
return Err(Error::DepthExceeded(new_depth, depth_limit));
}
if new_depth > MAX_DELEGATION_DEPTH {
return Err(Error::DepthExceeded(new_depth, MAX_DELEGATION_DEPTH));
}
if self.parent.is_expired() {
use chrono::TimeZone;
let expiry = Utc
.timestamp_opt(self.parent.payload.expires_at as i64, 0)
.single()
.unwrap_or_else(Utc::now);
return Err(Error::WarrantExpired(expiry));
}
match self.parent.payload.warrant_type {
WarrantType::Execution => {
let parent_tools = &self.parent.payload.tools;
for (tool, constraints) in &self.tools {
if let Some(parent_constraints) = parent_tools.get(tool) {
parent_constraints.validate_attenuation(constraints)?;
} else if let Some(parent_wildcard) = parent_tools.get("*") {
parent_wildcard.validate_attenuation(constraints)?;
} else {
return Err(Error::MonotonicityViolation(format!(
"tool '{}' not in parent's tools",
tool
)));
}
}
if self.tools.is_empty() {
return Err(Error::Validation(
"execution warrant must have at least one tool".to_string(),
));
}
if let Some(child_clearance) = self.clearance {
match self.parent.payload.clearance {
Some(parent_clearance) => {
if child_clearance > parent_clearance {
return Err(Error::MonotonicityViolation(format!(
"clearance cannot increase: parent {:?}, child {:?}",
parent_clearance, child_clearance
)));
}
}
None => {
return Err(Error::MonotonicityViolation(format!(
"cannot introduce clearance {:?} in child when parent has none",
child_clearance
)));
}
}
}
}
WarrantType::Issuer => {
if let Some(parent_issuable) = &self.parent.payload.issuable_tools {
if let Some(ref child_issuable) = self.issuable_tools {
for tool in child_issuable {
if !parent_issuable.contains(tool) {
return Err(Error::MonotonicityViolation(format!(
"issuable_tool '{}' not in parent's issuable_tools",
tool
)));
}
}
}
}
if let Some(parent_bounds) = &self.parent.payload.constraint_bounds {
parent_bounds.validate_attenuation(&self.constraint_bounds)?;
}
}
}
self.validate_multisig_monotonicity()?;
if let Some(gate_bytes) = self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
.cloned()
{
let parent_gates = crate::approval_gate::parse_approval_gate_map(Some(&gate_bytes))?;
if let Some(parent_gates) = parent_gates {
match crate::approval_gate::propagate_approval_gates(&parent_gates, &self.tools) {
Some(scoped) => {
let encoded = crate::approval_gate::encode_approval_gate_map(&scoped)?;
self.extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
encoded,
);
}
None => {
self.extensions
.remove(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY);
}
}
}
}
{
let parent_gate_bytes = self
.parent
.extension(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY);
let parent_gates = crate::approval_gate::parse_approval_gate_map(parent_gate_bytes)?;
let child_gate_bytes = self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY);
let child_gates = crate::approval_gate::parse_approval_gate_map(child_gate_bytes)?;
crate::approval_gate::verify_approval_gate_monotonicity(
parent_gates.as_ref(),
child_gates.as_ref(),
&self.tools,
)
.map_err(|e| Error::Validation(format!("approval gate monotonicity violation: {e}")))?;
}
let holder = self
.holder
.ok_or_else(|| Error::Validation("holder is required".to_string()))?;
let now_sec = Utc::now().timestamp() as u64;
let expires_at = if let Some(ttl) = self.ttl {
let chrono_ttl = ChronoDuration::from_std(ttl)
.map_err(|_| Error::InvalidTtl("TTL too large".to_string()))?;
let proposed = now_sec + chrono_ttl.num_seconds() as u64;
if proposed > self.parent.payload.expires_at {
self.parent.payload.expires_at
} else {
proposed
}
} else {
self.parent.payload.expires_at
};
let effective_min = self.min_approvals.or(self.parent.payload.min_approvals);
let payload = WarrantPayload {
version: WARRANT_VERSION as u8,
warrant_type: self.parent.payload.warrant_type,
id: WarrantId::new(),
holder,
tools: self.tools,
issuable_tools: match self.parent.payload.warrant_type {
WarrantType::Issuer => self.issuable_tools.clone(),
WarrantType::Execution => None,
},
max_issue_depth: match self.parent.payload.warrant_type {
WarrantType::Issuer => self.max_issue_depth,
WarrantType::Execution => None,
},
constraint_bounds: match self.parent.payload.warrant_type {
WarrantType::Issuer => {
if self.constraint_bounds.is_empty() {
None
} else {
Some(self.constraint_bounds)
}
}
WarrantType::Execution => None,
},
clearance: self.clearance,
issued_at: now_sec,
expires_at,
max_depth: effective_max.unwrap_or(MAX_DELEGATION_DEPTH) as u8,
depth: self.parent.depth() + 1, session_id: self.session_id,
agent_id: self.agent_id,
issuer: signing_key.public_key(),
parent_hash: {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&self.parent.payload_bytes);
Some(hasher.finalize().into())
},
required_approvers: self.required_approvers,
min_approvals: effective_min,
extensions: self.extensions,
};
if payload.warrant_type == WarrantType::Issuer && !payload.tools.is_empty() {
return Err(Error::InvalidWarrantType {
message: "issuer warrant cannot have tools".to_string(),
});
}
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(1); preimage.extend_from_slice(&payload_bytes);
let signature = signing_key.sign(&preimage);
Ok(Warrant {
payload,
signature,
payload_bytes,
envelope_version: 1,
})
}
pub fn build_with_receipt(
self,
signing_key: &SigningKey,
) -> Result<(Warrant, crate::diff::DelegationReceipt)> {
let mut diff = self.diff();
let delegator_fingerprint = signing_key.public_key().fingerprint();
let delegatee_fingerprint = self
.get_holder()
.map(|h| h.fingerprint())
.unwrap_or_else(|| signing_key.public_key().fingerprint());
let child = self.build(signing_key)?;
diff.child_warrant_id = Some(child.id().to_string());
let receipt = crate::diff::DelegationReceipt::from_diff(
diff,
child.id().to_string(),
delegator_fingerprint,
delegatee_fingerprint,
);
Ok((child, receipt))
}
}
#[derive(Debug)]
pub struct IssuanceBuilder<'a> {
issuer: &'a Warrant,
tools: BTreeMap<String, ConstraintSet>,
clearance: Option<Clearance>,
ttl: Option<Duration>,
max_depth: Option<u32>,
session_id: Option<String>,
agent_id: Option<String>,
holder: Option<PublicKey>,
required_approvers: Option<Vec<PublicKey>>,
min_approvals: Option<u32>,
extensions: BTreeMap<String, Vec<u8>>,
}
impl<'a> IssuanceBuilder<'a> {
fn new(issuer: &'a Warrant) -> Self {
let mut extensions = BTreeMap::new();
if let Some(gate_bytes) = issuer
.payload
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
gate_bytes.clone(),
);
}
Self {
issuer,
tools: BTreeMap::new(),
clearance: None,
ttl: None,
max_depth: None,
session_id: issuer.payload.session_id.clone(),
agent_id: issuer.payload.agent_id.clone(),
holder: None,
required_approvers: None,
min_approvals: None,
extensions,
}
}
pub fn tool(mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) -> Self {
self.tools.insert(tool.into(), constraints.into());
self
}
pub fn capability(
self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) -> Self {
self.tool(tool, constraints)
}
pub fn clearance(mut self, clearance: Clearance) -> Self {
self.clearance = Some(clearance);
self
}
pub fn ttl(mut self, ttl: Duration) -> Self {
self.ttl = Some(ttl);
self
}
pub fn max_depth(mut self, max_depth: u32) -> Self {
self.max_depth = Some(max_depth);
self
}
pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = Some(agent_id.into());
self
}
pub fn holder(mut self, public_key: PublicKey) -> Self {
self.holder = Some(public_key);
self
}
pub fn required_approvers(mut self, approvers: Vec<PublicKey>) -> Self {
self.required_approvers = Some(approvers);
self
}
pub fn min_approvals(mut self, min: u32) -> Self {
self.min_approvals = Some(min);
self
}
pub fn extension(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
let key = key.into();
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return self;
}
self.extensions.insert(key, value);
self
}
pub fn build(mut self, signing_key: &SigningKey) -> Result<Warrant> {
if let Some(approvers) = &mut self.required_approvers {
approvers.sort_by_key(|a| a.to_bytes());
}
if signing_key.public_key() != *self.issuer.authorized_holder() {
return Err(Error::DelegationAuthorityError {
expected: self.issuer.authorized_holder().fingerprint(),
actual: signing_key.public_key().fingerprint(),
});
}
if self.issuer.is_expired() {
use chrono::TimeZone;
let expiry = Utc
.timestamp_opt(self.issuer.payload.expires_at as i64, 0)
.single()
.unwrap_or_else(Utc::now);
return Err(Error::WarrantExpired(expiry));
}
if self.tools.is_empty() {
return Err(Error::Validation(
"execution warrant requires at least one tool".to_string(),
));
}
if let Some(issuable_tools) = &self.issuer.payload.issuable_tools {
for (tool, constraints) in &self.tools {
if !issuable_tools.contains(tool) {
return Err(Error::Validation(format!(
"tool '{}' is not in issuer's issuable_tools",
tool
)));
}
if let Some(bounds) = &self.issuer.payload.constraint_bounds {
bounds.validate_attenuation(constraints)?;
}
}
} else {
return Err(Error::InvalidWarrantType {
message: "issuer warrant requires at least one issuable_tool".to_string(),
});
}
let holder = self.holder.ok_or(Error::Validation(
"execution warrant requires holder".to_string(),
))?;
let ttl = self.ttl.ok_or(Error::MissingField("ttl".to_string()))?;
if holder == self.issuer.payload.holder {
return Err(Error::SelfIssuanceProhibited {
reason: "issuer cannot grant execution warrants to themselves".to_string(),
});
}
if holder == self.issuer.payload.issuer {
return Err(Error::SelfIssuanceProhibited {
reason: "execution warrant holder cannot be the issuer warrant's issuer (issuer-holder separation required)".to_string(),
});
}
if let Some(issuable_tools) = &self.issuer.payload.issuable_tools {
for tool in self.tools.keys() {
if !issuable_tools.contains(tool) {
return Err(Error::UnauthorizedToolIssuance {
tool: tool.clone(),
allowed: issuable_tools.clone(),
});
}
}
} else {
return Err(Error::InvalidWarrantType {
message: "issuer warrant requires at least one issuable_tool".to_string(),
});
}
if let Some(clearance) = self.clearance {
match self.issuer.payload.clearance {
Some(issuer_clearance) => {
if clearance > issuer_clearance {
return Err(Error::ClearanceLevelExceeded {
requested: format!("{:?}", clearance),
limit: format!("{:?}", issuer_clearance),
});
}
}
None => {
return Err(Error::MonotonicityViolation(format!(
"cannot introduce clearance {:?} when issuer has none",
clearance
)));
}
}
}
if let Some(constraint_bounds) = &self.issuer.payload.constraint_bounds {
if !constraint_bounds.is_empty() {
for (tool, constraints) in self.tools.iter() {
for (field, constraint) in constraints.iter() {
if let Some(bound) = constraint_bounds.get(field) {
bound.validate_attenuation(constraint).map_err(|e| {
Error::Validation(format!(
"constraint for tool '{}' field '{}' exceeds issuer's constraint_bounds: {}",
tool, field, e
))
})?;
}
}
}
}
}
let new_depth = self.issuer.depth() + 1;
if let Some(max_issue_depth) = self.issuer.payload.max_issue_depth {
if new_depth > max_issue_depth {
return Err(Error::IssueDepthExceeded {
depth: new_depth,
max: max_issue_depth,
});
}
}
if let Some(max_depth) = self.max_depth {
if let Some(max_issue_depth) = self.issuer.payload.max_issue_depth {
if max_depth > max_issue_depth {
return Err(Error::IssueDepthExceeded {
depth: max_depth,
max: max_issue_depth,
});
}
}
}
if let (Some(approvers), Some(min)) = (&self.required_approvers, self.min_approvals) {
if min as usize > approvers.len() {
return Err(Error::Validation(format!(
"min_approvals ({}) cannot exceed required_approvers count ({})",
min,
approvers.len()
)));
}
}
for constraints in self.tools.values() {
constraints.validate_depth()?;
}
if let Some(gate_bytes) = self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
.cloned()
{
let parent_gates = crate::approval_gate::parse_approval_gate_map(Some(&gate_bytes))?;
if let Some(parent_gates) = parent_gates {
match crate::approval_gate::propagate_approval_gates(&parent_gates, &self.tools) {
Some(scoped) => {
let encoded = crate::approval_gate::encode_approval_gate_map(&scoped)?;
self.extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
encoded,
);
}
None => {
self.extensions
.remove(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY);
}
}
}
}
if self.extensions.len() > MAX_EXTENSION_KEYS {
return Err(Error::Validation(format!(
"extensions count {} exceeds limit {}",
self.extensions.len(),
MAX_EXTENSION_KEYS
)));
}
for (key, val) in &self.extensions {
if key.len() > MAX_EXTENSION_KEY_SIZE {
return Err(Error::Validation(format!(
"extension key '{}...' length {} exceeds limit {}",
key.chars().take(KEY_DISPLAY_TRUNCATION).collect::<String>(),
key.len(),
MAX_EXTENSION_KEY_SIZE
)));
}
if val.len() > MAX_EXTENSION_VALUE_SIZE {
return Err(Error::Validation(format!(
"extension '{}' value size {} exceeds limit {}",
key,
val.len(),
MAX_EXTENSION_VALUE_SIZE
)));
}
}
let chrono_ttl = ChronoDuration::from_std(ttl)
.map_err(|_| Error::InvalidTtl("TTL too large".to_string()))?;
let now_sec = Utc::now().timestamp() as u64;
let expires_at = now_sec + chrono_ttl.num_seconds() as u64;
let effective_max_u8 = if let Some(configured) = self.max_depth {
if configured > 255 {
return Err(Error::Validation("max_depth exceeds u8 range".to_string()));
}
configured as u8
} else {
self.issuer.payload.max_depth
};
let payload = WarrantPayload {
version: WARRANT_VERSION as u8,
warrant_type: WarrantType::Execution,
id: WarrantId::new(),
holder,
tools: self.tools,
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: self.clearance,
issued_at: now_sec,
expires_at,
max_depth: effective_max_u8,
depth: self.issuer.depth() + 1, session_id: self.session_id,
agent_id: self.agent_id,
issuer: signing_key.public_key(),
parent_hash: {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&self.issuer.payload_bytes);
Some(hasher.finalize().into())
},
required_approvers: self.required_approvers,
min_approvals: self.min_approvals,
extensions: self.extensions,
};
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(1); preimage.extend_from_slice(&payload_bytes);
let signature = signing_key.sign(&preimage);
let warrant = Warrant {
payload,
signature,
payload_bytes,
envelope_version: 1,
};
let tools_list: Vec<String> = warrant.payload.tools.keys().cloned().collect();
log_event(AuditEvent {
id: Uuid::new_v4().to_string(),
event_type: AuditEventType::WarrantIssued,
timestamp: Utc::now(),
provider: "tenuo".to_string(),
external_id: None,
public_key_hex: Some(hex::encode(signing_key.public_key().to_bytes())),
actor: format!("issuer_warrant:{}", &self.issuer.id().to_string()[..8]),
details: Some(format!(
"execution warrant issued from issuer: tools={:?}, depth={}, issuer_id={}",
tools_list,
warrant.depth(),
self.issuer.id()
)),
related_ids: Some(vec![warrant.id().to_string(), self.issuer.id().to_string()]),
});
Ok(warrant)
}
}
#[derive(Debug, Clone)]
pub struct OwnedIssuanceBuilder {
issuer: Warrant,
tools: BTreeMap<String, ConstraintSet>,
clearance: Option<Clearance>,
ttl: Option<Duration>,
max_depth: Option<u32>,
session_id: Option<String>,
agent_id: Option<String>,
holder: Option<PublicKey>,
required_approvers: Option<Vec<PublicKey>>,
min_approvals: Option<u32>,
intent: Option<String>,
extensions: BTreeMap<String, Vec<u8>>,
}
impl OwnedIssuanceBuilder {
pub fn new(issuer: Warrant) -> Self {
let mut extensions = BTreeMap::new();
if let Some(gate_bytes) = issuer
.payload
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
gate_bytes.clone(),
);
}
Self {
session_id: issuer.payload.session_id.clone(),
agent_id: issuer.payload.agent_id.clone(),
issuer,
tools: BTreeMap::new(),
clearance: None,
ttl: None,
max_depth: None,
holder: None,
required_approvers: None,
min_approvals: None,
intent: None,
extensions,
}
}
pub fn issuer(&self) -> &Warrant {
&self.issuer
}
pub fn tools(&self) -> &BTreeMap<String, ConstraintSet> {
&self.tools
}
pub fn holder(&self) -> Option<&PublicKey> {
self.holder.as_ref()
}
pub fn clearance(&self) -> Option<Clearance> {
self.clearance
}
pub fn ttl_seconds(&self) -> Option<u64> {
self.ttl.map(|d| d.as_secs())
}
pub fn max_depth(&self) -> Option<u32> {
self.max_depth
}
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub fn agent_id(&self) -> Option<&str> {
self.agent_id.as_deref()
}
pub fn min_approvals(&self) -> Option<u32> {
self.min_approvals
}
pub fn required_approvers(&self) -> Option<&[PublicKey]> {
self.required_approvers.as_deref()
}
pub fn intent(&self) -> Option<&str> {
self.intent.as_deref()
}
pub fn set_tool(&mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) {
self.tools.insert(tool.into(), constraints.into());
}
pub fn set_capability(
&mut self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) {
self.set_tool(tool, constraints)
}
pub fn tool(mut self, tool: impl Into<String>, constraints: impl Into<ConstraintSet>) -> Self {
self.tools.insert(tool.into(), constraints.into());
self
}
pub fn capability(
self,
tool: impl Into<String>,
constraints: impl Into<ConstraintSet>,
) -> Self {
self.tool(tool, constraints)
}
pub fn set_clearance(&mut self, level: Clearance) {
self.clearance = Some(level);
}
pub fn set_ttl(&mut self, ttl: Duration) {
self.ttl = Some(ttl);
}
pub fn set_max_depth(&mut self, max_depth: u32) {
self.max_depth = Some(max_depth);
}
pub fn terminal(mut self) -> Self {
self.max_depth = Some(self.issuer.depth() + 1);
self
}
pub fn set_terminal(&mut self) {
self.max_depth = Some(self.issuer.depth() + 1);
}
pub fn set_session_id(&mut self, session_id: impl Into<String>) {
self.session_id = Some(session_id.into());
}
pub fn set_agent_id(&mut self, agent_id: impl Into<String>) {
self.agent_id = Some(agent_id.into());
}
pub fn set_holder(&mut self, public_key: PublicKey) {
self.holder = Some(public_key);
}
pub fn get_holder(&self) -> Option<&PublicKey> {
self.holder.as_ref()
}
pub fn set_required_approvers(&mut self, approvers: Vec<PublicKey>) {
self.required_approvers = Some(approvers);
}
pub fn set_min_approvals(&mut self, min: u32) {
self.min_approvals = Some(min);
}
pub fn set_intent(&mut self, intent: impl Into<String>) {
self.intent = Some(intent.into());
}
pub fn add_extension(&mut self, key: String, value: Vec<u8>) {
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return;
}
self.extensions.insert(key, value);
}
pub fn set_approval_gates_extension(&mut self, bytes: Vec<u8>) -> Result<()> {
let explicit = match crate::approval_gate::parse_approval_gate_map(Some(&bytes))? {
Some(g) => g,
None => return Ok(()),
};
let merged = match self
.extensions
.get(crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY)
{
Some(existing) => {
match crate::approval_gate::parse_approval_gate_map(Some(existing))? {
Some(inherited) => {
crate::approval_gate::merge_approval_gate_maps(&inherited, &explicit)
}
None => explicit,
}
}
None => explicit,
};
let encoded = crate::approval_gate::encode_approval_gate_map(&merged)?;
self.extensions.insert(
crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY.to_string(),
encoded,
);
Ok(())
}
pub fn extension(mut self, key: impl Into<String>, value: Vec<u8>) -> Self {
let key = key.into();
if key == crate::approval_gate::APPROVAL_GATE_EXTENSION_KEY {
return self;
}
self.extensions.insert(key, value);
self
}
pub fn build(self, signing_key: &SigningKey) -> Result<Warrant> {
IssuanceBuilder {
issuer: &self.issuer,
tools: self.tools,
clearance: self.clearance,
ttl: self.ttl,
max_depth: self.max_depth,
session_id: self.session_id,
agent_id: self.agent_id,
holder: self.holder,
required_approvers: self.required_approvers,
min_approvals: self.min_approvals,
extensions: self.extensions,
}
.build(signing_key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraints::{Exact, Pattern, Range};
fn create_test_keypair() -> SigningKey {
SigningKey::generate()
}
#[test]
fn test_warrant_creation() {
let keypair = create_test_keypair();
let mut constraints = ConstraintSet::new();
constraints.insert("cluster", Pattern::new("staging-*").unwrap());
let warrant = Warrant::builder()
.tool("upgrade_cluster", constraints)
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let caps = warrant.capabilities().unwrap();
assert!(caps.contains_key("upgrade_cluster"));
assert_eq!(warrant.depth(), 0);
assert!(warrant.is_root());
assert!(!warrant.is_expired());
}
#[test]
fn test_issued_in_future_rejection() {
let keypair = create_test_keypair();
let mut warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let future = Utc::now().timestamp() as u64 + 3600;
warrant.payload.issued_at = future;
warrant.payload.expires_at = future + 300;
match warrant.validate() {
Err(Error::IssuedInFuture) => (),
res => panic!("Expected IssuedInFuture error, got {:?}", res),
}
let now = Utc::now().timestamp() as u64;
warrant.payload.issued_at = now;
warrant.payload.expires_at = now;
match warrant.validate() {
Err(Error::Validation(msg)) if msg.contains("strictly greater") => (),
res => panic!("Expected strict expiry error, got {:?}", res),
}
}
#[test]
fn test_warrant_verification() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert!(warrant.verify(&keypair.public_key()).is_ok());
let other_keypair = create_test_keypair();
assert!(warrant.verify(&other_keypair.public_key()).is_err());
}
#[test]
fn test_warrant_authorization() {
let keypair = create_test_keypair();
let mut constraints = ConstraintSet::new();
constraints.insert("cluster", Pattern::new("staging-*").unwrap());
constraints.insert("version", Pattern::new("1.28.*").unwrap());
let warrant = Warrant::builder()
.tool("upgrade_cluster", constraints)
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let mut args = HashMap::new();
args.insert(
"cluster".to_string(),
ConstraintValue::String("staging-web".to_string()),
);
args.insert(
"version".to_string(),
ConstraintValue::String("1.28.5".to_string()),
);
let pop_sig = warrant.sign(&keypair, "upgrade_cluster", &args).unwrap();
assert!(warrant
.authorize("upgrade_cluster", &args, Some(&pop_sig))
.is_ok());
assert!(warrant
.authorize("delete_cluster", &args, Some(&pop_sig))
.is_err());
let mut bad_args = args.clone();
bad_args.insert(
"cluster".to_string(),
ConstraintValue::String("production-web".to_string()),
);
assert!(warrant
.authorize("upgrade_cluster", &bad_args, Some(&pop_sig))
.is_err());
}
#[test]
fn test_attenuation_basic() {
let parent_keypair = create_test_keypair();
let _child_keypair = create_test_keypair();
let mut parent_constraints = ConstraintSet::new();
parent_constraints.insert("cluster", Pattern::new("staging-*").unwrap());
let parent = Warrant::builder()
.tool("upgrade_cluster", parent_constraints)
.ttl(Duration::from_secs(600))
.holder(parent_keypair.public_key())
.build(&parent_keypair)
.unwrap();
let mut child_constraints = ConstraintSet::new();
child_constraints.insert("cluster", Exact::new("staging-web"));
let child = parent
.attenuate()
.tool("upgrade_cluster", child_constraints)
.build(&parent_keypair) .unwrap();
assert_eq!(child.depth(), 1);
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(parent.payload_bytes());
let parent_hash: [u8; 32] = hasher.finalize().into();
assert_eq!(child.parent_hash(), Some(&parent_hash));
assert!(child.expires_at() <= parent.expires_at());
}
#[test]
fn test_attenuation_monotonicity_enforced() {
let parent_keypair = create_test_keypair();
let _child_keypair = create_test_keypair();
let mut parent_constraints = ConstraintSet::new();
parent_constraints.insert("cluster", Pattern::new("staging-*").unwrap());
let parent = Warrant::builder()
.tool("upgrade_cluster", parent_constraints)
.ttl(Duration::from_secs(600))
.holder(parent_keypair.public_key())
.build(&parent_keypair)
.unwrap();
let mut child_constraints = ConstraintSet::new();
child_constraints.insert("cluster", Pattern::new("*").unwrap());
let result = parent
.attenuate()
.tool("upgrade_cluster", child_constraints)
.build(&parent_keypair);
assert!(result.is_err());
match result.unwrap_err() {
Error::PatternExpanded {
parent: p,
child: c,
} => {
assert_eq!(p, "staging-*");
assert_eq!(c, "*");
}
e => panic!("Expected PatternExpanded, got {:?}", e),
}
}
#[test]
fn test_attenuation_ttl_cannot_exceed_parent() {
let parent_keypair = create_test_keypair();
let _child_keypair = create_test_keypair();
let parent = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(parent_keypair.public_key())
.build(&parent_keypair)
.unwrap();
let child = parent
.attenuate()
.inherit_all() .ttl(Duration::from_secs(3600))
.build(&parent_keypair) .unwrap();
assert!(child.expires_at() <= parent.expires_at());
}
#[test]
fn test_attenuation_max_depth_limit() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.max_depth(5)
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert_eq!(warrant.depth(), 0);
assert_eq!(warrant.max_depth(), Some(5));
let child = warrant.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(child.depth(), 1);
assert_eq!(child.max_depth(), Some(5));
let mut current = child;
for _ in 2..5 {
current = current.attenuate().inherit_all().build(&keypair).unwrap();
}
assert_eq!(current.depth(), 4);
let level5 = current.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(level5.depth(), 5);
assert!(
level5.is_terminal(),
"warrant at depth=max_depth should be terminal"
);
let result = level5.attenuate().inherit_all().build(&keypair);
assert!(result.is_err(), "cannot attenuate terminal warrant");
}
#[test]
fn test_attenuation_depth_limit_with_max_depth() {
let keypair = create_test_keypair();
let mut warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.max_depth(3)
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
for _ in 0..3 {
warrant = warrant.attenuate().inherit_all().build(&keypair).unwrap();
}
let result = warrant.attenuate().inherit_all().build(&keypair);
assert!(result.is_err());
match result.unwrap_err() {
Error::DepthExceeded(_, _) => {}
e => panic!("Expected DepthExceeded, got {:?}", e),
}
}
#[test]
fn test_range_constraint_attenuation() {
let parent_keypair = create_test_keypair();
let _child_keypair = create_test_keypair();
let mut parent_constraints = ConstraintSet::new();
parent_constraints.insert("amount", Range::max(10000.0).unwrap());
let parent = Warrant::builder()
.tool("transfer_funds", parent_constraints)
.ttl(Duration::from_secs(600))
.holder(parent_keypair.public_key())
.build(&parent_keypair)
.unwrap();
let mut child_constraints = ConstraintSet::new();
child_constraints.insert("amount", Range::max(5000.0).unwrap());
let child = parent
.attenuate()
.tool("transfer_funds", child_constraints)
.build(&parent_keypair); assert!(child.is_ok());
let mut invalid_constraints = ConstraintSet::new();
invalid_constraints.insert("amount", Range::max(20000.0).unwrap());
let invalid = parent
.attenuate()
.tool("transfer_funds", invalid_constraints)
.build(&parent_keypair); assert!(invalid.is_err());
}
#[test]
fn test_session_binding() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.session_id("session_123")
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert_eq!(warrant.session_id(), Some("session_123"));
let child = warrant.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(child.session_id(), Some("session_123"));
}
#[test]
fn test_pop_signature_wrong_keypair() {
let correct_keypair = create_test_keypair();
let wrong_keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(600))
.holder(correct_keypair.public_key())
.build(&correct_keypair)
.unwrap();
let mut args = HashMap::new();
args.insert(
"param".to_string(),
ConstraintValue::String("value".to_string()),
);
let wrong_pop_sig = warrant.sign(&wrong_keypair, "test", &args).unwrap();
assert!(warrant
.authorize("test", &args, Some(&wrong_pop_sig))
.is_err());
}
#[test]
fn test_pop_signature_wrong_tool() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test_tool", ConstraintSet::new())
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let mut args = HashMap::new();
args.insert(
"param".to_string(),
ConstraintValue::String("value".to_string()),
);
let pop_sig = warrant.sign(&keypair, "wrong_tool", &args).unwrap();
assert!(warrant
.authorize("test_tool", &args, Some(&pop_sig))
.is_err());
}
#[test]
fn test_pop_signature_wrong_args() {
let keypair = create_test_keypair();
let mut constraints = ConstraintSet::new();
constraints.insert("cluster", Pattern::new("staging-*").unwrap());
let warrant = Warrant::builder()
.tool("test", constraints)
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let mut correct_args = HashMap::new();
correct_args.insert(
"cluster".to_string(),
ConstraintValue::String("staging-web".to_string()),
);
let mut wrong_args = HashMap::new();
wrong_args.insert(
"cluster".to_string(),
ConstraintValue::String("prod-web".to_string()),
);
let pop_sig = warrant.sign(&keypair, "test", &wrong_args).unwrap();
assert!(warrant
.authorize("test", &correct_args, Some(&pop_sig))
.is_err());
}
#[test]
fn test_pop_signature_expired_warrant() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(1)) .holder(keypair.public_key())
.build(&keypair)
.unwrap();
let args = HashMap::new();
let pop_sig = warrant.sign(&keypair, "test", &args).unwrap();
std::thread::sleep(Duration::from_secs(2));
assert!(warrant.authorize("test", &args, Some(&pop_sig)).is_err());
}
#[test]
fn test_pop_signature_no_signature() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let args = HashMap::new();
assert!(warrant.authorize("test", &args, None).is_err());
}
#[test]
fn test_warrant_id_format() {
let id = WarrantId::new();
assert!(id.to_string().starts_with("tnu_wrt_"));
assert_eq!(id.to_hex().len(), 32);
let parsed = WarrantId::from_string(id.to_string()).unwrap();
assert_eq!(parsed, id);
let parsed_hex = WarrantId::from_string(id.to_hex()).unwrap();
assert_eq!(parsed_hex, id);
let invalid = WarrantId::from_string("invalid_id");
assert!(invalid.is_err());
}
#[test]
fn test_max_depth_policy_limit() {
let keypair = create_test_keypair();
let root = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.max_depth(3)
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert_eq!(root.max_depth(), Some(3));
assert_eq!(root.effective_max_depth(), 3);
let level1 = root.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(level1.depth(), 1);
assert_eq!(level1.max_depth(), Some(3));
let level2 = level1.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(level2.depth(), 2);
let level3 = level2.attenuate().inherit_all().build(&keypair).unwrap();
assert_eq!(level3.depth(), 3);
let result = level3.attenuate().inherit_all().build(&keypair);
assert!(result.is_err());
match result.unwrap_err() {
Error::DepthExceeded(4, 3) => {}
e => panic!("Expected DepthExceeded(4, 3), got {:?}", e),
}
}
#[test]
fn test_max_depth_monotonicity() {
let keypair = create_test_keypair();
let root = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.max_depth(5)
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let child = root
.attenuate()
.inherit_all()
.max_depth(3)
.build(&keypair)
.unwrap();
assert_eq!(child.max_depth(), Some(3));
let result = child
.attenuate()
.inherit_all()
.max_depth(10)
.build(&keypair);
assert!(result.is_err());
match result.unwrap_err() {
Error::MonotonicityViolation(_) => {}
e => panic!("Expected MonotonicityViolation, got {:?}", e),
}
}
#[test]
fn test_max_depth_protocol_cap() {
let keypair = create_test_keypair();
let result = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.max_depth(100) .holder(keypair.public_key())
.build(&keypair);
assert!(result.is_err());
match result.unwrap_err() {
Error::DepthExceeded(100, max) if max == crate::MAX_DELEGATION_DEPTH => {}
e => panic!(
"Expected DepthExceeded(100, {}), got {:?}",
crate::MAX_DELEGATION_DEPTH,
e
),
}
}
#[test]
fn test_no_max_depth_uses_protocol_default() {
let keypair = create_test_keypair();
let root = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert_eq!(root.max_depth(), Some(MAX_DELEGATION_DEPTH));
assert_eq!(root.effective_max_depth(), MAX_DELEGATION_DEPTH);
}
#[test]
fn test_multisig_root_warrant() {
use crate::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let issuer = create_test_keypair();
let approver1 = create_test_keypair();
let approver2 = create_test_keypair();
let mut gates = ApprovalGateMap::new();
gates.insert("sensitive_action".into(), ToolApprovalGate::whole_tool());
let warrant = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key(), approver2.public_key()])
.min_approvals(2)
.holder(issuer.public_key())
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&issuer)
.unwrap();
assert!(warrant.requires_multisig());
assert_eq!(warrant.approval_threshold(), 2);
assert_eq!(warrant.required_approvers().unwrap().len(), 2);
}
#[test]
fn test_multisig_default_all_must_sign() {
use crate::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let issuer = create_test_keypair();
let approver1 = create_test_keypair();
let approver2 = create_test_keypair();
let mut gates = ApprovalGateMap::new();
gates.insert("sensitive_action".into(), ToolApprovalGate::whole_tool());
let warrant = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key(), approver2.public_key()])
.holder(issuer.public_key())
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&issuer)
.unwrap();
assert!(warrant.requires_multisig());
assert_eq!(warrant.approval_threshold(), 2); }
#[test]
fn test_multisig_min_approvals_exceeds_approvers() {
let issuer = create_test_keypair();
let approver1 = create_test_keypair();
let result = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key()])
.min_approvals(3)
.holder(issuer.public_key())
.build(&issuer);
assert!(result.is_err());
}
#[test]
fn test_multisig_attenuation_add_approvers() {
use crate::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let issuer = create_test_keypair();
let delegator = create_test_keypair();
let approver1 = create_test_keypair();
let approver2 = create_test_keypair();
let mut gates = ApprovalGateMap::new();
gates.insert("sensitive_action".into(), ToolApprovalGate::whole_tool());
let root = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key()])
.min_approvals(1)
.holder(issuer.public_key())
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&issuer)
.unwrap();
let child = root
.attenuate()
.inherit_all()
.add_approvers(vec![approver2.public_key()])
.raise_min_approvals(2)
.holder(delegator.public_key())
.build(&issuer) .unwrap();
assert_eq!(child.required_approvers().unwrap().len(), 2);
assert_eq!(child.approval_threshold(), 2);
}
#[test]
fn test_multisig_attenuation_cannot_remove_approvers() {
use crate::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let issuer = create_test_keypair();
let delegatee = create_test_keypair();
let approver1 = create_test_keypair();
let approver2 = create_test_keypair();
let mut gates = ApprovalGateMap::new();
gates.insert("sensitive_action".into(), ToolApprovalGate::whole_tool());
let root = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key(), approver2.public_key()])
.min_approvals(1)
.holder(issuer.public_key())
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&issuer)
.unwrap();
let child = root
.attenuate()
.inherit_all()
.holder(delegatee.public_key())
.build(&issuer) .unwrap();
assert_eq!(child.required_approvers().unwrap().len(), 2);
}
#[test]
fn test_multisig_attenuation_cannot_lower_threshold() {
use crate::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let issuer = create_test_keypair();
let delegatee = create_test_keypair();
let approver1 = create_test_keypair();
let approver2 = create_test_keypair();
let mut gates = ApprovalGateMap::new();
gates.insert("sensitive_action".into(), ToolApprovalGate::whole_tool());
let root = Warrant::builder()
.tool("sensitive_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.required_approvers(vec![approver1.public_key(), approver2.public_key()])
.min_approvals(2)
.holder(issuer.public_key())
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&issuer)
.unwrap();
let child = root
.attenuate()
.inherit_all()
.raise_min_approvals(1) .holder(delegatee.public_key())
.build(&issuer) .unwrap();
assert_eq!(child.approval_threshold(), 2);
}
#[test]
fn test_no_multisig_by_default() {
let keypair = create_test_keypair();
let warrant = Warrant::builder()
.tool("regular_action", ConstraintSet::new())
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
assert!(!warrant.requires_multisig());
assert_eq!(warrant.approval_threshold(), 0);
assert!(warrant.required_approvers().is_none());
}
#[test]
fn test_issuer_warrant_issues_execution_warrant() {
let issuer_kp = create_test_keypair();
let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string(), "send_email".to_string()])
.clearance(Clearance::INTERNAL)
.max_issue_depth(2)
.constraint_bound("path", Pattern::new("/data/*").unwrap())
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let mut constraints = ConstraintSet::new();
constraints.insert("path", Pattern::new("/data/q3.pdf").unwrap());
let execution_warrant = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", constraints)
.clearance(Clearance::EXTERNAL)
.ttl(Duration::from_secs(60))
.holder(holder_kp.public_key())
.build(&issuer_kp) .unwrap();
assert_eq!(execution_warrant.r#type(), WarrantType::Execution);
let caps = execution_warrant.capabilities().unwrap();
assert!(caps.contains_key("read_file"));
assert_eq!(
execution_warrant.authorized_holder(),
&holder_kp.public_key()
);
assert_eq!(execution_warrant.depth(), 1);
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(issuer_warrant.payload_bytes());
let parent_hash: [u8; 32] = hasher.finalize().into();
assert_eq!(execution_warrant.parent_hash(), Some(&parent_hash));
}
#[test]
fn test_issuer_cannot_issue_to_self() {
let issuer_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::INTERNAL)
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(issuer_kp.public_key()) .build(&issuer_kp);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("self-issuance prohibited"),
"Expected self-issuance error, got: {}",
err
);
}
#[test]
fn test_issuer_holder_separation() {
let issuer_kp = create_test_keypair(); let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::INTERNAL)
.ttl(Duration::from_secs(3600))
.holder(holder_kp.public_key())
.build(&issuer_kp)
.unwrap();
let result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(issuer_kp.public_key()) .build(&holder_kp);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("issuer-holder separation required"),
"Expected issuer-holder separation error, got: {}",
err
);
}
#[test]
fn test_holder_cycling_allowed_with_monotonicity() {
let keypair_a = create_test_keypair();
let keypair_b = create_test_keypair();
let mut root_constraints = ConstraintSet::new();
root_constraints.insert("env", Pattern::new("*").unwrap());
let root = Warrant::builder()
.tool("test", root_constraints)
.ttl(Duration::from_secs(3600))
.holder(keypair_a.public_key())
.build(&keypair_a)
.unwrap();
let mut child_b_constraints = ConstraintSet::new();
child_b_constraints.insert("env", Pattern::new("staging-*").unwrap());
let child_b = root
.attenuate()
.tool("test", child_b_constraints)
.holder(keypair_b.public_key())
.build(&keypair_a) .unwrap();
assert_eq!(child_b.depth(), 1);
let result = child_b
.attenuate()
.inherit_all()
.holder(keypair_a.public_key())
.build(&keypair_b);
assert!(result.is_ok());
let child_a = result.unwrap();
assert_eq!(child_a.depth(), 2);
}
#[test]
fn test_issuance_validates_tool_in_issuable_tools() {
let issuer_kp = create_test_keypair();
let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::INTERNAL)
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("send_email", ConstraintSet::new()) .ttl(Duration::from_secs(60))
.holder(holder_kp.public_key())
.build(&issuer_kp);
let err = result.expect_err("expected unauthorized tool issuance error");
let msg = err.to_string();
assert!(
msg.contains("unauthorized tool issuance")
|| msg.contains("not in issuer's issuable_tools"),
"unexpected error: {msg}"
);
}
#[test]
fn test_issuance_validates_clearance() {
let issuer_kp = create_test_keypair();
let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::EXTERNAL)
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", ConstraintSet::new())
.clearance(Clearance::INTERNAL) .ttl(Duration::from_secs(60))
.holder(holder_kp.public_key())
.build(&issuer_kp);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("clearance level exceeded"));
}
#[test]
fn test_issuance_validates_constraint_bounds() {
let issuer_kp = create_test_keypair();
let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::INTERNAL)
.constraint_bound("path", Pattern::new("/data/*").unwrap())
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let mut constraints = ConstraintSet::new();
constraints.insert("path", Pattern::new("/etc/*").unwrap()); let result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", constraints)
.ttl(Duration::from_secs(60))
.holder(holder_kp.public_key())
.build(&issuer_kp);
match result {
Err(Error::PatternExpanded { parent, child }) => {
assert_eq!(parent, "/data/*");
assert_eq!(child, "/etc/*");
}
Err(other) => panic!("unexpected error: {:?}", other),
Ok(_) => panic!("expected constraint bounds violation"),
}
}
#[test]
fn test_issuance_validates_max_issue_depth() {
let issuer_kp = create_test_keypair();
let holder_kp = create_test_keypair();
let issuer_warrant = Warrant::builder()
.r#type(WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.clearance(Clearance::INTERNAL)
.max_issue_depth(1) .ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let exec1 = issuer_warrant
.issue_execution_warrant()
.unwrap()
.tool("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(holder_kp.public_key())
.build(&issuer_kp)
.unwrap();
assert_eq!(exec1.depth(), 1);
}
#[test]
fn test_execution_warrant_cannot_issue() {
let issuer_kp = create_test_keypair();
let execution_warrant = Warrant::builder()
.r#type(WarrantType::Execution)
.tool("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.holder(issuer_kp.public_key())
.build(&issuer_kp)
.unwrap();
let result = execution_warrant.issue_execution_warrant();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("can only issue execution warrants from issuer warrants"));
}
#[test]
fn test_clearance_ordering() {
assert!(Clearance::SYSTEM > Clearance::PRIVILEGED);
assert!(Clearance::PRIVILEGED > Clearance::INTERNAL);
assert!(Clearance::INTERNAL > Clearance::PARTNER);
assert!(Clearance::PARTNER > Clearance::EXTERNAL);
assert!(Clearance::EXTERNAL > Clearance::UNTRUSTED);
assert_eq!(Clearance::UNTRUSTED.0, 0);
assert_eq!(Clearance::EXTERNAL.0, 10);
assert_eq!(Clearance::PARTNER.0, 20);
assert_eq!(Clearance::INTERNAL.0, 30);
assert_eq!(Clearance::PRIVILEGED.0, 40);
assert_eq!(Clearance::SYSTEM.0, 50);
}
#[test]
fn test_effective_max_depth_latching() {
let root_kp = create_test_keypair();
let root = Warrant::builder()
.tool("test", ConstraintSet::new())
.ttl(Duration::from_secs(3600))
.holder(root_kp.public_key())
.build(&root_kp)
.unwrap();
assert_eq!(root.effective_max_depth(), MAX_DELEGATION_DEPTH);
let child = root
.attenuate()
.inherit_all()
.max_depth(5)
.build(&root_kp)
.unwrap();
assert_eq!(child.effective_max_depth(), 5);
let result = child
.attenuate()
.inherit_all()
.max_depth(10)
.build(&root_kp);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
Error::MonotonicityViolation(_) | Error::Validation(_) => {}
_ => panic!(
"Expected monitoring violation or validation error, got {:?}",
err
),
}
let grandchild = child
.attenuate()
.inherit_all()
.max_depth(3)
.build(&root_kp)
.unwrap();
assert_eq!(grandchild.effective_max_depth(), 3);
}
}