wasm-capability-contract 0.3.0

Generic, domain-agnostic capability pattern: CapabilityEngine/CapabilityRegistry/CapabilityDispatcher trait shapes + component/capability types. Trait definitions only -- see wasm-capability-core for this pattern's own default implementation, extracted from agent-runtime's ADR-001 (agent-runtime#31, ADR-011).
Documentation
//! Integration-level checks spanning this crate's whole public surface —
//! proves the pieces compose the way ADR-001's own manifest example
//! claims, not just that each type works in isolation.

use wasm_capability_contract::{CapabilityGrant, CapabilityScope, EgressIdentity};

/// ADR-001's own manifest example, `granted_capabilities` array, all six
/// capabilities granted — verbatim from the ADR.
const GRANTED_CAPABILITIES_JSON: &str = r#"[
    { "name": "http-egress", "scope": { "type": "http", "allowed_hosts": ["api.example.com"],
        "identity": { "type": "sst_m2m", "client_id": "agent-runtime-invoice-processor" } } },
    { "name": "grpc-egress", "scope": { "type": "grpc", "allowed_methods": ["inventory.Inventory/Get"],
        "identity": { "type": "sst_m2m", "client_id": "agent-runtime-invoice-processor" } } },
    { "name": "llm-complete", "scope": { "type": "complete", "allowed_models": ["claude-sonnet-5"] } },
    { "name": "mcp-egress", "scope": { "type": "mcp", "allowed_tools": ["search-docs"] } },
    { "name": "database", "scope": { "type": "database", "allowed_queries": ["get_invoice_by_id"] } },
    { "name": "secrets", "scope": { "type": "secrets", "allowed_secrets": ["invoice-api-key"] } }
]"#;

/// @covers: CapabilityGrant, CapabilityScope, EgressIdentity
/// ADR-001's own manifest example must deserialize into exactly six
/// grants, each with the field values the ADR documents — field-by-field,
/// not just "it parsed."
#[test]
fn test_adr_001_manifest_example_deserializes_into_six_grants_with_correct_fields() {
    let grants: Vec<CapabilityGrant> = serde_json::from_str(GRANTED_CAPABILITIES_JSON)
        .unwrap_or_else(|e| panic!("ADR-001's own manifest example must deserialize: {e}"));
    assert_eq!(grants.len(), 6, "expected exactly six grants");

    let by_name = |name: &str| {
        grants
            .iter()
            .find(|g| g.name == name)
            .unwrap_or_else(|| panic!("expected a grant named '{name}'"))
    };

    match &by_name("http-egress").scope {
        CapabilityScope::Http {
            allowed_hosts,
            identity,
        } => {
            assert_eq!(allowed_hosts, &["api.example.com".to_string()]);
            match identity {
                EgressIdentity::SstM2m { client_id } => {
                    assert_eq!(client_id, "agent-runtime-invoice-processor");
                }
                other => panic!("expected SstM2m identity, got {other:?}"),
            }
        }
        other => panic!("expected Http scope, got {other:?}"),
    }

    match &by_name("grpc-egress").scope {
        CapabilityScope::Grpc {
            allowed_methods, ..
        } => {
            assert_eq!(allowed_methods, &["inventory.Inventory/Get".to_string()]);
        }
        other => panic!("expected Grpc scope, got {other:?}"),
    }

    match &by_name("llm-complete").scope {
        CapabilityScope::Complete { allowed_models } => {
            assert_eq!(allowed_models, &["claude-sonnet-5".to_string()]);
        }
        other => panic!("expected Complete scope, got {other:?}"),
    }

    match &by_name("mcp-egress").scope {
        CapabilityScope::Mcp { allowed_tools } => {
            assert_eq!(allowed_tools, &["search-docs".to_string()]);
        }
        other => panic!("expected Mcp scope, got {other:?}"),
    }

    match &by_name("database").scope {
        CapabilityScope::Database { allowed_queries } => {
            assert_eq!(allowed_queries, &["get_invoice_by_id".to_string()]);
        }
        other => panic!("expected Database scope, got {other:?}"),
    }

    match &by_name("secrets").scope {
        CapabilityScope::Secrets { allowed_secrets } => {
            assert_eq!(allowed_secrets, &["invoice-api-key".to_string()]);
        }
        other => panic!("expected Secrets scope, got {other:?}"),
    }
}

/// @covers: CapabilityGrant
/// The full grant list must round-trip through JSON unchanged as a whole
/// collection, not just member-by-member — proves `Vec<CapabilityGrant>`
/// itself (the exact shape `HostManifest.granted_capabilities` uses)
/// serializes losslessly.
#[test]
fn test_full_granted_capabilities_list_round_trips_unchanged() {
    let grants: Vec<CapabilityGrant> = serde_json::from_str(GRANTED_CAPABILITIES_JSON)
        .unwrap_or_else(|e| panic!("must deserialize: {e}"));
    let json = serde_json::to_string(&grants).unwrap_or_else(|e| panic!("must serialize: {e}"));
    let restored: Vec<CapabilityGrant> =
        serde_json::from_str(&json).unwrap_or_else(|e| panic!("must re-deserialize: {e}"));
    assert_eq!(restored, grants);
}