MAP Docs
Engine

Middleware

IdentityMiddleware::enrich runs before MapEngine::handle_request and attaches the resolved caller identity to the invocation context.

IdentityMiddleware lives in engine/identity/src/middleware.rs. It is the only middleware MAP runs before MapEngine::handle_request. Everything else — rate limiting, security gating, audit — is internal to the engine.

What it does

pub struct IdentityMiddleware {
    resolver: Arc<dyn IdentityResolver>,
}

impl IdentityMiddleware {
    pub async fn enrich(&self, mut ctx: InvokeContext) -> InvokeContext {
        let outcome = self.resolver.resolve(&ctx.agent_did).await;
        ctx.resolved_identity = Some(match outcome {
            ResolveOutcome::Resolved(id)         => id,
            ResolveOutcome::Partial { identity, .. } => identity,
            ResolveOutcome::Failed { did, .. }   => ResolvedAgentIdentity::unresolved(&did),
        });
        ctx
    }
}

The middleware never raises. If resolution fails, the request still flows into the engine with an unresolved identity and will typically be denied at Stage 4 (Security Gating).

IdentityResolver trait

#[async_trait]
pub trait IdentityResolver: Send + Sync {
    async fn resolve(&self, did: &str) -> ResolveOutcome;
}

This is the plugin point. Production deployments wire OasResolver (from the protocols/oas-lib adapter) — but the resolver is intentionally pluggable to support local dev (LocalAdminResolver returns ResolvedAgentIdentity::local_admin() for every DID) and offline modes.

ResolveOutcome

pub enum ResolveOutcome {
    /// Full resolution: identity document, lineage walked, signatures verified.
    Resolved(ResolvedAgentIdentity),

    /// Partial: identity returned but with warnings (stale, lineage unverified, etc.).
    /// The engine still treats the identity as valid; warnings are recorded for review.
    Partial { identity: ResolvedAgentIdentity, warnings: Vec<String> },

    /// Resolution failed entirely. The fallback `unresolved` identity is used.
    Failed { did: String, error: String },
}

Partial is common during MOAT treaty negotiation, when a counterparty's DID is reachable but their lineage proofs haven't yet been replicated locally. The request goes through, but a LineageUnverified event is recorded.

Where it sits in the request flow

HTTP gateway
    ├─ parse Authorization header → agent_did
    ├─ parse trace context → traceparent / tracestate

IdentityMiddleware::enrich
    ├─ resolver.resolve(agent_did)

MapEngine::handle_request
    ├─ Stage 1: Version Resolution
    ├─ Stage 2: Context Enrichment (adds correlation_id, tenant, timestamp)
    ├─ Stage 3: Rate Limiting
    ├─ Stage 4: Security Gating (reads resolved_identity.capabilities)
    ├─ ...

InvokeContext::resolved_identity is Option<ResolvedAgentIdentity> so the engine type does not change between gateway and CLI ingress paths.

The CLI runs IdentityMiddleware::enrich locally before sending the call to the engine, using the stored agent_did from ~/.map/config.json. The same flow applies; only the resolver is different (CLI uses OasResolver against the cached registry; gateway uses the hot OAS resolver bound to MARS).

Reading the identity in a protocol module

async fn invoke(
    &self,
    operation: &str,
    payload: Value,
    ctx: &InvokeContext,
) -> Result<Response, ProtocolError> {
    let id = ctx.identity().ok_or(ProtocolError::PolicyDenied {
        reason: "unresolved identity".into()
    })?;

    if !ctx.caller_lineage_verified() {
        return Err(ProtocolError::PolicyDenied {
            reason: "lineage must be verified for this operation".into()
        });
    }

    if !id.has_capability(&format!("map.{}.{}", self.protocol_name().to_lowercase(), operation)) {
        return Err(ProtocolError::MissingCapability(
            format!("map.{}.{}", self.protocol_name().to_lowercase(), operation)
        ));
    }

    // ... normal logic
}

Most protocol crates use the helper common_auth::reject_payload_tenant_mismatch(ctx, &payload)? to enforce the basic tenant binding before any work. See engine/common/src/auth.rs.

See also

  • ResolvedAgentIdentity — the data structure populated here
  • MACS — the protocol that does the upstream signature verification
  • OAS adapter — the default IdentityResolver implementation

On this page