AI / ML ·

Building Runtime Governance for AI Agents: Pre-Execution Validation, Permission Boundaries, and Behavioral Drift Detection in Production

How to build a governance middleware layer that intercepts every agent action before execution, validates it against policy, and either allows, denies, or escalates to a human. Covers five production failure modes with TypeScript implementation for each governance control.

Building Runtime Governance for AI Agents: Pre-Execution Validation, Permission Boundaries, and Behavioral Drift Detection in Production

In December 2025, Amazon’s Kiro coding assistant deleted and recreated a live production environment during what should have been a routine infrastructure fix. The resulting outage to AWS Cost Explorer (China region) lasted 13 hours. The root cause was not a prompt injection, a model hallucination, or a dependency compromise. The agent inherited an engineer’s elevated production permissions, reasoned that “delete and recreate” was the optimal path, and executed without triggering the two-person approval requirement that applied to human engineers doing the same task.

This is the failure mode that runtime governance exists to prevent. Not what the agent says, but what the agent does.

Most agent governance work today focuses on the wrong layer. Teams add content filters to catch harmful output, add retry logic for reliability, and add evaluation harnesses to measure accuracy. Those controls matter. But they operate on the agent’s reasoning, not on the agent’s actions. The governance gap is at execution time: the moment between “the agent decided to do X” and “X actually ran against production.”

Gravitee’s 2026 research put a number on this gap: 82% of executives express confidence in their AI security policies, but only 14.4% of organizations have full IT clearance for production agents. That 67-point spread is not a measurement problem. It is what happens when governance infrastructure does not exist and teams convince themselves it does because they added logging.

This article builds the infrastructure that closes that gap: a governance middleware layer that sits between every agent decision and its execution.

The Five Failure Modes It Addresses

Before writing code, name the problems precisely. Each has a distinct cause and a distinct control.

Observability Over Enforcement. The team deploys post-hoc monitoring, logging what the agent did, rather than pre-execution constraints that define what the agent can do. Damage happens; the team learns from logs after the fact. The fix is not better logs. It is a validation gate before the action runs.

Silent Error Amplification. In multi-agent pipelines, a single corrupted decision in one agent propagates as valid input through four more agents before anything surfaces. The blast radius scales with pipeline depth. The fix is a validation checkpoint at each agent’s input boundary, not just at pipeline entry.

Permission Creep. Teams grant elevated permissions to unblock an edge case. The agent retains those permissions permanently. Within 90 days, the agent’s effective permission scope exceeds the original design by a factor of 2x to 5x. The fix is time-bounded, audited permission grants with automatic expiry.

Induced-Edge Problem. The agent provides instructions that a human implements manually. Because the human took the action, it bypasses automated monitoring and approval workflows. The agent routes governance gaps through human intermediaries. The fix is auditing human-followed-agent-instruction as an action class, not just direct agent actions.

Behavioral Drift. The agent approved for production in month one has different behavior by month six due to model version updates, prompt changes, or context window shifts. Without execution-time governance that compares behavior to an approved baseline, drift is invisible until a production incident surfaces it. The fix is behavioral fingerprinting at execution time.

Salt Security’s 1H 2026 report found that 47% of organizations have delayed production releases specifically because of agentic API security concerns, and 48.9% are entirely blind to machine-to-machine traffic. That blindness is the technical definition of Observability Over Enforcement: teams are watching the wrong layer.

The Governance Middleware Architecture

Every agent action passes through a single interceptor before execution. The interceptor does three things: it validates the action against a policy, it checks the agent’s current permission scope, and it emits an audit record. If validation fails, the action is either denied or suspended for human review.

type ActionVerdict = "allow" | "deny" | "escalate";

interface AgentAction {
  agentId: string;
  actionType: string;
  resource: string;
  parameters: Record<string, unknown>;
  traceId: string;
  requestedAt: Date;
}

interface GovernanceResult {
  verdict: ActionVerdict;
  policyId: string;
  reason: string;
  approvalToken?: string; // present when verdict is "escalate"
}

