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
IdentityResolverimplementation