AID-H-017.007: Dual-LLM Isolation Pattern

Version 1.20261015 · Actionable sub-technique

Implement a Privileged-LLM and Quarantined-LLM split to isolate raw untrusted data from privileged planning and tool execution. The quarantined path alone may read raw web pages, email, retrieved documents, or tool output and has no privileged tools; it emits a bounded typed result. The privileged path plans from trusted instructions and validated typed data rather than raw untrusted content.

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

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.007: Dual-LLM Isolation Pattern | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version