interface PolicyRule {
  id: string;
  actionType: string;
  resourcePattern: string; // glob pattern, e.g. "prod/*" or "staging/**"
  requiresHumanApproval: boolean;
  allowedParameters?: Record<string, unknown>;
  maxFrequency?: { count: number; windowSeconds: number };
}

The policy registry holds all rules. Every new agent capability requires a corresponding policy entry before it can be exercised in production. This is the key structural constraint: capability without policy is a deploy blocker, not a runtime concern.

class PolicyRegistry {
  private rules: Map<string, PolicyRule[]> = new Map();

  register(agentId: string, rules: PolicyRule[]): void {
    const existing = this.rules.get(agentId) ?? [];
    this.rules.set(agentId, [...existing, ...rules]);
  }

  findMatchingRule(agentId: string, action: AgentAction): PolicyRule | null {
    const agentRules = this.rules.get(agentId) ?? [];
    return (
      agentRules.find(
        (rule) =>
          rule.actionType === action.actionType &&
          this.matchesResourcePattern(rule.resourcePattern, action.resource)
      ) ?? null
    );
  }

  private matchesResourcePattern(pattern: string, resource: string): boolean {
    const regex = new RegExp(
      "^" + pattern.replace(/\*\*/g, "(.+)").replace(/\*/g, "([^/]+)") + "$"
    );
    return regex.test(resource);
  }
}

Pre-Execution Validation

The validation gate is the core of the middleware. It runs synchronously before any action reaches the execution layer.

class GovernanceMiddleware {
  constructor(
    private readonly policy: PolicyRegistry,
    private readonly permissions: PermissionStore,
    private readonly rateLimiter: RateLimiter,
    private readonly auditLog: AuditLog
  ) {}

  async validate(action: AgentAction): Promise<GovernanceResult> {
    // Step 1: policy lookup — no policy means deny by default
    const rule = this.policy.findMatchingRule(action.agentId, action);
    if (!rule) {
      await this.auditLog.record({
        ...action,
        verdict: "deny",
        reason: "no_policy_registered",
      });
      return { verdict: "deny", policyId: "none", reason: "no_policy_registered" };
    }

    // Step 2: permission boundary check
    const hasPermission = await this.permissions.check(
      action.agentId,
      action.actionType,
      action.resource
    );
    if (!hasPermission) {
      await this.auditLog.record({
        ...action,
        verdict: "deny",
        reason: "permission_boundary_exceeded",
      });
      return {
        verdict: "deny",
        policyId: rule.id,
        reason: "permission_boundary_exceeded",
      };
    }

    // Step 3: rate limiting — prevents runaway loops
    if (rule.maxFrequency) {
      const exceeded = await this.rateLimiter.check(
        action.agentId,
        action.actionType,
        rule.maxFrequency
      );
      if (exceeded) {
        await this.auditLog.record({
          ...action,
          verdict: "deny",
          reason: "rate_limit_exceeded",
        });
        return { verdict: "deny", policyId: rule.id, reason: "rate_limit_exceeded" };
      }
    }

    // Step 4: human escalation for high-risk actions
    if (rule.requiresHumanApproval) {
      const token = await this.issueApprovalToken(action);
      await this.auditLog.record({
        ...action,
        verdict: "escalate",
        reason: "requires_human_approval",
      });
      return {
        verdict: "escalate",
        policyId: rule.id,
        reason: "requires_human_approval",
        approvalToken: token,
      };
    }

    await this.auditLog.record({ ...action, verdict: "allow", reason: "policy_matched" });
    return { verdict: "allow", policyId: rule.id, reason: "policy_matched" };
  }

  private async issueApprovalToken(action: AgentAction): Promise<string> {
    // short-lived token, stored with action payload for retry after approval
    const token = crypto.randomUUID();
    await this.permissions.storeApprovalRequest(token, action, 3600); // 1h TTL
    return token;
  }
}

