Picking Up Where We Left Off
In Part 1, we established why post-execution safety fails and why pre-execution gates are an architectural necessity for AI systems that take real-world actions. Now we're going deeper: what does the internal structure of an action governance layer actually look like?
This isn't about specific tools or frameworks. This is about the structural components any team needs to implement if they want deterministic, auditable action governance.
The Four Components of Action Governance
An action governance layer has four distinct components that operate in sequence. Skip any one of them and you'll end up with gaps that compound under production load.
┌──────────────────────────────────────────────────────────┐
│ Action Governance Layer │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌───────────┐ │
│ │ Action │→ │ Policy │→ │ Decision│→ │ Execution │ │
│ │ Intake │ │ Resolver │ │ Engine │ │ Boundary │ │
│ └─────────┘ └──────────┘ └─────────┘ └───────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
Let's break each one down.
Component 1: Action Intake — Normalizing Intent
Before you can evaluate an action, you need a consistent representation of what the system intends to do. Raw LLM output isn't that. Tool calls aren't that. You need a normalized action intent structure.
The action intake component transforms whatever your upstream system produces into a standardized format your governance layer can evaluate.
class ActionIntent:
"""
Normalized representation of what the system wants to do.
This is the CONTRACT between your AI system and your
governance layer.
"""
def __init__(self):
self.action_type = None # category of action (read, write, delete, communicate)
self.target_resource = None # what system/data is being acted upon
self.requesting_context = None # who/what initiated this action
self.parameters = {} # action-specific details
self.timestamp = None # when this intent was resolved
self.trace_id = None # correlation back to original request
def normalize_intent(raw_action, source_context):
"""
Transform raw system output into evaluable intent.
This normalization is critical because:
1. Different upstream systems produce different formats
2. Policy evaluation needs consistent structure
3. Audit trails require standardized records
"""
intent = ActionIntent()
# Classify what TYPE of action this is
# (not the specific API call, but the semantic category)
intent.action_type = classify_action_semantics(raw_action)
# Identify WHAT is being acted upon
# (the resource, not the endpoint)
intent.target_resource = resolve_target(raw_action)
# Capture WHO/WHAT is asking for this
# (user context, session state, permission scope)
intent.requesting_context = source_context
# Preserve action-specific details for policy evaluation
intent.parameters = extract_evaluable_params(raw_action)
return intent
Why this matters: Without normalization, your policies must understand every possible format your AI system might produce. That creates brittle coupling between your LLM layer and your governance layer. When you change models or add new tool integrations, your policies break.
With normalization, policies evaluate against a stable contract regardless of what's upstream.
Component 2: Policy Resolver — Finding What Applies
Not every policy applies to every action. A policy resolver determines which policies are relevant given the specific action intent and its context.
This is where most teams make their first architectural mistake: they evaluate ALL policies against every action. At scale, this creates latency problems. More critically, it creates maintenance problems — when policies conflict, you need clear precedence rules.
class PolicyResolver:
"""
Determines which policies apply to a given action intent.
Key design decision: policies are resolved based on
action properties, not hardcoded to specific endpoints
or tool names. This makes the system resilient to
upstream changes.
"""
def resolve(self, intent, governance_context):
"""
Returns ordered list of applicable policies.
Order matters — first deny wins, constraints accumulate.
"""
applicable = []
# Layer 1: Universal policies (always apply)
# Example: "no action during maintenance window"
applicable.extend(
self.get_universal_policies(governance_context)
)
# Layer 2: Resource-specific policies
# Example: "PHI access requires active consent record"
applicable.extend(
self.get_resource_policies(intent.target_resource)
)
# Layer 3: Action-type policies
# Example: "delete actions require elevated context"
applicable.extend(
self.get_action_type_policies(intent.action_type)
)
# Layer 4: Context-specific policies
# Example: "after-hours actions limited to read-only"
applicable.extend(
self.get_contextual_policies(intent.requesting_context)
)
# Sort by precedence — deny policies evaluate first
return self.order_by_precedence(applicable)
The layered resolution pattern ensures that broad organizational policies (Layer 1) always apply, while specific resource and contextual policies add granularity. This mirrors how compliance actually works in regulated environments — there are baseline rules everyone follows, plus specific rules for specific situations.
Component 3: Decision Engine — Deterministic Evaluation
The decision engine takes the normalized intent and resolved policies, then produces a structured decision. This is the core of your governance layer, and it MUST be deterministic.
class DecisionEngine:
"""
Evaluates action intent against applicable policies.
CRITICAL PROPERTY: Given the same intent and policy set,
this engine MUST produce the same decision every time.
No randomness. No probabilistic inference. No LLM calls.
Why? Because governance decisions need to be:
- Reproducible (for audit)
- Explainable (for users and regulators)
- Testable (for CI/CD)
"""
def evaluate(self, intent, policies, context):
decision_trace = [] # Record every evaluation step
accumulated_constraints = []
for policy in policies:
# Each policy evaluates independently
result = policy.evaluate(intent, context)
# Record this evaluation for audit trail
decision_trace.append(PolicyEvaluation(
policy_id=policy.id,
policy_version=policy.version,
input_hash=hash(intent, context),
result=result
))
# First DENY wins — stop evaluation
if result.outcome == "deny":
return GateDecision(
outcome="deny",
triggering_policy=policy.id,
reasoning=result.explanation,
trace=decision_trace,
escalation=result.escalation_path
)
# DEFER pauses evaluation for human review
if result.outcome == "defer":
return GateDecision(
outcome="defer",
triggering_policy=policy.id,
reasoning=result.explanation,
trace=decision_trace,
review_context=result.review_payload
)
# ALLOW may carry constraints
if result.constraints:
accumulated_constraints.extend(result.constraints)
# All policies passed — action is allowed with constraints
return GateDecision(
outcome="allow",
constraints=merge_constraints(accumulated_constraints),
trace=decision_trace
)
Key design decision: First-deny-wins. This is intentional. In governance, a single applicable policy that says "no" should override any number of policies that say "yes." This matches how regulatory compliance works — you need ALL applicable rules to pass, not a majority vote.
Component 4: Execution Boundary — The Actual Enforcement Point
The execution boundary is where the decision becomes enforcement. This is the physical point in your architecture where allowed actions proceed and denied actions stop.
class ExecutionBoundary:
"""
The enforcement point. Nothing passes without a decision.
This component has ONE job: enforce the gate decision.
It does not evaluate. It does not interpret. It enforces.
Architectural constraint: there must be NO path from
intent to execution that bypasses this boundary.
"""
def enforce(self, intent, decision, action_executor):
# Record enforcement event (regardless of outcome)
audit_record = self.create_audit_record(intent, decision)
if decision.outcome == "deny":
# Action stops here. Period.
self.emit_denial_event(intent, decision)
self.store_audit(audit_record)
return RefusalResponse(
reason=decision.reasoning,
policy=decision.triggering_policy,
trace_id=intent.trace_id
)
if decision.outcome == "defer":
# Action queued for human review
self.emit_deferral_event(intent, decision)
self.queue_for_review(intent, decision)
self.store_audit(audit_record)
return DeferralResponse(
reason=decision.reasoning,
review_id=generate_review_id(),
trace_id=intent.trace_id
)
if decision.outcome == "allow":
# Execute with constraints applied
constrained_action = apply_constraints(
intent, decision.constraints
)
result = action_executor.execute(constrained_action)
# Record successful execution
audit_record.execution_result = result
self.store_audit(audit_record)
return result
Why separate enforcement from evaluation? Because they have different failure modes. If your decision engine has a bug, you want to fix evaluation logic without touching execution paths. If your execution boundary has a latency issue, you want to optimize enforcement without risking policy logic changes.
Separation of concerns isn't just clean architecture — it's operational safety.
The Audit Trail: Your Regulatory Lifeline
Every component above produces audit data. When a regulator asks "why did your system do X?" or "why did your system refuse Y?", your answer is the complete decision trace:
- What action was intended (normalized intent)
- Which policies applied (resolver output)
- How each policy evaluated (decision trace)
- What enforcement action was taken (boundary record)
- What constraints were applied (if allowed)
This isn't optional logging. This is the architectural proof that your system has governance.
class GovernanceAuditRecord:
"""
Complete record of a governance decision.
This record must be:
- Immutable (no post-hoc modification)
- Complete (captures full decision context)
- Queryable (supports compliance reporting)
- Tamper-evident (hash chain or similar)
"""
trace_id: str # Correlation to original request
timestamp: datetime # When decision was made
intent: ActionIntent # What was attempted
resolved_policies: list # What policies applied
decision_trace: list # How each policy evaluated
final_decision: GateDecision # The outcome
enforcement_action: str # What happened at boundary
execution_result: any # Result (if allowed)
Common Implementation Mistakes
Having seen teams attempt this pattern, here are the pitfalls:
Mistake 1: Putting policy logic in the LLM prompt. Your system prompt is not a governance layer. It's a suggestion to a probabilistic system. Policies must be externalized and deterministically evaluated.
Mistake 2: Using the LLM to evaluate its own actions. If you're asking GPT-4 whether GPT-4's proposed action is safe, you've built a system that can talk itself into anything. Gate evaluation must be independent of action generation.
Mistake 3: Building the gate as an afterthought. If your system already executes actions and you're trying to bolt on governance, you'll discover bypass paths everywhere. Pre-execution gates work best when designed into the architecture from the start.
Mistake 4: Ignoring the "defer" outcome. Allow/deny is easy. Defer — "this action needs a human to decide" — is where real governance lives. Without escalation paths, your gate will either be too permissive or too restrictive.
Tradeoffs at This Layer
Policy maintenance is ongoing work. Policies aren't "set and forget." As your system's capabilities expand, policies need to expand with them. Budget for policy engineering as a continuous activity.
Testing governance is different from testing features. You need adversarial testing — "can I construct an intent that bypasses policy X?" This requires a different testing mindset than functional testing.
Performance at scale. With thousands of actions per minute, gate evaluation latency matters. Policy resolution needs caching strategies. Decision engines need optimization. Plan for this from the architecture phase.
What's Next
Part 3 dives into refusal infrastructure — how to architect principled refusal as a first-class system behavior, not an error state. When your gate says "no," what happens next determines whether your system is governable or just filtered.
These patterns represent educational architectural concepts. Production implementations require domain-specific policy design, performance optimization, and integration considerations unique to each deployment context.
Designing action governance for AI systems in regulated environments? We've shipped this architecture across healthcare, finance, and enterprise. Connect with us at Tailored Techworks on LinkedIn — we talk architecture, not marketing.
Top comments (0)