delegated 0.1.1

Fail-closed trust evaluation for agentic AI systems — delegation tokens, policy enforcement, and audit for agent-to-agent and human-to-agent workflows.
Documentation

delegated

Fail-closed trust evaluation for agentic AI systems.

delegated verifies delegation tokens and enforces policy on every agent action — before your tools, APIs, or downstream agents run anything. Drop it in as Tower middleware, call the standalone adapters, or use the client SDK to attach trust claims to outbound requests.

crates.io docs.rs

What it does

  • Verifies Ed25519-signed delegation tokens and agent identity documents
  • Enforces configurable policy: allowed actions, max spend, delegation depth, calendar constraints, email domain allowlists, cognitive and reputation gates
  • Blocks revoked tokens, emergency-denied agents, nonce replays — fail-closed on backend errors
  • Audits every decision to a structured JSONL sink
  • Issues delegation tokens and identity documents via fluent builders with key rotation support

Feature flags

Flag What it enables
(none) Core evaluation pipeline, adapters, builders, file-backed state
async AsyncTrustStateStore trait + async engine variants
axum DelegatedLayer Tower middleware for axum
client DelegatedClient for sending trust-validated outbound requests
redis RedisTrustStateStore backed by Redis (async)
tracing tracing spans on the evaluation hot path
metrics metrics counters and histograms
oidc-bridge IdentityVerifier trait for OIDC-based identity verification

Quickstart — server side (axum middleware)

use std::sync::Arc;
use delegated::{
    DelegatedLayerBuilder, InMemoryAsyncTrustState, JsonlFileAuditSink,
};

let trust_state = Arc::new(InMemoryAsyncTrustState::new());
let sink = Arc::new(JsonlFileAuditSink::new("audit.jsonl"));
let layer = DelegatedLayerBuilder::new(trust_state, sink)
    .with_max_body_bytes(1024 * 1024) // optional, default is 1 MiB
    .build();

let app = axum::Router::new()
    .route("/tools/call", axum::routing::post(my_handler))
    .layer(layer);

Every POST to /tools/call is evaluated before my_handler runs. Denied requests return 403 with {allowed, stage, reason} JSON; allowed requests pass through. Oversized request bodies return 413.

Quickstart — client side

Build a RequestEnvelope with the issuance builders and attach it to outbound requests:

use delegated::{
    DelegatedClient,
    issuance::{AgentIdentityDocumentBuilder, DelegationTokenBuilder, RequestEnvelopeBuilder},
};
use ed25519_dalek::SigningKey;

let key = SigningKey::from_bytes(&secret_key_bytes);

let doc = AgentIdentityDocumentBuilder::new()
    .agent_id("agent:example:scheduler:v1")
    .owner_id("org:example")
    .issuer("https://trust.example.ai")
    .identity_type("spiffe")
    .subject("spiffe://example.ai/agents/scheduler")
    .key_id("key-2026-01")
    // Register an additional key for rotation:
    .additional_public_key("key-2026-02", &rotation_key.verifying_key())
    .supported_protocol("http")
    .supported_auth_method("delegation_token")
    .endpoint("http", "https://agents.example.ai/scheduler")
    .build_and_sign(&key)?;

let token = DelegationTokenBuilder::new()
    .issuer("https://trust.example.ai")
    .agent_id("agent:example:scheduler:v1")
    .delegator_id("user:alice")
    .owner_id("org:example")
    .audience("tool:google-calendar")
    .allowed_action("calendar.create_event")
    .key_id("key-2026-01")
    .expires_in(chrono::Duration::hours(1))
    .build_and_sign(&key)?;

let envelope = RequestEnvelopeBuilder::new()
    .identity_document(doc)
    .token(token)
    .audience("tool:google-calendar")
    .action("calendar.create_event")
    .build()?;

let client = DelegatedClient::new();
let resp = client.evaluate_http("https://api.example.com/trust", &envelope).await?;
if resp.is_allowed() {
    // proceed
}

Standalone adapters (without axum)

Call the adapters directly in any async or sync context:

// HTTP (sync)
use delegated::{handle_http_json_request, JsonlFileAuditSink};
use chrono::Utc;

let sink = JsonlFileAuditSink::new("audit.jsonl");
let response = handle_http_json_request(&raw_body, Utc::now(), &sink);
// response.status_code, response.body["allowed"]

// MCP
use delegated::handle_mcp_jsonrpc_request;
let response = handle_mcp_jsonrpc_request(&raw_body, Utc::now(), &sink);

// A2A
use delegated::handle_a2a_request;
let response = handle_a2a_request(&raw_body, Utc::now(), &sink);

Host context vs request runtime context

runtime_context in RequestEnvelope is caller-provided data used for request-specific checks (spend amount, target email/calendar, etc).

Security-sensitive trust signals (delegation depth, cognitive/reputation/risk assessments, extra approvals, clock leeway) are supplied by your infrastructure via HostContext/HostContextProvider and are never trusted from inbound request JSON.