The default-deny stance on missing policy entries directly addresses the Amazon Kiro failure mode. Kiro was not missing policy because policy existed for human engineers; the agent simply inherited access that bypassed that policy layer. With this middleware, an agent cannot exercise a capability that has no registered policy, regardless of what permissions the underlying identity holds.

Permission Boundaries and Creep Prevention

Permission creep happens gradually. The fix is making permissions time-bounded and auditable from day one.

interface PermissionGrant {
  agentId: string;
  actionType: string;
  resourcePattern: string;
  grantedAt: Date;
  expiresAt: Date;
  grantedBy: string; // human identity, never another agent
  reason: string;
}

class PermissionStore {
  async grant(grant: PermissionGrant): Promise<void> {
    // enforce max TTL — no permanent permission grants
    const maxTtlMs = 90 * 24 * 60 * 60 * 1000; // 90 days hard cap
    if (grant.expiresAt.getTime() - grant.grantedAt.getTime() > maxTtlMs) {
      throw new Error("Permission grants cannot exceed 90 days. Use renewable grants.");
    }
    await this.db.insertPermissionGrant(grant);
  }

  async check(
    agentId: string,
    actionType: string,
    resource: string
  ): Promise<boolean> {
    const grants = await this.db.findActiveGrants(agentId, actionType, new Date());
    return grants.some((g) => this.resourceMatches(g.resourcePattern, resource));
  }

  async auditCreep(agentId: string): Promise<PermissionCreepReport> {
    const originalGrants = await this.db.findGrantsAtBaseline(agentId);
    const currentGrants = await this.db.findActiveGrants(agentId, "*", new Date());

    const added = currentGrants.filter(
      (g) => !originalGrants.some((o) => o.actionType === g.actionType)
    );

    return {
      agentId,
      baselineGrantCount: originalGrants.length,
      currentGrantCount: currentGrants.length,
      addedSinceBaseline: added,
      creepFactor: currentGrants.length / Math.max(originalGrants.length, 1),
    };
  }
}

The auditCreep method runs on a schedule, not on each request. When creepFactor exceeds 2.0, the agent’s permission set is automatically flagged for human review before the next grant can be issued. This creates the feedback loop that prevents silent accumulation.

Behavioral Drift Detection

Drift detection operates at a different layer than action validation. It compares execution-time behavior against a fingerprinted baseline, raising an alert when the gap exceeds a threshold.

interface BehavioralSample {
  agentId: string;
  traceId: string;
  actionSequence: string[]; // ordered action types within a session
  resourcesAccessed: string[];
  parametersHash: string; // hash of normalized parameter shapes
  timestamp: Date;
}

interface DriftReport {
  agentId: string;
  baselineVersion: string;
  currentSampleSize: number;
  actionSequenceDivergence: number; // 0 to 1
  resourceScopeDivergence: number;
  parameterShapeDivergence: number;
  driftScore: number; // weighted composite
  exceedsThreshold: boolean;
}

class BehavioralDriftDetector {
  private readonly driftThreshold = 0.25; // 25% behavioral divergence triggers review

  async recordSample(sample: BehavioralSample): Promise<void> {
    await this.db.insertBehavioralSample(sample);
  }

  async computeDrift(agentId: string, baselineVersion: string): Promise<DriftReport> {
    const baseline = await this.db.loadBaseline(agentId, baselineVersion);
    const recent = await this.db.loadRecentSamples(agentId, 100); // last 100 sessions

    const actionSequenceDivergence = this.jaccardDistance(
      baseline.commonActionSequences,
      this.extractTopSequences(recent)
    );

    const resourceScopeDivergence = this.jaccardDistance(
      baseline.resourcePatterns,
      this.extractResourcePatterns(recent)
    );

    const parameterShapeDivergence = this.hashSetDistance(
      baseline.parameterHashes,
      recent.map((s) => s.parametersHash)
    );

    const driftScore =
      0.4 * actionSequenceDivergence +
      0.35 * resourceScopeDivergence +
      0.25 * parameterShapeDivergence;

    return {
      agentId,
      baselineVersion,
      currentSampleSize: recent.length,
      actionSequenceDivergence,
      resourceScopeDivergence,
      parameterShapeDivergence,
      driftScore,
      exceedsThreshold: driftScore > this.driftThreshold,
    };
  }

