m strings are prohibited. Allowing detailed explanations (e.g., "API key detected at byte offset 412") creates an oracle attack surface. Agents or malicious actors could iteratively probe the proxy to reverse-engineer scanner rules. A fixed enum list eliminates this risk.
- Categorical Retry Hints: Traditional
Retry-After headers expose rate-limit windows. Instead, the schema uses discrete advice: none (payload will never pass), transient (infrastructure or temporary limit), policy (requires configuration change). This prevents timing-based rate limit mapping.
- Single Reason Enforcement: A block carries exactly one primary reason. Complexity is managed by layering severity and retry advice, not by concatenating multiple codes.
Step 2: Proxy Middleware Implementation
The enforcement layer intercepts requests, evaluates rules, and attaches headers before returning the response. Below is a TypeScript implementation demonstrating the injection logic:
import type { RequestHandler } from 'express';
import { SecurityEngine } from './security-engine';
export const blockReasonMiddleware = (engine: SecurityEngine): RequestHandler => {
return async (req, res, next) => {
try {
const decision = await engine.evaluate(req);
if (decision.allowed) {
return next();
}
// Attach structured block headers
res.set('X-Enforce-Block-Reason', decision.reason);
res.set('X-Enforce-Block-Reason-Version', '1');
res.set('X-Enforce-Block-Reason-Severity', decision.severity);
res.set('X-Enforce-Block-Reason-Retry', decision.retryAdvice);
res.set('X-Enforce-Block-Reason-Layer', decision.layer);
// Optional: v2.4+ receipt pointer (reserved, populated post-wiring)
if (decision.receiptId) {
res.set('X-Enforce-Block-Reason-Receipt', decision.receiptId);
}
res.status(403).end();
} catch (err) {
// Fallback to generic block on internal failure
res.set('X-Enforce-Block-Reason', 'parse_error');
res.set('X-Enforce-Block-Reason-Retry', 'transient');
res.status(500).end();
}
};
};
Step 3: Client-Side Parser & Retry Orchestrator
The consuming agent must parse headers and adjust behavior deterministically. Tightly coupling retry logic to proxy internals is an anti-pattern; instead, use a state machine driven by the advisory fields.
export interface BlockSignal {
reason: string;
severity: 'info' | 'warning' | 'critical';
retryAdvice: 'none' | 'transient' | 'policy';
layer: string;
}
export class RetryAwareClient {
private budget: number;
private maxRetries: number;
constructor(maxRetries: number = 3) {
this.maxRetries = maxRetries;
this.budget = maxRetries;
}
async fetchWithEnforcement(url: string, init?: RequestInit): Promise<Response> {
const response = await fetch(url, init);
if (response.ok) return response;
const signal = this.parseBlockHeaders(response);
this.handleBlockSignal(signal, url);
return response;
}
private parseBlockHeaders(res: Response): BlockSignal {
return {
reason: res.headers.get('X-Enforce-Block-Reason') || 'unknown',
severity: (res.headers.get('X-Enforce-Block-Reason-Severity') as BlockSignal['severity']) || 'warning',
retryAdvice: (res.headers.get('X-Enforce-Block-Reason-Retry') as BlockSignal['retryAdvice']) || 'transient',
layer: res.headers.get('X-Enforce-Block-Reason-Layer') || 'gateway'
};
}
private handleBlockSignal(signal: BlockSignal, url: string): void {
switch (signal.retryAdvice) {
case 'none':
console.warn(`[ENFORCE] Hard deny: ${signal.reason} (${signal.layer}). Budget preserved.`);
// Do not decrement budget. Surface to user/orchestrator.
break;
case 'transient':
if (this.budget > 0) {
this.budget--;
console.info(`[ENFORCE] Transient block: ${signal.reason}. Retries remaining: ${this.budget}`);
// Trigger backoff retry logic here
} else {
console.error(`[ENFORCE] Budget exhausted after transient blocks.`);
}
break;
case 'policy':
console.error(`[ENFORCE] Policy block: ${signal.reason}. Requires operator intervention.`);
// Do not retry. Alert configuration management system.
break;
default:
console.warn(`[ENFORCE] Unrecognized advice: ${signal.retryAdvice}. Defaulting to safe halt.`);
}
}
}
Step 4: Multi-Protocol Mapping
Agents operate across HTTP, WebSockets, and MCP stdio. The schema must translate across transport boundaries:
- HTTP: Direct header injection as shown.
- WebSockets: Map to close frame reason codes and extension metadata.
- MCP/JSON-RPC: Inject into the
error object payload (code, message, data.retryAdvice, data.severity). The stdio transport lacks HTTP headers, so the JSON-RPC envelope becomes the canonical surface.
Pitfall Guide
Explanation: Returning human-readable explanations like "Request contained AWS key AKIA..." leaks scanner internals. Attackers can use this as an oracle to iteratively adjust payloads until they bypass detection.
Fix: Enforce a strict closed vocabulary. Use categorical codes (dlp_match, ssrf_private_ip) and log detailed context server-side only.
2. Conflating Severity with Retry Advice
Explanation: Severity (critical, warning) indicates logging/alerting priority. Retry advice (none, transient) dictates client action. Mixing them causes agents to retry critical policy violations or ignore transient infrastructure failures.
Fix: Keep fields orthogonal. Severity drives observability pipelines; retry advice drives client state machines.
Explanation: Exposing exact rate-limit windows allows agents to map proxy capacity and schedule requests to maximize throughput during blind spots.
Fix: Use categorical hints. transient signals "retry with exponential backoff"; none signals "stop immediately". Never expose millisecond windows.
4. Ignoring Non-HTTP Transport Surfaces
Explanation: MCP agents and WebSocket streams do not parse HTTP headers. Returning block reasons only on HTTP paths creates inconsistent agent behavior across toolchains.
Fix: Implement protocol adapters. Map the same reason enum to WebSocket close codes and JSON-RPC error payloads. Maintain a single source of truth for the vocabulary.
5. Overloading with Multiple Reason Codes
Explanation: Attempting to return ssrf_private_ip, rate_limit, prompt_injection in a single response creates ambiguity. The client cannot determine which rule triggered first or which takes precedence.
Fix: Enforce single-reason semantics. The proxy should evaluate rules in a deterministic order and return the first match. Use the layer field to indicate which subsystem fired.
6. Hardcoding Client Logic to Specific Proxies
Explanation: Tightly coupling agent retry logic to X-Pipelock-Block-Reason creates vendor lock-in. If the proxy changes or is replaced, the agent breaks.
Fix: Abstract the parser behind an interface. Accept any header matching the open spec. Validate against a allowlist of known reasons and fallback to unknown for forward compatibility.
7. Skipping Schema Versioning
Explanation: Adding fields or changing semantics without versioning causes silent failures. Older clients may misinterpret new advice values or crash on unexpected headers.
Fix: Always emit X-Enforce-Block-Reason-Version. Clients should validate the version before parsing. Implement graceful degradation for unknown versions.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume CI/CD agent pipelines | Structured block headers + retry=none skip | Prevents retry storms on policy violations, saves compute | β 40-60% compute waste |
| Interactive developer agents | Structured headers + retry=policy escalation | Routes configuration issues to operators without burning budget | β Support ticket volume |
| Legacy proxy without header support | Custom error body parsing + fallback logic | Maintains compatibility while migrating to open spec | β Initial dev overhead |
| Multi-vendor proxy environment | Open-spec header adoption + version validation | Ensures consistent agent behavior across infrastructure | β Vendor lock-in risk |
| Strict compliance/air-gapped systems | Closed vocab + server-side detailed logging | Prevents info leakage while preserving audit trails | β Storage cost (logs) |
Configuration Template
Proxy Middleware Registration (TypeScript/Express)
import { SecurityEngine } from './security-engine';
import { blockReasonMiddleware } from './block-reason-middleware';
const engine = new SecurityEngine({
vocab: 'pipelock-v1', // Maps to closed enum list
layers: ['egress', 'content', 'mcp', 'posture', 'contract'],
receiptEnabled: false // Set true when v2.4 receipt wiring is active
});
app.use(blockReasonMiddleware(engine));
Client Retry Orchestrator Setup
const agentClient = new RetryAwareClient({
maxRetries: 3,
backoffStrategy: 'exponential',
maxBackoffMs: 5000,
onPolicyBlock: (signal) => notifyOpsChannel(signal)
});
// Usage in tool execution
const result = await agentClient.fetchWithEnforcement(targetUrl, {
method: 'POST',
body: JSON.stringify(payload)
});
Quick Start Guide
- Deploy the middleware: Attach the block-reason middleware to your proxy's request pipeline. Ensure it wraps all enforcement evaluation paths.
- Validate header emission: Trigger a known block (e.g., private IP access, DLP pattern). Verify the response contains all five headers with correct enum values.
- Update client parser: Replace blind retry loops with the
RetryAwareClient pattern. Map retryAdvice to explicit control flow.
- Test multi-protocol paths: Run the same block scenario over WebSocket and MCP stdio. Confirm reason codes translate correctly to close frames and JSON-RPC errors.
- Monitor budget metrics: Deploy observability dashboards tracking
retry_budget_consumed vs block_reason. Tune agent budgets based on actual transient vs. policy block ratios.
Structured enforcement signals transform security proxies from termination points into coordination layers. By standardizing the feedback contract, operators gain immediate visibility, agents preserve compute, and the entire stack operates with deterministic failure semantics. The schema is open, the vocabulary is closed, and the operational payoff scales linearly with request volume.