Trust state

Sync (suitable for single-process / CLI use):

use delegated::{InMemoryTrustState, FileBackedTrustState, TrustStateAdmin};
use std::sync::Arc;

// In-memory with interior mutability — share as Arc<InMemoryTrustState>
let state = Arc::new(InMemoryTrustState::new());
state.revoke_token("dlg_abc")?;
state.emergency_deny_agent("agent:bad")?;
state.revoke_tokens(&["dlg_1", "dlg_2", "dlg_3"])?;
state.clear_emergency_deny_list()?;
state.flush_expired_nonces(chrono::Utc::now())?;

// File-backed (advisory lock, CLI / single-process only)
let state = FileBackedTrustState::new("~/.delegated/trust-state.json");

Async (Redis for production):

#[cfg(feature = "redis")]
use delegated::RedisTrustStateStore;
let state = Arc::new(RedisTrustStateStore::connect("redis://127.0.0.1").await?);
// revoke_tokens uses a Redis pipeline; clear_emergency_deny_list uses SCAN+DEL

Revocation and control plane

use delegated::{revoke_token_with_receipt, emergency_deny_agent, InMemoryTrustState};

let state = InMemoryTrustState::new();
// Revoke with an auditable receipt
let op = revoke_token_with_receipt(
    &state, "req_123", "dlg_abc".to_string(), "user:operator",
    Some("compromised".to_string()), chrono::Utc::now(),
)?;
println!("receipt: {}", op.receipt.request_id);

Trust pipeline

Every evaluation runs these stages in order, fail-closing at the first failure:

  1. normalize_request — parse and contract-validate the request envelope
  2. validate_profile_compatibility — SPIFFE / OIDC / Developer profile checks
  3. verify_signatures — Ed25519 on identity document and delegation token
  4. validate_identity_document_lifetime — expiry with configurable clock leeway
  5. enforce_revocation_and_redelegation — revocation, emergency deny, nonce replay, delegation depth
  6. validate_token_lifetime — token issued_at / expires_at window
  7. validate_token_bindingagent_id, delegator_id, audience cross-check
  8. evaluate_policy — allowed actions, max spend, calendar constraints, cognitive/reputation gates

Custom policy

use delegated::{Policy, PolicyCheck, RequestEnvelope, HostContext};

struct MyPolicy;
impl Policy for MyPolicy {
    fn evaluate(&self, envelope: &RequestEnvelope, ctx: &HostContext) -> Vec<PolicyCheck> {
        vec![PolicyCheck {
            name: "my_check".to_string(),
            passed: envelope.agent_id.starts_with("agent:trusted:"),
            reason: "agent not in trusted namespace".to_string(),
        }]
    }
}

// Pass to evaluate_request_with_policy / evaluate_and_audit_with_policy

CLI

cargo run --bin delegated-cli -- help

# Sign an identity document
delegated-cli sign-identity identity.json <base64url-private-key>

# Sign a delegation token
delegated-cli sign-token token.json <base64url-private-key>

# Verify a request envelope offline
delegated-cli verify-request request.json

# Interactive grant approval
delegated-cli approve-grant-interactive proposal.json user:operator

# Revoke a token (persists to ~/.delegated/trust-state.json)
delegated-cli revoke-token req_123 dlg_abc user:operator --reason "manual revoke"

Repository layout

src/
  engine.rs            — trust evaluation orchestration
  stages.rs            — individual evaluation stages
  policy.rs            — built-in policy checks
  revocation.rs        — TrustStateStore/Admin traits + InMemoryTrustState + FileBackedTrustState
  revocation_async.rs  — AsyncTrustStateStore/Admin + InMemoryAsyncTrustState
  revocation_redis.rs  — RedisTrustStateStore (feature: redis)
  issuance.rs          — DelegationTokenBuilder + AgentIdentityDocumentBuilder + RequestEnvelopeBuilder
  adapters/
    http.rs            — HTTP adapter
    mcp.rs             — MCP JSON-RPC adapter
    a2a.rs             — A2A adapter
    axum_layer.rs      — Tower Layer middleware (feature: axum)
    guard.rs           — rate limit + concurrency guard
  client.rs            — DelegatedClient (feature: client)
  audit.rs             — AuditSink trait + JsonlFileAuditSink
  discovery.rs         — DiscoveryService + JWKS handlers
  control_plane.rs     — revocation receipts + operational reports
  crypto.rs            — signing + verification primitives
tests/
  conformance.rs       — end-to-end allow/deny/replay/revocation
  interop_harness.rs   — cross-adapter and cross-profile parity
  reference_cli.rs     — CLI signing/verification/approval workflows
  integration_server.rs — real axum server + DelegatedClient round-trips

Testing

# Core tests
cargo test

# Integration tests (requires axum + client features)
cargo test --features "axum,client" --test integration_server

# With all optional features
cargo test --features "async,axum,client,tracing,metrics"

License

Licensed under either MIT or Apache-2.0 at your option.