n a read-only data structure that the agent can query but never write to. The registry maps constraint identifiers to behavioral rules and validation logic.
interface ConstraintRule {
id: string;
description: string;
violationAction: 'BLOCK' | 'WARN';
metadata: Record<string, unknown>;
}
class ConstraintRegistry {
private readonly rules: Map<string, ConstraintRule> = new Map();
constructor(initialRules: ConstraintRule[]) {
initialRules.forEach(rule => this.rules.set(rule.id, rule));
}
getRule(id: string): ConstraintRule | undefined {
return this.rules.get(id);
}
validateAction(action: string): { allowed: boolean; violatedRules: string[] } {
const violated: string[] = [];
for (const rule of this.rules.values()) {
if (this.checkViolation(rule, action)) {
violated.push(rule.id);
}
}
return { allowed: violated.length === 0, violatedRules: violated };
}
private checkViolation(rule: ConstraintRule, action: string): boolean {
// Domain-specific matching logic
return action.includes(rule.id) && rule.violationAction === 'BLOCK';
}
}
Why this choice: A Map-based registry ensures O(1) lookups during runtime evaluation. The class exposes only read methods, preventing accidental mutation. Validation logic is isolated, making it trivial to swap rule engines without touching the agent core.
Step 2: Design the Exception Ticket Schema
Exceptions must be explicitly scoped. Vague requests like "allow pause flag overrides" create precedent and weaken constraints. Instead, each ticket targets a single action, includes cryptographic context, and enforces temporal boundaries.
interface ExceptionTicket {
ticketId: string;
constraintId: string;
targetAction: {
type: string;
payload: Record<string, unknown>;
idempotencyKey: string;
};
justification: string;
requiredApprovals: Array<{ role: string; weight: number }>;
thresholdWeight: number;
expiresAt: string; // ISO 8601
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'EXPIRED' | 'EXECUTED';
createdAt: string;
}
Why this choice: The idempotencyKey prevents duplicate execution if the dispatcher retries. The status field enforces a strict state machine. Time-bound expiration eliminates stale tickets that could be exploited later. Explicit scoping ensures no precedent is set for future requests.
Step 3: Implement the Weighted Approval Engine
Agents cannot approve their own exceptions. External signatories must provide weighted approvals that meet a predefined threshold. The engine validates signatures, aggregates weights, and transitions ticket state.
interface ApprovalVote {
ticketId: string;
approverRole: string;
weight: number;
signature: string;
timestamp: string;
}
class ApprovalEngine {
private votes: Map<string, ApprovalVote[]> = new Map();
submitVote(vote: ApprovalVote): boolean {
const existing = this.votes.get(vote.ticketId) || [];
if (existing.some(v => v.approverRole === vote.approverRole)) {
throw new Error('Duplicate role vote');
}
this.votes.set(vote.ticketId, [...existing, vote]);
return this.evaluateThreshold(vote.ticketId);
}
private evaluateThreshold(ticketId: string): boolean {
const ticket = this.getTicket(ticketId);
if (!ticket || ticket.status !== 'PENDING') return false;
const totalWeight = this.votes.get(ticketId)?.reduce((sum, v) => sum + v.weight, 0) || 0;
if (totalWeight >= ticket.thresholdWeight) {
ticket.status = 'APPROVED';
return true;
}
return false;
}
private getTicket(id: string): ExceptionTicket | undefined {
// Fetch from persistent store
return undefined;
}
}
Why this choice: Role-based weighting allows granular control (e.g., operator: 2, reviewer: 1). Duplicate role prevention stops vote stuffing. Threshold evaluation runs synchronously on submission, ensuring immediate state transition. Cryptographic signatures (omitted for brevity) should validate approver identity in production.
Step 4: Build the Scoped Action Executor
Approved tickets are routed to a dedicated executor that runs only the specified action. The executor operates in a sandboxed context with no access to the constraint registry or approval engine.
class ScopedExecutor {
async execute(ticket: ExceptionTicket): Promise<{ success: boolean; result: unknown }> {
if (ticket.status !== 'APPROVED') {
throw new Error('Ticket not approved');
}
if (new Date(ticket.expiresAt) < new Date()) {
ticket.status = 'EXPIRED';
throw new Error('Ticket expired');
}
try {
const result = await this.runIsolatedAction(ticket.targetAction);
ticket.status = 'EXECUTED';
return { success: true, result };
} catch (error) {
ticket.status = 'PENDING'; // Revert for retry or manual review
throw error;
}
}
private async runIsolatedAction(action: ExceptionTicket['targetAction']): Promise<unknown> {
// Sandboxed execution environment
// Whitelist allowed operations, enforce resource limits
return { executed: true, actionId: action.idempotencyKey };
}
}
Why this choice: The executor validates state and expiration before execution. Failure reverts the ticket to PENDING rather than marking it failed, enabling safe retries. The isolated execution context prevents privilege escalation and ensures the agent's core logic remains untouched.
Pitfall Guide
1. Overly Broad Exception Scopes
Explanation: Requesting exceptions for entire categories of actions (e.g., "allow all pause flag modifications") creates implicit precedent and weakens constraint integrity.
Fix: Enforce single-action granularity. Each ticket must specify exact parameters, targets, and payloads. Validate scope against a whitelist of allowed exception types.
2. Missing Temporal Boundaries
Explanation: Tickets without expiration accumulate in pending states, creating attack surfaces for replay or delayed execution.
Fix: Mandate expiresAt on all tickets. Implement a background sweeper that automatically transitions expired tickets to EXPIRED and logs the event.
3. Approval Weight Miscalibration
Explanation: Setting thresholds too low allows single approvers to bypass constraints. Setting them too high creates operational bottlenecks.
Fix: Map weights to organizational risk tiers. Use dynamic thresholds based on action severity. Validate weight distribution during ticket creation.
4. Executor Privilege Escalation
Explanation: If the executor shares memory or file system access with the agent core, approved actions can inadvertently modify constraints or approval state.
Fix: Run the executor in a sandboxed process or container. Use capability-based security to restrict file/network access. Validate outputs against expected schemas.
5. Audit Trail Fragmentation
Explanation: Logging exceptions across multiple systems breaks traceability and complicates compliance reviews.
Fix: Implement centralized event sourcing. Every state transition (creation, vote, approval, execution, expiration) must emit a structured event with cryptographic hashes linking to the original ticket.
6. Self-Approval Loops
Explanation: If the agent can generate or sign approval tokens, the entire governance model collapses.
Fix: Enforce identity isolation. Approver credentials must reside outside the agent's execution environment. Use hardware-backed keys or external identity providers for signature validation.
7. Idempotency Neglect
Explanation: Network retries or dispatcher crashes can cause duplicate execution of approved actions, leading to state corruption.
Fix: Require idempotencyKey in every ticket. The executor must check a deduplication store before running the action and return cached results on repeat requests.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-risk infrastructure change | Delegated Exception Routing with multi-role threshold | Prevents unilateral modification, maintains full audit trail | Medium (approval latency) |
| Low-risk UI/content update | Static constraint with WARN violation action | Reduces operational overhead while preserving visibility | Low |
| Emergency failover | Pre-approved exception templates with elevated weights | Enables rapid response without sacrificing governance | Low-Medium (template maintenance) |
| Experimental agent behavior | Isolated sandbox with no constraint routing | Contains risk during development phase | Low |
Configuration Template
exception_routing:
registry:
mode: READ_ONLY
refresh_interval: 0 # Immutable at runtime
ticket:
required_fields:
- constraint_id
- target_action
- idempotency_key
- expires_at
max_ttl_hours: 48
approval:
roles:
operator:
weight: 2
min_required: 1
reviewer:
weight: 1
min_required: 2
signature_algorithm: ES256
executor:
sandbox: true
resource_limits:
cpu: "0.5"
memory: "256Mi"
network: "deny_outbound"
idempotency_store: redis
audit:
format: JSON
retention_days: 365
hash_algorithm: SHA-256
Quick Start Guide
- Initialize the Registry: Load your constraint rules into a
ConstraintRegistry instance. Ensure the file system or database backing it has write permissions disabled for the agent process.
- Deploy the Dispatcher: Run the
ApprovalEngine and ScopedExecutor as separate services. Configure them to share a persistent ticket store (PostgreSQL or Redis) with strict access controls.
- Configure Approver Identities: Generate cryptographic key pairs for each approver role. Store private keys in a hardware security module or external secret manager. Never embed them in agent code.
- Test the Flow: Submit a test ticket with a 5-minute TTL. Route it through the approval engine, verify threshold calculation, and execute via the sandboxed executor. Validate that the constraint registry remains unmodified and all events are logged.
- Monitor & Iterate: Track approval latency, ticket expiration rates, and executor success metrics. Adjust weight thresholds and TTL values based on operational feedback. Deploy automated sweeps for expired tickets and failed executions.