tenuo 0.1.0-beta.23

Agent Capability Flow Control - Rust core library
Documentation
use proptest::prelude::*;
use rand::seq::SliceRandom;
use std::collections::{BTreeMap, HashMap};
use std::thread;
use std::time::Duration;
use tenuo::approval::compute_request_hash;
use tenuo::approval_gate::{
    evaluate_approval_gates, ApprovalGateMap, ArgApprovalGate, ToolApprovalGate,
};
use tenuo::{Authorizer, ConstraintSet, ConstraintValue, SigningKey, Warrant};

const SHUFFLE_RUNS: usize = 16;

fn scalar_value_strategy() -> impl Strategy<Value = ConstraintValue> {
    prop_oneof![
        any::<bool>().prop_map(ConstraintValue::Boolean),
        (-100_000i64..=100_000i64).prop_map(ConstraintValue::Integer),
        (-10_000.0f64..10_000.0f64)
            .prop_filter("finite float", |f| f.is_finite())
            .prop_map(ConstraintValue::Float),
        "[a-zA-Z0-9_\\-]{0,16}".prop_map(ConstraintValue::String),
    ]
}

fn value_strategy() -> impl Strategy<Value = ConstraintValue> {
    let list_item = scalar_value_strategy();
    prop_oneof![
        scalar_value_strategy(),
        prop::collection::vec(list_item, 0..=4).prop_map(ConstraintValue::List),
    ]
}

fn args_strategy() -> impl Strategy<Value = BTreeMap<String, ConstraintValue>> {
    prop::collection::btree_map("[a-z][a-z0-9_]{0,10}", value_strategy(), 0..=10)
}

fn shuffled_hashmaps(
    args: &BTreeMap<String, ConstraintValue>,
    count: usize,
) -> Vec<HashMap<String, ConstraintValue>> {
    let entries: Vec<(String, ConstraintValue)> =
        args.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
    let mut out = Vec::with_capacity(count);
    for _ in 0..count {
        let mut shuffled = entries.clone();
        shuffled.shuffle(&mut rand::rng());
        let mut map = HashMap::with_capacity(shuffled.len());
        for (k, v) in shuffled {
            map.insert(k, v);
        }
        out.push(map);
    }
    out
}

