typesec-agent 0.11.0

Agent executor with typestate and capability-based access control
Documentation
use super::*;
use std::sync::Arc;
use typesec_core::{
    CanExecute, CanRead, ResourceId, SubjectId,
    policy::{PolicyEngine, PolicyResult},
    resource::GenericResource,
    typestate::Credentials,
};

struct AllowAll;
impl PolicyEngine for AllowAll {
    fn check(&self, _: &SubjectId, _: &str, _: &ResourceId) -> PolicyResult {
        PolicyResult::Allow
    }
}

fn no_op_tool(_resource: &GenericResource) -> ToolFuture<'_> {
    Box::pin(async { Ok(()) })
}

#[tokio::test]
async fn protected_tool_invokes_with_capability() {
    let agent = SecureAgent::new(Arc::new(AllowAll))
        .authenticate_unverified(Credentials::new("agent:test", "tok"))
        .expect("auth ok");
    let resource = GenericResource::new("Gmail.ListEmails", "tool");
    let cap: Capability<CanExecute, GenericResource> =
        agent.request_capability(&resource).await.expect("cap ok");
    let tool = ProtectedTool::<CanExecute, _, _>::new(
        "gmail.list",
        "List email messages",
        resource,
        no_op_tool,
    );

    assert_eq!(tool.spec().required_permission, "execute");
    tool.invoke(&agent, &cap).await.expect("tool should run");
}

#[tokio::test]
async fn protected_tool_rejects_capability_for_other_resource() {
    let agent = SecureAgent::new(Arc::new(AllowAll))
        .authenticate_unverified(Credentials::new("agent:test", "tok"))
        .expect("auth ok");
    let cap_resource = GenericResource::new("Gmail.ListEmails", "tool");
    let tool_resource = GenericResource::new("Gmail.DeleteEmail", "tool");
    let cap: Capability<CanExecute, GenericResource> = agent
        .request_capability(&cap_resource)
        .await
        .expect("cap ok");
    let tool = ProtectedTool::<CanExecute, _, _>::new(
        "gmail.delete",
        "Delete an email message",
        tool_resource,
        no_op_tool,
    );

    assert!(matches!(
        tool.invoke(&agent, &cap).await,
        Err(TaskError::CapabilityMismatch(reason)) if reason.contains("Gmail.ListEmails")
    ));
}

#[tokio::test]
async fn registry_lists_and_invokes_registered_tools() {
    let agent = SecureAgent::new(Arc::new(AllowAll))
        .authenticate_unverified(Credentials::new("agent:test", "tok"))
        .expect("auth ok");
    let resource = GenericResource::new("Gmail.ListEmails", "tool");
    let cap: Capability<CanExecute, GenericResource> =
        agent.request_capability(&resource).await.expect("cap ok");
    let mut registry = ToolRegistry::new();
    registry.register(ProtectedTool::<CanExecute, _, _>::new(
        "gmail.list",
        "List email messages",
        resource,
        no_op_tool,
    ));

    let specs = registry.list_specs();
    assert_eq!(specs.len(), 1);
    assert_eq!(specs[0].name, "gmail.list");
    assert_eq!(
        registry
            .spec("gmail.list")
            .expect("registered spec")
            .required_permission,
        "execute"
    );

    registry
        .invoke("gmail.list", &agent, &cap)
        .await
        .expect("registry invoke should run");
}

#[tokio::test]
async fn registry_rejects_wrong_capability_type_and_unknown_tool() {
    let agent = SecureAgent::new(Arc::new(AllowAll))
        .authenticate_unverified(Credentials::new("agent:test", "tok"))
        .expect("auth ok");
    let resource = GenericResource::new("Gmail.ListEmails", "tool");
    let read_cap: Capability<CanRead, GenericResource> =
        agent.request_capability(&resource).await.expect("cap ok");
    let mut registry = ToolRegistry::new();
    registry.register(ProtectedTool::<CanExecute, _, _>::new(
        "gmail.list",
        "List email messages",
        resource,
        no_op_tool,
    ));

    assert!(matches!(
        registry.invoke("gmail.list", &agent, &read_cap).await,
        Err(TaskError::CapabilityMismatch(reason)) if reason.contains("Capability<execute")
    ));
    assert!(matches!(
        registry.invoke("missing", &agent, &read_cap).await,
        Err(TaskError::UnknownTool(name)) if name == "missing"
    ));
}