AID-D-002.005: Emitted Reasoning-Trace Semantic Monitoring
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.
AID-D-002.005-G001
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
- Hugging Face Transformers
- vLLM
- SGLang
- OpenTelemetry
- Prometheus
Source-available / open-weight tools
- Granite Guardian 4.1 (Apache-2.0; open-weight)
- gpt-oss-safeguard (Apache-2.0; open-weight)
Threat mappings
MITRE ATLAS
- AML.T0051 LLM Prompt Injection (reasoning-trace findings can reveal instruction displacement that was not visible in the final answer)
- AML.T0054 LLM Jailbreak (reasoning-trace findings can reveal safeguard-evasion behavior before or despite a benign-looking answer)
MAESTRO
- Evasion of Detection (L5) (the trace monitor supplies a second semantic observation channel)
- Agent Goal Manipulation (L7) (reasoning traces can reveal displaced goals or attacker-controlled planning)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection (semantic monitoring detects injection and jailbreak effects expressed in an emitted reasoning trace)
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack (trace findings can expose goal displacement in reasoning-capable agents)
NIST Adversarial Machine Learning 2025
- NISTAML.018 Prompt Injection
- NISTAML.027 Misaligned Outputs (reasoning-trace findings are a corroborating signal, not proof of internal alignment)
Cisco Integrated AI Security and Safety Framework
- AISubtech-2.2.2 Goal Substitution (semantic findings in reasoning traces actually exposed by the runtime)
- AITech-1.1 Direct Prompt Injection and Jailbreak (Prompt-based Manipulation) (semantic findings in reasoning traces actually exposed by the runtime)
- AITech-1.2 Indirect Prompt Injection (semantic findings in reasoning traces actually exposed by the runtime)
- AITech-2.2 Goal Drift (semantic findings in reasoning traces actually exposed by the runtime)
- AITech-7.1 Reasoning Corruption (semantic findings in reasoning traces actually exposed by the runtime)
- AITech-9.2 Detection Evasion (semantic findings in reasoning traces actually exposed by the runtime)
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.12: LLM Jailbreak
- Model Serving - Inference response 10.1: Lack of audit and monitoring inference quality
- Agents - Core 13.6: Intent Breaking & Goal Manipulation