Client Fail-Closed Guidance

Overview

This document provides guidance for implementing fail-closed behavior in client applications that consume the Agent Passport verification API.

Fail-Closed Principles

When implementing agent passport verification in your application, follow these fail-closed principles:

1. Default to Deny

// ✅ Good: Fail closed by default
if (!agentPassport || agentPassport.status !== "active") {
  throw new Error("Agent not authorized");
}

// ❌ Bad: Fail open
if (agentPassport?.status === "suspended") {
  throw new Error("Agent suspended");
}

2. Handle Network Failures

// ✅ Good: Fail closed on network errors
try {
  const passport = await verifyAgentPassport(agentId);
  return passport;
} catch (error) {
  if (error.code === "network_error" || error.code === "timeout") {
    // Fail closed: deny access when verification fails
    throw new Error("Agent verification failed - access denied");
  }
  throw error;
}

// ❌ Bad: Fail open on network errors
try {
  const passport = await verifyAgentPassport(agentId);
  return passport;
} catch (error) {
  // This allows access when verification fails - dangerous!
  console.warn("Verification failed, allowing access anyway");
  return null;
}

3. Respect Cache Headers

// ✅ Good: Use ETag for efficient caching
const response = await fetch(`/api/verify/${agentId}`, {
  headers: {
    'If-None-Match': lastETag, // Use cached version if available
  }
});

if (response.status === 304) {
  // Use cached passport data
  return cachedPassport;
}

4. Implement Timeout and Retry Logic

// ✅ Good: Reasonable timeout with fail-closed behavior
const verifyWithTimeout = async (agentId: string) => {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
  
  try {
    const passport = await verifyAgentPassport(agentId, {
      timeout: 5000,
      baseUrl: process.env.AGENT_PASSPORT_BASE_URL
    });
    clearTimeout(timeoutId);
    return passport;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error("Agent verification timed out - access denied");
    }
    throw error;
  }
};

5. Check Permissions and Regions

// ✅ Good: Verify permissions and regions
const checkAgentAccess = (passport: AgentPassport, requiredPermission: string, region: string) => {
  if (passport.status !== "active") {
    throw new Error("Agent is not active");
  }
  
  if (!passport.permissions.includes(requiredPermission)) {
    throw new Error("Agent lacks required permission");
  }
  
  if (!passport.regions.includes(region)) {
    throw new Error("Agent not allowed in this region");
  }
  
  return true;
};

Middleware Implementation

Express.js Example

import { agentPassportMiddleware } from '@agent-passport/middleware-express';

app.use('/api/agents', agentPassportMiddleware({
  failClosed: true, // ✅ Fail closed by default
  requiredPermissions: ['read:data'],
  allowedRegions: ['US-CA', 'US-NY'],
  timeout: 5000,
}));

FastAPI Example

from aporthq_middleware_fastapi import AgentPassportMiddleware

app.add_middleware(AgentPassportMiddleware,
    fail_closed=True,  # ✅ Fail closed by default
    required_permissions=['read:data'],
    allowed_regions=['US-CA', 'US-NY'],
    timeout=5000,
)

Monitoring and Alerting

Key Metrics to Monitor

  1. Verification Success Rate: Should be > 99.5%
  2. Verification Latency: p95 < 100ms
  3. Cache Hit Rate: Should be > 90%
  4. Network Error Rate: Should be < 0.1%

Alert Conditions

  • Verification success rate drops below 99%
  • p95 latency exceeds 100ms
  • Network error rate exceeds 1%
  • Cache hit rate drops below 80%

Report-Only Harness Mode

Some APort harnesses support an explicit warn or report-only mode for rollout. In that mode, the verifier still returns the original OAP policy decision. If the policy denies the action, the signed decision remains allow: false; the harness may still let the local action continue after warning the user because the operator chose non-blocking rollout.

Do not treat this as a policy allow. In dashboards and audits, show it as:

  • Policy decision: Denied
  • Expected runtime disposition: Warned and continued
  • Runtime disposition: Not reported unless the harness reports actual execution
  • Operator mode: Report-only

Hosted verification can store this runtime metadata next to the decision without changing the signed OAP decision. Pure local/offline verification remains local unless the operator explicitly enables telemetry.

See OAP Decisions vs Harness Enforcement for the full model.

Best Practices Summary

  1. Always fail closed - When in doubt, deny access
  2. Use timeouts - Don't wait indefinitely for verification
  3. Cache aggressively - Use ETag headers for efficiency
  4. Monitor everything - Track success rates and latency
  5. Test failure modes - Ensure your app behaves correctly when verification fails
  6. Document assumptions - Make it clear when and why access is granted
  7. Track report-only separately - A report-only continuation after allow: false is still a policy denial

Common Anti-Patterns

❌ Fail Open on Errors

// Don't do this - allows access when verification fails
try {
  const passport = await verifyAgentPassport(agentId);
  return passport;
} catch (error) {
  return { status: 'unknown' }; // Dangerous!
}

❌ Ignoring Status

// Don't do this - ignores agent status
const passport = await verifyAgentPassport(agentId);
return passport; // Should check passport.status === 'active'

❌ No Timeout

// Don't do this - can hang indefinitely
const passport = await verifyAgentPassport(agentId); // No timeout!

❌ Caching Forever

// Don't do this - ignores cache headers
const passport = await verifyAgentPassport(agentId);
localStorage.setItem('passport', JSON.stringify(passport)); // Never expires!