fn assert_all_equal<T: PartialEq + std::fmt::Debug>(items: &[T], label: &str) {
    assert!(!items.is_empty(), "{label}: expected non-empty output");
    for i in 1..items.len() {
        assert_eq!(items[i], items[0], "{label}: output differs at index {i}");
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(48))]
    #[test]
    fn parallel_shuffled_maps_produce_identical_outputs(
        args in args_strategy(),
        timestamp in 1_700_000_000i64..1_900_000_000i64,
    ) {
        let issuer = SigningKey::generate();
        let holder = SigningKey::generate();
        let tool_name = "determinism.tool";

        let warrant = Warrant::builder()
            .capability(tool_name, ConstraintSet::new())
            .ttl(Duration::from_secs(600))
            .holder(holder.public_key())
            .build(&issuer)
            .expect("warrant should build");

        let mut gated_args = BTreeMap::new();
        gated_args.insert("k0".to_string(), ArgApprovalGate::All);
        let approval_gate_map = ApprovalGateMap(BTreeMap::from([(
            tool_name.to_string(),
            ToolApprovalGate::with_args(gated_args),
        )]));

        let sign_outputs = thread::scope(|scope| {
            let shuffled = shuffled_hashmaps(&args, SHUFFLE_RUNS);
            let mut handles = Vec::with_capacity(SHUFFLE_RUNS);
            for map in shuffled {
                let warrant = &warrant;
                let holder = &holder;
                handles.push(scope.spawn(move || {
                    warrant
                        .sign_with_timestamp(holder, tool_name, &map, Some(timestamp))
                        .expect("sign should succeed")
                        .to_bytes()
                        .to_vec()
                }));
            }
            handles.into_iter().map(|h| h.join().expect("join")).collect::<Vec<_>>()
        });
        assert_all_equal(&sign_outputs, "sign");

        let dedup_outputs = thread::scope(|scope| {
            let shuffled = shuffled_hashmaps(&args, SHUFFLE_RUNS);
            let mut handles = Vec::with_capacity(SHUFFLE_RUNS);
            for map in shuffled {
                let warrant = &warrant;
                handles.push(scope.spawn(move || warrant.dedup_key(tool_name, &map)));
            }
            handles.into_iter().map(|h| h.join().expect("join")).collect::<Vec<_>>()
        });
        assert_all_equal(&dedup_outputs, "dedup_key");

        let request_hash_outputs = thread::scope(|scope| {
            let shuffled = shuffled_hashmaps(&args, SHUFFLE_RUNS);
            let mut handles = Vec::with_capacity(SHUFFLE_RUNS);
            for map in shuffled {
                let warrant = &warrant;
                let holder_pk = holder.public_key();
                handles.push(scope.spawn(move || {
                    compute_request_hash(
                        &warrant.id().to_string(),
                        tool_name,
                        &map,
                        Some(&holder_pk),
                    )
                    .to_vec()
                }));
            }
            handles.into_iter().map(|h| h.join().expect("join")).collect::<Vec<_>>()
        });
        assert_all_equal(&request_hash_outputs, "compute_request_hash");

        let approval_gate_outputs = thread::scope(|scope| {
            let shuffled = shuffled_hashmaps(&args, SHUFFLE_RUNS);
            let mut handles = Vec::with_capacity(SHUFFLE_RUNS);
            for map in shuffled {
                let approval_gate_map = &approval_gate_map;
                handles.push(scope.spawn(move || {
                    evaluate_approval_gates(Some(approval_gate_map), tool_name, &map)
                        .expect("approval gate evaluation should succeed")
                }));
            }
            handles.into_iter().map(|h| h.join().expect("join")).collect::<Vec<_>>()
        });
        assert_all_equal(&approval_gate_outputs, "evaluate_approval_gates");
    }
}

#[test]
fn parallel_check_chain_with_pop_args_serializes_identically() {
    let issuer = SigningKey::generate();
    let holder = SigningKey::generate();
    let tool_name = "determinism.chain";
    let timestamp = chrono::Utc::now().timestamp();

    let warrant = Warrant::builder()
        .capability(tool_name, ConstraintSet::new())
        .ttl(Duration::from_secs(600))
        .holder(holder.public_key())
        .build(&issuer)
        .expect("warrant should build");
    let chain = vec![warrant.clone()];

    let mut authorizer = Authorizer::new();
    authorizer.add_trusted_root(issuer.public_key());

    let args = BTreeMap::from([
        ("a".to_string(), ConstraintValue::Integer(1)),
        ("b".to_string(), ConstraintValue::String("x".to_string())),
        ("c".to_string(), ConstraintValue::Boolean(true)),
    ]);

    let outputs = thread::scope(|scope| {
        let shuffled = shuffled_hashmaps(&args, SHUFFLE_RUNS);
        let mut handles = Vec::with_capacity(SHUFFLE_RUNS);
        for map in shuffled {
            let warrant = &warrant;
            let holder = &holder;
            let authorizer = &authorizer;
            let chain = &chain;
            handles.push(scope.spawn(move || {
                let signature = warrant
                    .sign_with_timestamp(holder, tool_name, &map, Some(timestamp))
                    .expect("sign should succeed");

                let result = authorizer
                    .check_chain_with_pop_args(chain, tool_name, &map, &map, Some(&signature), &[])
                    .expect("authorization should succeed");

                let mut bytes = Vec::new();
                ciborium::ser::into_writer(&result, &mut bytes)
                    .expect("result serialization should succeed");
                bytes
            }));
        }
        handles
            .into_iter()
            .map(|h| h.join().expect("join"))
            .collect::<Vec<_>>()
    });

    assert_all_equal(&outputs, "check_chain_with_pop_args result serialization");
}