AID-H-002.008: Realtime Multimodal Input Stream Security
Pillars: data, app
Lifecycle: building, operation
Scope boundary
Owns safe decoding, normalization, segmentation, bounded sampling, control-token handling, and the preventive admit, block, or downgrade decision for realtime microphone, camera, call, video, and screen-share streams.
AID-H-002.003: AID-H-002.008 gates ephemeral streams incrementally; AID-H-002.003 sanitizes complete non-realtime files and artifacts. Live timing, transcript churn, and source changes require decisions before stable artifact bytes exist.
AID-D-007.002: AID-H-002.008 owns preventive realtime stream admission and downgrade; AID-D-007.002 emits semantic, temporal, source-change, and action-timing findings over the normalized stream. The detector informs policy, while this Harden control decides whether stream observations may reach the model or agent.
Implementation guidance
- AID-H-002.008-G001: Normalize live audio to one canonical PCM profile, segment speech turns with bounded voice activity detection, and reject segments that exceed duration, confidence, or transcript-control-token policy.
- AID-H-002.008-G002: Gate realtime video and screen-share frames with bounded frame sampling, OCR normalization, context allowlists, and high-risk action downgrade before visual stream content enters prompt or tool context.
AID-H-002.008-G001
Concept:
Audio is one realtime multimodal stream, not a special taxonomy category. Treat microphone and voice-mode input as an incremental stream that must be normalized, segmented, and gated before the transcript can become model instructions, tool parameters, or memory. The same admission contract should be used alongside camera, video, or screen-share gates in this sub-technique.
Step 1: Normalize and segment audio before transcription
# File: realtime_input/audio_gate.py
from __future__ import annotations
import os
import io
import re
from dataclasses import dataclass
import webrtcvad
from pydub import AudioSegment
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
SAMPLE_RATE = _required_positive_int("VERIFIED_H002_REALTIME_AUDIO_SAMPLE_RATE_HZ")
FRAME_MS = _required_positive_int("VERIFIED_H002_REALTIME_AUDIO_FRAME_MILLISECONDS")
MAX_SEGMENT_MS = _required_positive_int("VERIFIED_H002_REALTIME_AUDIO_MAX_SEGMENT_MILLISECONDS")
MAX_STREAM_MS = _required_positive_int("VERIFIED_H002_REALTIME_AUDIO_MAX_STREAM_MILLISECONDS")
VAD_MODE = _required_positive_int("VERIFIED_H002_REALTIME_AUDIO_VAD_MODE")
# WebRTC VAD supports only these named PCM protocol profiles.
if SAMPLE_RATE not in {8000, 16000, 32000, 48000} or FRAME_MS not in {10, 20, 30} or VAD_MODE not in {0, 1, 2, 3}:
raise RuntimeError("verified realtime audio profile is unsupported by WebRTC VAD")
CONTROL_TOKEN_RE = re.compile(r"(<\|/?(?:system|developer|assistant|tool).*?\|>|\b(system|developer)\s*:)", re.I)
@dataclass(frozen=True)
class VoiceSegment:
pcm16le: bytes
start_ms: int
end_ms: int
def normalize_audio(blob: bytes, input_format: str) -> AudioSegment:
if not blob:
raise ValueError("empty_audio_stream")
source = AudioSegment.from_file(io.BytesIO(blob), format=input_format)
if len(source) > MAX_STREAM_MS:
raise ValueError("audio_stream_too_long")
return source.set_frame_rate(SAMPLE_RATE).set_channels(1).set_sample_width(2)
def split_speech_segments(blob: bytes, input_format: str) -> list[VoiceSegment]:
audio = normalize_audio(blob, input_format)
vad = webrtcvad.Vad(VAD_MODE)
frame_bytes = int(SAMPLE_RATE * FRAME_MS / 1000) * 2
pcm = audio.raw_data
segments: list[VoiceSegment] = []
current = bytearray()
current_start_ms: int | None = None
for offset in range(0, len(pcm) - frame_bytes + 1, frame_bytes):
frame = pcm[offset:offset + frame_bytes]
frame_start_ms = int(offset / 2 / SAMPLE_RATE * 1000)
speech = vad.is_speech(frame, SAMPLE_RATE)
if speech:
if current_start_ms is None:
current_start_ms = frame_start_ms
current.extend(frame)
if (frame_start_ms - current_start_ms) > MAX_SEGMENT_MS:
raise ValueError("voice_segment_too_long")
elif current_start_ms is not None:
segments.append(VoiceSegment(bytes(current), current_start_ms, frame_start_ms))
current.clear()
current_start_ms = None
if current_start_ms is not None:
end_ms = int(len(pcm) / 2 / SAMPLE_RATE * 1000)
segments.append(VoiceSegment(bytes(current), current_start_ms, end_ms))
if not segments:
raise ValueError("no_speech_detected")
return segments
Step 2: Gate the ASR transcript before model fusion
# File: realtime_input/transcript_gate.py
from __future__ import annotations
import os
from realtime_input.audio_gate import CONTROL_TOKEN_RE
from multimodal.stream_findings import admission_reasons
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
MAX_TRANSCRIPT_CHARS = _required_positive_int("VERIFIED_H002_REALTIME_TRANSCRIPT_MAX_CHARS")
def admit_voice_transcript(
transcript: str,
workflow_risk: str,
expected_session_id: str,
expected_turn_index: int,
d007_finding: dict,
expected_policy_version: str,
expected_policy_sha256: str,
) -> dict:
cleaned = transcript.replace("\x00", "").strip()
reasons: list[str] = []
if not cleaned:
reasons.append("empty_transcript")
if len(cleaned) > MAX_TRANSCRIPT_CHARS:
reasons.append("transcript_too_long")
if CONTROL_TOKEN_RE.search(cleaned):
reasons.append("control_token_or_role_marker")
reasons.extend(admission_reasons(
d007_finding, observation={"transcript": transcript},
observation_id=f"audio:{expected_turn_index}", modality="audio",
session_id=expected_session_id, policy_version=expected_policy_version,
policy_sha256=expected_policy_sha256))
decision = "allow" if not reasons else "block"
return {
"decision": decision,
"reasons": reasons,
"normalized_transcript": cleaned if decision == "allow" else "",
}Operational notes
- Do not use speaker, liveness, or ASR confidence as proof of identity. D-007 may use those values as calibrated risk signals; this gate consumes its structured finding.
- Run the final gate after D-007 scores the normalized observation and before the transcript is merged with system prompts, retrieved context, tool parameters, or agent memory.
- Keep rejected segment metadata, finding/evidence digests, and the admission decision; retain raw audio only when signed privacy and retention policy permits it.
Action: Normalize and bound each speech turn, pass the normalized observation to AID-D-007.002, then require a valid finding plus deterministic transcript checks before admitting speech as instructions.
Consume the signed aidefend.stream-admission-finding.v1 record produced by AID-D-007.002-G003. The trusted receiver verifies the detector identity, signature and freshness; it supplies the expected policy from current server configuration. Audio binds the original transcript object; visual binds the complete VisualStreamFrame object before normalization. Never accept the finding or policy from a client request.
AID-H-002.008-G002
Concept:
Realtime camera, live video, and screen-share streams are not ordinary uploaded images. The system sees them incrementally, and the dangerous text may appear for only a few frames before a tool action is proposed. Sample frames at a bounded rate, extract OCR text only when needed, and fail closed when visual context changes during high-impact actions.
Step 1: Normalize frame observations into an admission record
# File: realtime_input/visual_stream_gate.py
from __future__ import annotations
import os
import re
from dataclasses import dataclass, asdict
from multimodal.stream_findings import admission_reasons
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
MAX_FRAME_PIXELS = _required_positive_int("VERIFIED_H002_REALTIME_FRAME_MAX_PIXELS")
MAX_OCR_CHARS = _required_positive_int("VERIFIED_H002_REALTIME_OCR_MAX_CHARS")
CONTROL_TOKEN_RE = re.compile(r"(<\|/?(?:system|developer|assistant|tool).*?\|>|\b(system|developer)\s*:)", re.I)
ALLOWED_STREAM_TYPES = {"camera", "live_video", "screen_share"}
@dataclass(frozen=True)
class VisualStreamFrame:
session_id: str
frame_index: int
stream_type: str
width: int
height: int
ocr_text: str
visual_confidence: float
context_label: str
workflow_risk: str
def admit_visual_frame(
frame: VisualStreamFrame,
allowed_context_labels: set[str],
d007_finding: dict,
expected_policy_version: str,
expected_policy_sha256: str,
) -> dict:
reasons: list[str] = []
normalized_ocr = re.sub(r"\s+", " ", frame.ocr_text).strip()
if frame.stream_type not in ALLOWED_STREAM_TYPES:
reasons.append("stream_type_not_allowed")
if frame.width * frame.height > MAX_FRAME_PIXELS:
reasons.append("frame_too_large")
if len(normalized_ocr) > MAX_OCR_CHARS:
reasons.append("ocr_text_too_long")
if CONTROL_TOKEN_RE.search(normalized_ocr):
reasons.append("control_token_or_role_marker")
if frame.context_label not in allowed_context_labels:
reasons.append("visual_context_not_allowed")
reasons.extend(admission_reasons(
d007_finding, observation=asdict(frame),
observation_id=f"visual:{frame.frame_index}", modality="visual",
session_id=frame.session_id, policy_version=expected_policy_version,
policy_sha256=expected_policy_sha256))
decision = "allow" if not reasons else "block"
return {
"decision": decision,
"reasons": reasons,
"session_id": frame.session_id,
"frame_index": frame.frame_index,
"normalized_ocr": normalized_ocr if decision == "allow" else "",
"context_label": frame.context_label,
}Step 2: Bind stream findings and gates to action timing
Send each normalized frame observation to AID-D-007.002. If a tool call, payment, message send, file export, or identity action is proposed after a visual context switch, require a fresh D-007 finding bound to the same session/frame and a fresh allowed admission decision before passing visual text into the action planner. Never reuse a result from an earlier screen, camera angle, tab, or participant.
Verification: Replay the complete sampled-frame population, signed admission policy, D-007 visual finding, OCR normalization output, and final decision; confirm the assembled context excludes every denied frame.
Action: Treat realtime visual content as per-frame preprocessing facts, D-007 findings, and H-002.008 admission records. Bound OCR and frame dimensions locally, consume the semantic detector finding, and force high-impact actions to use only fresh, exactly bound allow decisions.
Consume the signed aidefend.stream-admission-finding.v1 record produced by AID-D-007.002-G003. The trusted receiver verifies the detector identity, signature and freshness; it supplies the expected policy from current server configuration. Audio binds the original transcript object; visual binds the complete VisualStreamFrame object before normalization. Never accept the finding or policy from a client request.
Open-source tools
- FFmpeg
- pydub
- librosa
- WebRTC VAD (py-webrtcvad)
- OpenCV
- Tesseract OCR
- Silero VAD (voice-activity segmentation only; stream normalization, control-token checks, and admission remain external)
Source-available / open-weight tools
- OpenAI Whisper (MIT; open-weight)
Commercial / hosted tools
- Azure AI Speech
- Amazon Transcribe
- Google Cloud Speech-to-Text
- Deepgram
- AssemblyAI
- Azure AI Content Safety
- Amazon Rekognition
- Google Cloud Vision API
Threat mappings
MITRE ATLAS
- AML.T0051 LLM Prompt Injection (realtime audio, video, or screen-share streams can carry prompt-injection payloads)
- AML.T0051.001 LLM Prompt Injection: Indirect (live external media can inject instructions through transcript or OCR paths)
- AML.T0043 Craft Adversarial Data (adversarial audio or visual stream content can manipulate recognition or downstream intent)
- AML.T0015 Evade AI Model (audio/video perturbations can evade transcription, OCR, or moderation controls)
- AML.T0052.001 Phishing: Deepfake-Assisted Phishing (realtime stream gates can downgrade suspected cloned, manipulated, or spoofed media before agent action)
- AML.T0129 Triggers in Multimodal Inputs (live transcript and OCR admission gates inspect modality-derived instructions before fusion)
MAESTRO
- Input Validation Attacks (L3)
- Adversarial Examples (L1)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
- LLM06:2026 Unbounded Consumption
OWASP ML Top 10 2023
- ML01:2023 Input Manipulation Attack
OWASP Top 10 for Agentic Applications 2026
- ASI01:2026 Agent Goal Hijack
NIST Adversarial Machine Learning 2025
- NISTAML.018 Prompt Injection
- NISTAML.022 Evasion
- NISTAML.015 Indirect Prompt Injection (realtime media streams can carry injected instructions from external speakers, screens, or video content)
Cisco Integrated AI Security and Safety Framework
- AISubtech-1.1.4 Token Exploitation
- AISubtech-1.3.1 Image-Text Injection
- AISubtech-1.3.3 Audio Command Injection
- AISubtech-1.3.4 Video Overlay Manipulation
- AISubtech-13.1.2 Memory Flooding
- AITech-1.1 Direct Prompt Injection and Jailbreak (Prompt-based Manipulation)
- AITech-1.3 Multimodal Injection and Manipulation
- AITech-13.1 Disruption of Availability
Google Secure AI Framework 2.0 - Risks
- PIJ: Prompt Injection
- MEV: Model Evasion
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.1: Prompt inject
- Model Serving - Inference requests 9.9: Input Resource Control
- Agents - Core 13.6: Intent Breaking & Goal Manipulation