AID-H-017.003: Decoupled Plan-Then-Execute Architecture
Pillars: app
Lifecycle: building, operation
Scope boundary
Owns architectural separation of non-side-effecting LLM planning from deterministic validation, transformation, action selection, and dispatch. The model proposes a typed plan but cannot directly invoke real tools.
AID-H-017.001: AID-H-017.003 separates planning from execution; AID-H-017.001 makes the overall loop interruptible and auditable at bounded state transitions. Decoupling the phases does not by itself provide breaker, pause, or termination guarantees.
AID-H-018: AID-H-017.003 routes model plans through a deterministic dispatcher; AID-H-018 authorizes the exact tool, parameter, value flow, approval, freshness, and side effect before execution. An Action Selector is an architectural enforcement point, not a substitute for the authorization policy family.
Implementation guidance
- AID-H-017.003-G001: Implement an 'Action Selector' middleware to gatekeep LLM plans.
AID-H-017.003-G001
Concept:
The LLM should never have direct access to exec(), raw API clients, shells, or database drivers. Instead, it outputs a proposed_action structure (for example JSON with 'action' and 'parameters'). A deterministic Action Selector receives this proposal, validates it against an allowlist and context policy (for example: is this action allowed for this agent role and this user session?), and only then triggers the real tool. If validation fails, the action is rejected without execution.
Code: Action Selector Pattern
# File: agent/action_selector.py
from pathlib import Path
from typing import Any, Callable
import logging
# allowed_actions_by_role and tool_registry are loaded from the verified signed policy.
def _is_safe_path(path_str: str) -> bool:
'''Reject traversal and sibling-prefix bypasses such as /srv/app/data-evil.'''
try:
base = Path('/srv/app/data').resolve()
target = (base / path_str).resolve()
return target == base or target.is_relative_to(base)
except (OSError, RuntimeError, TypeError, ValueError):
return False
def execute_plan(
agent_role: str,
plan: dict[str, Any],
*,
allowed_actions_by_role: dict[str, set[str]],
tool_registry: dict[str, Callable[[dict[str, Any]], dict[str, Any]]],
) -> dict[str, Any]:
action_name = plan.get('action')
params = plan.get('parameters') or {}
# 1. Deterministic gatekeeping (Action Selector)
allowed_actions = allowed_actions_by_role.get(agent_role, set())
if action_name not in allowed_actions:
logging.warning(
'BLOCKED: Agent %s tried unauthorized action %s',
agent_role,
action_name,
)
return {'error': 'Action not authorized by policy.'}
# 2. Parameter validation (for example, prevent path traversal)
path = params.get('path')
if path is not None and not _is_safe_path(str(path)):
logging.warning(
'BLOCKED: Agent %s attempted unsafe path access: %s',
agent_role,
path,
)
return {'error': 'Invalid path parameter.'}
# 3. Execution dispatch via registry
tool = tool_registry.get(str(action_name))
if tool is None:
logging.error('No registered tool for action: %s', action_name)
return {'error': 'Internal routing error.'}
return tool(params)
Verification
# File: tests/test_action_selector_paths.py
from agent.action_selector import _is_safe_path
def test_child_path_is_allowed() -> None:
assert _is_safe_path('tenant-a/report.txt') is True
def test_parent_traversal_is_rejected() -> None:
assert _is_safe_path('../secret.txt') is False
def test_sibling_prefix_bypass_is_rejected() -> None:
assert _is_safe_path('../data-evil/f') is False
Action: Refactor agent loops so the LLM output is parsed as data, not code. Pass this data through a deterministic execute_plan function that (1) checks the requested action against a role-based allowlist, (2) validates sensitive parameters such as paths and resource scopes with resolved path containment, and (3) dispatches only to tools registered in a safe registry.
Keep the action safely bounded
Reconcile the complete population of every plan emitted for the in-scope agent roles and every selector disposition; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.
Verify safely
A separately credentialed verifier independently replays complete plan ledger and selector decisions with unknown-action, policy drift, parameter mutation, traversal, command-text, and registry-mismatch fixtures, reads back effective state from the enforcement system, exercises positive, negative, boundary, stale-evidence, and dependency-failure fixtures, and reconciles every eligible item.
Open-source tools
- FastAPI
- Flask
- Open Policy Agent (OPA)
- Pydantic
- LangChain
- LangGraph
Commercial / hosted tools
- Kong Gateway Enterprise
- Kong Konnect
- Cerbos Hub
Threat mappings
MITRE ATLAS
- AML.T0050 Command and Scripting Interpreter
- AML.T0051 LLM Prompt Injection
- AML.T0051.000 LLM Prompt Injection: Direct
- AML.T0051.001 LLM Prompt Injection: Indirect
- AML.T0053 AI Agent Tool Invocation
- AML.T0054 LLM Jailbreak
- AML.T0100 AI Agent Clickbait (plan-execute separation prevents automatic navigation to malicious content)
- AML.T0102 Generate Malicious Commands (plan-execute separation prevents direct execution of generated commands)
- AML.T0051.002 LLM Prompt Injection: Triggered
- AML.T0117 Autonomous Attack-Path Adaptation (each replanned step is validated against safety policy before dispatch)
MAESTRO
- Agent Tool Misuse (L7)
- Input Validation Attacks (L3)
- Agent Goal Manipulation (L7) (plan validation catches manipulated goals before execution)
- Compromised Agents (L7) (decoupled architecture limits compromised agent actions to plan output only)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection (deterministic plan validation prevents injected model output from directly reaching side-effecting tools)
- LLM03:2026 Excessive Agency
- LLM08:2026 Hidden Context Exposure
- LLM10:2026 Improper Output Handling
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack
- ASI02:2026 Tool Misuse and Exploitation
- ASI05:2026 Unexpected Code Execution (RCE)
NIST Adversarial Machine Learning 2025
- NISTAML.015 Indirect Prompt Injection (decoupled architecture prevents injection from reaching tools)
- NISTAML.018 Prompt Injection
- NISTAML.039 Compromising connected resources (plan validation prevents reaching connected systems)
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.1 Parameter Manipulation
- AISubtech-12.1.3 Unsafe System / Browser / File Execution
- AISubtech-14.1.2 Insufficient Access Controls (Enforces the stated application access boundary; hosting infrastructure is a separate scope)
- AISubtech-2.1.2 Capability and Permission Overreach
- AISubtech-9.1.1 Code Execution
- AISubtech-9.1.2 Unauthorized or Unsolicited System Access
- AITech-12.1 Tool Exploitation
- AITech-12.2 Insecure Output Handling
- AITech-2.1 Excessive Agency
- AITech-9.1 Model or Agentic System Manipulation
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection (plan validation prevents injection from reaching tools)
- RA: Rogue Actions (decoupled architecture prevents rogue tool execution)
- IIC: Insecure Integrated Component (action selector validates all tool interactions)
- IMO: Insecure Model Output (plan validation catches insecure model outputs before execution)
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.1: Prompt inject
- Agents - Core 13.2: Tool Misuse (plan validation prevents tool misuse)
- Agents - Core 13.3: Privilege Compromise
- Agents - Core 13.6: Intent Breaking & Goal Manipulation (plan validation catches manipulated goals)
- Agents - Core 13.11: Unexpected RCE and Code Attacks (decoupled architecture prevents direct code execution)
- Model Serving - Inference requests 9.12: LLM Jailbreak (plan validation blocks jailbreak-driven actions)