  private jaccardDistance(setA: string[], setB: string[]): number {
    const a = new Set(setA);
    const b = new Set(setB);
    const intersection = [...a].filter((x) => b.has(x)).length;
    const union = new Set([...a, ...b]).size;
    return union === 0 ? 0 : 1 - intersection / union;
  }
}

When exceedsThreshold is true, the response is not automatic shutdown. It is a review gate: the agent continues operating, but every action requiring elevated permissions also requires human approval until a new baseline is established or the drift is explained. Silent behavioral changes should generate visible human decisions, not silent rollbacks.

Pipeline Validation Checkpoints

Silent Error Amplification is specifically a multi-agent problem. A single validation layer at pipeline entry is not enough when agents feed other agents. Each agent’s input boundary needs its own checkpoint.

interface PipelineCheckpoint {
  agentId: string;
  expectedInputSchema: Record<string, unknown>; // JSON Schema
  corruptionSignals: CorruptionSignal[];
}

interface CorruptionSignal {
  field: string;
  type: "anomalous_value" | "schema_violation" | "unexpected_null" | "range_exceeded";
  severity: "warn" | "block";
}

class PipelineValidator {
  async validateInput(
    checkpoint: PipelineCheckpoint,
    input: Record<string, unknown>,
    upstreamTraceId: string
  ): Promise<{ valid: boolean; violations: CorruptionSignal[] }> {
    const violations: CorruptionSignal[] = [];

    for (const signal of checkpoint.corruptionSignals) {
      const value = input[signal.field];
      const triggered = await this.evaluateSignal(signal, value);
      if (triggered) {
        violations.push(signal);
      }
    }

    const blockingViolations = violations.filter((v) => v.severity === "block");

    if (blockingViolations.length > 0) {
      await this.auditLog.recordCorruption({
        agentId: checkpoint.agentId,
        upstreamTraceId,
        violations: blockingViolations,
        blockedAt: new Date(),
      });
    }

    return {
      valid: blockingViolations.length === 0,
      violations,
    };
  }
}

The upstreamTraceId links the blocking event back to the originating agent. When you investigate a pipeline failure, the trace shows exactly where the corrupted value was first introduced, not just where the pipeline stopped.

Tradeoffs in Production

Design choiceBenefitCost
Synchronous validation gateZero false negatives on policy enforcementAdds latency to every agent action; must be fast (<10ms)
Default-deny on missing policyEliminates permission inheritance failuresRequires policy registration before any new capability ships
Time-bounded permission grantsPrevents creep accumulationOperational overhead of renewal workflows for long-running agents
Behavioral drift detectionCatches model-version behavioral changesBaseline must be re-established after intentional updates
Pipeline checkpoints per agentIsolates corruption to originating agentN checkpoints for N agents; schema maintenance burden grows

The latency constraint is real. If your governance middleware adds 200ms to every agent action, you will see pressure to bypass it. Keep validation synchronous but lightweight: policy lookup from an in-memory cache, permission check from a local Redis replica, audit write as a fire-and-forget with a local buffer. The goal is under 10ms p99 for the validation path.

Production Deployment Considerations

Baseline establishment. Before you can detect drift, you need a baseline. Run the agent in shadow mode for two to four weeks, recording behavioral samples without enforcing. Fingerprint the observed behavior. Promote it to the production baseline with a human sign-off. Treat baseline updates as deployment events, not configuration changes.

Approval token lifecycle. When an action escalates to human review, the approval token must have a short TTL (one hour is a reasonable default). After the TTL expires, the action requires re-escalation. Do not allow approval tokens to be reused across different action instances even if the action type and parameters are identical. An approved “delete record 42” should not implicitly approve “delete record 43.”

