AID-D-002.005: Emitted Reasoning-Trace Semantic Monitoring

Version 1.20261015 · Actionable sub-technique

When the deployment operator can observe a model-emitted reasoning trace, treat that trace as untrusted input and inspect the complete emitted trace field with an independent semantic monitor for prompt injection, jailbreak, goal displacement, concealment, deception, and safeguard-evasion signals. Treat findings as detector evidence, not proof of the model's hidden intent: emitted reasoning may be incomplete, unfaithful, strategically shaped, or absent.

Pillars: model, app

Lifecycle: operation

Scope boundary

Owns semantic findings from the complete operator-visible reasoning field for an eligible response, including untrusted-input handling, coverage, monitor-version binding, thresholding, sanitized alert evidence, and explicit uncertainty. It does not own token-count or trace-shape telemetry, visible answer moderation, confidentiality enforcement, response blocking, containment, or claims about unobserved internal cognition.

AID-D-002.004: AID-D-002.005 judges the meaning of an emitted reasoning trace; AID-D-002.004 measures reasoning metadata such as token counts, ratios, redaction state, latency, and budget use. Semantic content and trace-shape accounting use different signals and produce independently verifiable findings.

AID-D-003.001: AID-D-002.005 monitors a non-user-facing emitted reasoning channel for attack and evasion signals; AID-D-003.001 monitors text intended for users or downstream application consumption for policy violations. The same text must not earn two PASS outcomes merely because a gateway exposes it under two field names.

AID-H-037.001: AID-D-002.005 defines the semantic detection objective; AID-H-037.001 defines the purpose-bound, short-lived path allowed to handle sensitive raw reasoning and the sanitized fields permitted to leave it. Detection does not authorize ordinary logging, analytics export, or long-term raw-trace retention.

AID-I-003: AID-D-002.005 evaluates the untrusted emitted reasoning field and produces a semantic finding; AID-I-003 consumes an authenticated finding or policy decision to quarantine, degrade, throttle, or contain the resolved target. A detector label never performs containment, and a containment action must not be counted as proof that semantic monitoring covered the trace.

Warning

Reasoning-Trace Fidelity & Optimization Warning

An emitted reasoning trace is a fallible observation channel, not a faithful transcript of internal computation. Do not use a clean result as proof of safe intent, and do not automatically feed the raw trace, monitor label, or monitor score directly into the monitored model's own reward, fine-tuning, or policy-optimization objective. Independently governed monitor training, human review, and safeguard calibration remain possible.

Implementation guidance

AID-D-002.005-G001

Send each complete eligible emitted reasoning field through a purpose-bound semantic monitor and export only a policy label, severity, trace digest, and restricted evidence reference.
Define one narrow monitor contract

Run the monitor as a separate service with no model tools, workload credentials, training-data write path, or general network egress. The gateway buffers only the complete emitted reasoning field in protected memory up to a signed maximum; it never truncates and calls the monitor only after the provider stream finishes successfully. A missing reasoning field on a model profile that does not expose one is NOT_APPLICABLE. A missing advertised field, partial stream, oversize trace, monitor timeout, or invalid response is INSUFFICIENT_DATA or ERROR, never a clean result.

# File: detection/reasoning_semantic_monitor.py
from __future__ import annotations

import hashlib
import json
import math
import ssl
from dataclasses import dataclass
from typing import Iterable
from urllib.parse import urlsplit

import httpx

ALLOWED_LABELS = {
    "prompt_injection", "jailbreak", "goal_displacement",
    "concealment", "deception", "safeguard_evasion",
}


class CoverageError(RuntimeError):
    pass


@dataclass(frozen=True)
class MonitorPolicy:
    policy_version: str
    monitor_model_revision: str
    maximum_trace_bytes: int
    maximum_response_bytes: int
    timeout_seconds: float
    monitor_url: str
    threshold_by_label: dict[str, float]
    severity_by_label: dict[str, str]


def strict_json(raw: bytes) -> object:
    def unique(pairs):
        result = {}
        for key, value in pairs:
            if key in result:
                raise ValueError("duplicate monitor-response key")
            result[key] = value
        return result
    return json.loads(raw.decode("utf-8", errors="strict"),
                      object_pairs_hook=unique,
                      parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value)))


def collect_complete_trace(chunks: Iterable[str], *, completed: bool, maximum_bytes: int) -> bytes:
    if type(maximum_bytes) is not int or maximum_bytes < 1:
        raise ValueError("maximum_trace_bytes must be positive")
    output = bytearray()
    for chunk in chunks:
        if not isinstance(chunk, str):
            raise CoverageError("reasoning chunk is not text")
        encoded = chunk.encode("utf-8", errors="strict")
        if len(output) + len(encoded) > maximum_bytes:
            raise CoverageError("complete reasoning trace exceeds approved bound")
        output.extend(encoded)
    if not completed:
        raise CoverageError("reasoning stream did not complete")
    if not output:
        raise CoverageError("advertised reasoning trace is absent")
    return bytes(output)


