/** * ToolOutputGuardrail — semantic screening of content returned by MCP tool * calls, memory reads, and external API responses before that content enters * agent reasoning. Closes the OWASP ASI01 (Agent Goal Hijacking) gap * identified in ruvnet/ruflo#2149 / ADR-131. * * Threat model * ------------ * Attackers embed malicious instructions in content the agent retrieves * autonomously (web page, MCP tool response, memory entry). An LLM cannot * reliably distinguish instructions from data once both are in the prompt. * System-level per-boundary guardrails are the only category with * sub-millisecond latency and no model dependency (arXiv:2601.17548, * Jan 2026 systematic review). * * Scope * ----- * - Detection only — does NOT modify the running prompt; callers decide * what action to take (allow, flag, redact, reject) via `scanAndEnforce`. * - Synchronous pattern match — designed for <1ms p99 on typical tool * responses (≤32KB). Large content is capped at `maxScanBytes` and the * truncation itself is reported as a low-severity finding. * - Pure-function shape — no I/O, no async, no model calls. Safe to invoke * in hot paths (every MCP tool result, every memory read). * * Non-goals * --------- * - Not a replacement for input validation at HTTP/CLI boundaries * (InputValidator handles that). This is the *content* boundary. * - Not a model-based classifier. False-positive rate is bounded by * pattern specificity; tune via `customPatterns` and `policy`. * * Reference: ADR-131, OpenAI Agents SDK ToolGuardrail API (March 2025). */ export type InjectionSeverity = 'low' | 'medium' | 'high' | 'critical'; export type InjectionCategory = 'instruction-override' | 'role-hijack' | 'exfiltration' | 'jailbreak' | 'hidden-unicode' | 'embedded-system' | 'tool-spoofing' | 'truncation'; export interface InjectionFinding { /** Short label for the matched pattern (stable identifier for telemetry). */ pattern: string; /** Risk weight assigned to this pattern. */ severity: InjectionSeverity; /** Category for downstream classification + OWASP mapping. */ category: InjectionCategory; /** Char offset of the first match in the scanned content. */ position: number; /** Up to 80 chars of surrounding context for human triage (redacted in `sanitized`). */ context: string; } export interface GuardrailResult { /** True iff no findings, or all findings under the `flag` policy threshold. */ safe: boolean; /** Findings in the order they were detected. */ findings: InjectionFinding[]; /** Highest severity observed; `none` if findings is empty. */ highest: InjectionSeverity | 'none'; } export type GuardrailAction = 'allow' | 'flag' | 'redact' | 'reject'; export interface GuardrailConfig { /** * Per-severity action. * Defaults: * low → allow * medium → flag * high → redact * critical → reject * * `allow` — content passes through unchanged, no logging required. * `flag` — content passes through; caller SHOULD log + monitor. * `redact` — matched substrings replaced with `[REDACTED:]`. * `reject` — caller MUST drop the content and treat the tool call * as failed (signal the agent that the tool returned an * unsafe payload rather than letting the payload through). */ policy?: Partial>; /** Add domain-specific patterns without subclassing. */ customPatterns?: Array<{ label: string; regex: RegExp; severity: InjectionSeverity; category: InjectionCategory; }>; /** * Maximum bytes of content to scan. Beyond this, the tail is ignored and * a `truncation` finding is added at `medium`. Default: 1 MiB. * Set to 0 to disable truncation (scan everything; slower on huge blobs). */ maxScanBytes?: number; } export declare class ToolOutputGuardrail { private readonly patterns; private readonly policy; private readonly maxScanBytes; constructor(config?: GuardrailConfig); /** * Pure scan — no side effects, no content modification. Useful when the * caller wants to log findings but cannot drop the content (e.g. read-only * audit mode). */ scan(content: string): GuardrailResult; /** * Scan + enforce policy. Returns the content to pass forward (possibly * redacted or empty) plus the scan result and the action that was taken. * Callers should treat `reject` as "drop the tool result and signal an * error" — do NOT silently substitute empty content. */ scanAndEnforce(content: string): { content: string; result: GuardrailResult; action: GuardrailAction; }; /** * Replace each non-truncation finding's matched substring with * `[REDACTED:]`. Truncation findings have no substring to redact * (their `position` is a length, not an offset into the content) and are * skipped. Idempotent for already-redacted strings. */ private redact; } /** Convenience factory that returns a guardrail with the default policy. */ export declare function createToolOutputGuardrail(config?: GuardrailConfig): ToolOutputGuardrail; /** * One-shot helper for callers that just want a yes/no answer without * constructing a guardrail. Uses the default policy. */ export declare function isToolOutputSafe(content: string): boolean; //# sourceMappingURL=tool-output-guardrail.d.ts.map