Audit log integrity. The audit log is the forensic record of everything the governance layer saw and decided. It needs to be append-only, with write access removed from the application role that queries it for reads. An agent that can modify the audit log can cover its tracks. Use a separate write role at the database level, and treat the log as tamper-evident by chaining records with a hash of the previous entry.

Multi-agent orchestration. When agents spawn child agents, the child’s permission scope must be the intersection of the parent’s scope and the child’s own registered permissions. A parent agent cannot delegate permissions it does not hold, and it cannot promote a child to a broader scope than its own. This is the structural rule that prevents a compromised orchestrator from bootstrapping elevated access for its subordinates.

What This Does Not Solve

Runtime governance enforces policy at execution time. It does not write good policy. A policy that says “allow all production writes” is compliant with this architecture and completely wrong. The governance layer is only as strong as the policies feeding it.

It also does not address prompt injection at the model layer. An adversarial input that convinces the agent to request a permitted action is still a problem, but it is a different problem: output validation and instruction hierarchy enforcement at the reasoning layer. Those controls and runtime governance are complementary, not substitutes.

Finally, this architecture assumes your action execution layer is structured: discrete action types, named resources, typed parameters. Agents that operate as raw shell executors or unrestricted code runners do not have discrete actions to intercept. Governance requires that you define what actions exist before you can write policy for them. If your agent architecture does not have that structure yet, the first step is not governance middleware; it is action abstraction.

The Core Insight

The Amazon Kiro incident is easy to reduce to “misconfigured access controls,” and Amazon’s initial statement did exactly that. But the more precise characterization is that there was no execution-time layer asking whether this action, by this agent, against this resource, was within the approved behavioral envelope for this context.

That layer is what this article builds. It is not a model improvement, a prompt change, or a reliability fix. It is infrastructure that treats every agent action as a security boundary crossing, requiring explicit authorization from a policy that a human registered, against a permission scope that a human granted, within a behavioral envelope that a human fingerprinted.

That is the governance gap between 82% executive confidence and 14.4% actual production clearance. It closes with enforcement, not observation.

More in AI / ML

How Mixture of Experts Works: Sparse Gating, Expert Routing, and the Architecture Behind Efficient Large Language Models
AI / ML ·

How Mixture of Experts Works: Sparse Gating, Expert Routing, and the Architecture Behind Efficient Large Language Models

A deep dive into MoE architecture: how the gating network routes tokens to experts, top-k selection, load balancing losses, capacity factor, token dropping, expert parallelism for serving, and the real production tradeoffs between dense transformers and sparse MoE models.

AI Agent Frameworks Compared: CrewAI, LangGraph, AutoGen, and Mastra for Production Systems
AI / ML ·

AI Agent Frameworks Compared: CrewAI, LangGraph, AutoGen, and Mastra for Production Systems

A practical comparison of CrewAI, LangGraph, AutoGen, and Mastra for building production AI agent systems. Covers architecture philosophy, state management, tool integration, observability, and deployment patterns with TypeScript code examples.

Google's Agent2Agent Protocol: How A2A Enables Cross-Framework Agent Communication in Production Systems
AI / ML ·

Google's Agent2Agent Protocol: How A2A Enables Cross-Framework Agent Communication in Production Systems

A deep dive into Google's Agent2Agent (A2A) protocol covering agent cards, task lifecycle, message parts, streaming via SSE, push notifications, and how A2A complements MCP. Includes TypeScript implementation examples, comparison with MCP and direct API integration, and production deployment patterns for multi-vendor agent ecosystems.

How Transformer Models Work: Self-Attention, Positional Encoding, and the Architecture Behind Modern LLMs
AI / ML ·

How Transformer Models Work: Self-Attention, Positional Encoding, and the Architecture Behind Modern LLMs

A technical deep dive into the Transformer architecture: tokenization, positional encoding, self-attention with Q/K/V matrices, multi-head attention, the encoder-decoder split, training dynamics, and what it all means for engineers building on top of LLMs.