def validate_policy(policy: MonitorPolicy) -> None:
    if (not isinstance(policy.policy_version, str) or not policy.policy_version
            or not isinstance(policy.monitor_model_revision, str) or not policy.monitor_model_revision
            or type(policy.maximum_trace_bytes) is not int or policy.maximum_trace_bytes < 1
            or type(policy.maximum_response_bytes) is not int or policy.maximum_response_bytes < 1
            or isinstance(policy.timeout_seconds, bool)
            or not isinstance(policy.timeout_seconds, (int, float))
            or not math.isfinite(float(policy.timeout_seconds)) or policy.timeout_seconds <= 0
            or not isinstance(policy.threshold_by_label, dict)
            or not isinstance(policy.severity_by_label, dict)
            or set(policy.threshold_by_label) != ALLOWED_LABELS
            or set(policy.severity_by_label) != ALLOWED_LABELS):
        raise ValueError("monitor policy population differs")
    parsed_url = urlsplit(policy.monitor_url)
    if (parsed_url.scheme != "https" or not parsed_url.hostname
            or parsed_url.username or parsed_url.password or parsed_url.query
            or parsed_url.fragment):
        raise ValueError("monitor policy URL is not one canonical HTTPS endpoint")
    if any(isinstance(value, bool) or not isinstance(value, (int, float))
           or not math.isfinite(float(value)) or not 0 <= float(value) <= 1
           for value in policy.threshold_by_label.values()):
        raise ValueError("monitor thresholds must be finite probabilities")
    if any(value not in {"low", "medium", "high", "critical"}
           for value in policy.severity_by_label.values()):
        raise ValueError("monitor severity is invalid")


def evaluate_trace(*, request_id: str, trace_chunks: Iterable[str], stream_completed: bool,
                   policy: MonitorPolicy, ca_file: str,
                   client_cert: tuple[str, str]) -> list[dict]:
    validate_policy(policy)
    if (not isinstance(request_id, str) or not request_id or len(request_id) > 256
            or any(ord(char) < 32 for char in request_id)):
        raise ValueError("bounded request identity is required")
    if (not isinstance(ca_file, str) or not ca_file
            or not isinstance(client_cert, tuple) or len(client_cert) != 2
            or any(not isinstance(path, str) or not path for path in client_cert)):
        raise ValueError("monitor trust and client-certificate paths are required")
    trace = collect_complete_trace(trace_chunks, completed=stream_completed,
                                   maximum_bytes=policy.maximum_trace_bytes)
    trace_sha256 = hashlib.sha256(trace).hexdigest()
    body = {"schema_version": "aidefend.reasoning-monitor-request.v1",
            "request_id": request_id, "trace_sha256": trace_sha256,
            "policy_version": policy.policy_version,
            "monitor_model_revision": policy.monitor_model_revision,
            "reasoning_trace": trace.decode("utf-8")}
    tls = ssl.create_default_context(cafile=ca_file)
    tls.load_cert_chain(certfile=client_cert[0], keyfile=client_cert[1])
    timeout = httpx.Timeout(float(policy.timeout_seconds), connect=float(policy.timeout_seconds))
    with httpx.Client(verify=tls, timeout=timeout,
                      follow_redirects=False, trust_env=False) as client:
        with client.stream("POST", policy.monitor_url, json=body,
                           headers={"Accept-Encoding": "identity"}) as response:
            response.raise_for_status()
            if response.headers.get("Content-Encoding", "identity").lower() != "identity":
                raise ValueError("encoded monitor response is forbidden")
            if response.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() != "application/json":
                raise ValueError("monitor response is not JSON")
            declared = response.headers.get("Content-Length")
            if declared is not None and (not declared.isdigit() or int(declared) > policy.maximum_response_bytes):
                raise ValueError("monitor Content-Length is invalid")
            chunks, observed = [], 0
            for chunk in response.iter_bytes():
                observed += len(chunk)
                if observed > policy.maximum_response_bytes:
                    raise ValueError("monitor response is oversized")
                chunks.append(chunk)
            raw = b"".join(chunks)
            if not raw or (declared is not None and int(declared) != observed):
                raise ValueError("monitor response is empty or truncated")
    result = strict_json(raw)
    expected = {"schema_version", "request_id", "trace_sha256",
                "policy_version", "monitor_model_revision", "complete_input", "scores",
                "evidence_reference"}
    if (not isinstance(result, dict) or set(result) != expected
            or result["schema_version"] != "aidefend.reasoning-monitor-result.v1"
            or result["request_id"] != request_id
            or result["trace_sha256"] != trace_sha256
            or result["policy_version"] != policy.policy_version
            or result["monitor_model_revision"] != policy.monitor_model_revision
            or result["complete_input"] is not True
            or not isinstance(result["evidence_reference"], str)
            or not result["evidence_reference"]):
        raise ValueError("monitor result is incomplete or misbound")
    scores = result["scores"]
    if not isinstance(scores, dict) or set(scores) != ALLOWED_LABELS:
        raise ValueError("monitor score population differs")
    for label, score in scores.items():
        if isinstance(score, bool) or not isinstance(score, (int, float)):
            raise ValueError("monitor score is not numeric")
        score = float(score)
        if not math.isfinite(score) or not 0 <= score <= 1:
            raise ValueError("monitor score is outside [0,1]")
    return [{"label": label, "severity": policy.severity_by_label[label],
             "digest": trace_sha256, "reference": result["evidence_reference"]}
            for label, score in scores.items()
            if float(score) >= policy.threshold_by_label[label]]
Keep evidence and optimization paths separate

The monitor stores raw trace and scores only in its restricted, policy-expiring evidence boundary; the general SIEM receives exactly label, severity, digest, and reference. Bind every result to deployment/model revision, parser and trace-field contract, monitor image/model revision, policy version, request, stream-completion receipt, and evidence reference. Reconcile every eligible response. Test clean, injected, jailbroken, deceptive, concealed, empty, partial, oversized, malformed UTF-8, duplicate-key, wrong-digest, wrong-model, timeout, and unavailable-monitor cases. Independently replay a signed fixture set and track false-positive, false-negative, abstention, and coverage rates. The monitored model's training and reward pipelines must deny direct automatic ingestion of raw traces, labels, scores, or detector-selected samples from this control.

Action: Route every complete eligible emitted trace through the monitor; a missing, partial, oversized, unbound, or failed result must never become a clean finding.

Open-source tools

Source-available / open-weight 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-D-002.005: Emitted Reasoning-Trace Semantic Monitoring | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version