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
- Verification Success Rate: Should be > 99.5%
- Verification Latency: p95 < 100ms
- Cache Hit Rate: Should be > 90%
- 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
- Always fail closed - When in doubt, deny access
- Use timeouts - Don't wait indefinitely for verification
- Cache aggressively - Use ETag headers for efficiency
- Monitor everything - Track success rates and latency
- Test failure modes - Ensure your app behaves correctly when verification fails
- Document assumptions - Make it clear when and why access is granted
- Track report-only separately - A report-only continuation after
allow: falseis 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!