AID-H-017.003: Decoupled Plan-Then-Execute Architecture

Version 1.20261015 · Actionable sub-technique

Architect the agent so that all high-level reasoning and task planning happen in a non-side-effecting LLM phase, separate from any code or tool execution. The LLM is restricted to outputting a structured plan (for example JSON with action names and parameters) and never calls tools directly. A deterministic Action Selector or Orchestrator then validates, transforms, and dispatches this plan against explicit safety policies and allowlists before invoking real tools. This combines planning and explanation separation from side-effectful actions, making it harder for hallucinated or injected instructions to directly reach infrastructure or sensitive APIs.

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.
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

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-017.003: Decoupled Plan-Then-Execute Architecture | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version