AID-H-017.007: Dual-LLM Isolation Pattern
Pillars: app, infra, model
Lifecycle: building, operation
Scope boundary
Owns enforced separation of raw-content parsing from privileged planning through independent data, process, network, credential, and tool boundaries. A two-model pipeline alone is not a complete security boundary, and every proposed action still requires canonical authorization.
AID-H-016.001: AID-H-017.007 separates raw untrusted content into an unprivileged model path; AID-H-016.001 structurally marks and separates instructions from data inside a prompt. Process and credential isolation provide a stronger architectural boundary than prompt formatting, but both can apply.
AID-H-018: AID-H-017.007 limits which model path can see raw content or hold privileged tools; AID-H-018 authorizes the exact action proposed by the privileged path. Schema-valid typed output from the quarantined model can still be malicious or misleading and must not bypass authorization.
Warning
Additional Latency, Cost & Boundary-Bypass Risk
Dual-model designs add model calls and can increase latency and token cost, but the amount is deployment-specific. Measure the exact parsing-model, privileged-model, routing, caching, retry, and tool path against versioned service objectives. A schema-valid result from the unprivileged model can still be malicious, incomplete, or misleading. Keep deterministic validation, information-flow controls, network and credential isolation, canonical action authorization, and adversarial separation tests in place. Unsupported content, parser failure, or separation uncertainty must keep privileged actions blocked.
Implementation guidance
- AID-H-017.007-G001: Enforce independent data, process, network, credential, and execution boundaries between raw-content parsing, privileged planning, and final action authorization.
AID-H-017.007-G001
Security boundary
Use three separately deployable identities: a Q worker that can read raw untrusted content but has no tools, secrets, or privileged network path; a boundary service that validates a narrow domain schema and emits a signed/canonical envelope; and a P worker that receives only trusted task instructions plus that envelope. The executor remains separate and applies AID-H-018. A generic free-text summary is not a security boundary, so prefer domain-specific typed values and identifiers.
Strict boundary service
# File: boundary/validate_observations.py
from __future__ import annotations
import os
import hashlib
import json
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
def _required_positive_int(name: str) -> int:
value = int(os.environ[name])
if value <= 0:
raise RuntimeError(f"{name} must be a positive integer")
return value
OBSERVATION_BATCH_MAX_ITEMS = _required_positive_int("VERIFIED_H017_OBSERVATION_BATCH_MAX_ITEMS")
MODEL_ID_MAX_CHARS = _required_positive_int("VERIFIED_H017_MODEL_ID_MAX_CHARS")
PRODUCT_ID_MAX_CHARS = _required_positive_int("VERIFIED_H017_PRODUCT_ID_MAX_CHARS")
RAW_OUTPUT_MAX_UTF8_BYTES = _required_positive_int("VERIFIED_H017_RAW_OUTPUT_MAX_UTF8_BYTES")
PRICE_MAX_MINOR = _required_positive_int("VERIFIED_H017_PRICE_MAX_MINOR")
# price-observation.v1 requires non-empty collections and identifiers.
NON_EMPTY_OBSERVATION_COUNT = 1
NON_EMPTY_MODEL_ID_LENGTH = 1
NON_EMPTY_PRODUCT_ID_LENGTH = 1
class PriceObservation(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
source_id: str = Field(pattern=r"^[a-f0-9]{64}$")
product_id: str = Field(pattern=r"^[A-Z0-9_-]+$", min_length=NON_EMPTY_PRODUCT_ID_LENGTH, max_length=PRODUCT_ID_MAX_CHARS)
price_minor: int = Field(ge=0, le=PRICE_MAX_MINOR)
currency: Literal["USD", "EUR", "GBP", "JPY"]
observed_at: datetime
class QuarantinedOutput(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
observations: list[PriceObservation] = Field(min_length=NON_EMPTY_OBSERVATION_COUNT, max_length=OBSERVATION_BATCH_MAX_ITEMS)
class BoundaryEnvelope(BaseModel):
model_config = ConfigDict(extra="forbid", strict=True)
schema_id: Literal["price-observation.v1"]
q_model_id: str = Field(min_length=NON_EMPTY_MODEL_ID_LENGTH, max_length=MODEL_ID_MAX_CHARS)
source_content_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
observations: list[PriceObservation]
def validate_q_output(
raw_model_json: str,
*,
q_model_id: str,
raw_source: bytes,
) -> bytes:
if len(raw_model_json.encode("utf-8")) > RAW_OUTPUT_MAX_UTF8_BYTES:
raise ValueError("quarantined model output exceeds signed-policy byte limit")
parsed = QuarantinedOutput.model_validate_json(raw_model_json, strict=True)
envelope = BoundaryEnvelope(
schema_id="price-observation.v1",
q_model_id=q_model_id,
source_content_sha256=hashlib.sha256(raw_source).hexdigest(),
observations=parsed.observations,
)
return json.dumps(
envelope.model_dump(mode="json"),
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
Deployment separation
# Q worker: no Kubernetes API token and no access to tool/executor services.
apiVersion: v1
kind: Pod
metadata:
name: q-worker
labels: { app: q-worker }
spec:
automountServiceAccountToken: false
containers:
- name: q-worker
image: registry.internal/q-worker@sha256:6d82c4f12182569d73a7938ef7646c2b96e7dc5600ea42c705d7f45ac84cc09b
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities: { drop: ["ALL"] }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: q-worker-egress
spec:
podSelector:
matchLabels: { app: q-worker }
policyTypes: ["Ingress", "Egress"]
ingress:
- from:
- podSelector:
matchLabels: { app: boundary-broker }
egress:
# Permit DNS only to the cluster DNS pods; use the labels for the deployed DNS service.
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
- to:
- podSelector:
matchLabels: { app: approved-model-gateway }
ports:
- { protocol: TCP, port: 443 }
Action: give the Q identity no executor credential and no path to the P worker; give the P identity no raw-content store permission; permit transfer only through the validating broker; bind each envelope to source hash, Q model/version, schema and policy version; and send every P proposal through AID-H-018 immediately before execution. Validate container identity and network policy with runtime readback, not manifest presence alone.
Verification and evidence
Replay direct/indirect injections, extra fields, free-text action requests, type coercion, oversized batches, unsupported currencies, raw-content leakage, broker bypass, Q-to-executor traffic, P-to-raw-store reads, forged envelopes, and AID-H-018 bypass. Report PASS only when the complete measured population traverses the broker and denied paths fail at runtime. Unsupported content is INSUFFICIENT_DATA, validation or enforcement failure is ERROR, and any observed bypass is FAIL. Preserve image digests, workload identities, schema/policy/model versions, source/envelope hashes, network-policy readback, authorization decision, and adversarial-test results.
Keep the action safely bounded
Reconcile the complete population of every raw-content ingestion, Q output, broker transfer, P plan, and resulting authorization request; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.
Open-source tools
- Pydantic
- python-jsonschema
- Envoy Proxy
- Istio
- Linkerd
- LangChain
- LangGraph
Commercial / hosted tools
- AWS Key Management Service (AWS KMS)
- Azure Key Vault
- Google Cloud Key Management Service
Threat mappings
MITRE ATLAS
- AML.T0011 User Execution
- AML.T0110 AI Agent Tool Poisoning
- AML.T0110.002 AI Agent Tool Poisoning: Runtime Response
- AML.T0011.002 User Execution: Poisoned AI Agent Tool
- AML.T0051 LLM Prompt Injection
- AML.T0051.001 LLM Prompt Injection: Indirect
- AML.T0051.002 LLM Prompt Injection: Triggered
- AML.T0053 AI Agent Tool Invocation
- AML.T0099 AI Agent Tool Data Poisoning
- AML.T0067 LLM Trusted Output Components Manipulation
MAESTRO
- Compromised RAG Pipelines (L2)
- Input Validation Attacks (L3)
- Agent Tool Misuse (L7)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
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
- ASI06:2026 Memory & Context Poisoning
NIST Adversarial Machine Learning 2025
- NISTAML.015 Indirect Prompt Injection
- NISTAML.018 Prompt Injection
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.2 Tool Poisoning
- AISubtech-4.3.1 Schema Inconsistencies
- AITech-1.2 Indirect Prompt Injection
- AITech-12.1 Tool Exploitation
- AITech-12.2 Insecure Output Handling
- AITech-4.2 Context Boundary Attacks
- AITech-4.3 Protocol Manipulation
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection
- RA: Rogue Actions
- IIC: Insecure Integrated Component
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.1: Prompt inject
- Agents - Core 13.2: Tool Misuse
- Agents - Core 13.1: Memory Poisoning