Repository navigation
feat: optional evidence-only detection backend for prompt injection (Python + Rust) - #3014
Conversation
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🟡 Contributor Check: MEDIUM
Automated check by AGT Contributor Check. |
…njection Add a pluggable, default-off evidence backend to the prompt-injection detector, following the pluggable-backend pattern of ADR-0015. When a backend is registered via evidence_backends, its advisory EvidenceSignal is appended to DetectionResult.evidence after the deterministic verdict is computed. Evidence never influences is_injection/threat_level/injection_type/confidence/ matched_patterns and never blocks on its own. EmbeddingSignalBackend adapts the existing prompt_injection_embedding kNN signal to this interface, connecting it to the detection pipeline (see microsoft#2918); content normalization (microsoft#2957) remains upstream of any backend. With no backend registered the detector output is unchanged. A backend that raises is recorded as a static error code and never breaks detection; evidence carries no raw input text. Tests: verdict-parity (backend on vs off), evidence-never-blocks, backend-failure isolation, no-raw-text, and adapter default-off / fake-embedder cases.
…injection Mirror the agent-os wiring in the Rust SDK, following ADR-0015's pluggable-backend pattern. PromptInjectionDetector gains an optional, default-off with_evidence_backends(...); each backend's advisory EvidenceSignal is appended to DetectionResult.evidence after the deterministic verdict is computed. Evidence never influences is_injection/threat_level/injection_type/confidence/ matched_patterns and never blocks on its own. DetectionResult is marked #[non_exhaustive] so the additive `evidence` field is non-breaking; EmbeddingSignalBackend adapts the existing prompt_injection_embedding kNN signal to the backend trait (see microsoft#2918). With no backend registered the detector output is unchanged. Tests: 6 new integration cases (verdict parity backend on vs off, evidence-never-blocks, adapter default-off / fake-embedder). agentmesh prompt_injection: 50 integration + 17 lib unit tests pass.
…export Document the optional, default-off evidence-backend design (pluggable ADR-0015 pattern, evidence-only, normalization upstream) and re-export EvidenceSignal / DetectionEvidenceBackend / EmbeddingSignalBackend from the agentmesh crate root for parity with the other prompt-injection types. Refs microsoft#2918 microsoft#2957.
adc0c18 to
df7762d
Compare
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
Good work on the core design — the Protocol/trait interface is clean, the default-off guarantee holds, and the verdict-parity tests are thorough. Three confirmed bugs and five plausible issues below. The three confirmed bugs need fixing before merge.
[BUG] Rust collect_evidence has no panic guard
cosine() uses assert_eq!(a.len(), b.len(), ...) which panics on dimension mismatch and propagates through EmbeddingSignalBackend::evaluate -> collect_evidence -> detect(), bypassing the unwrap_or_else (which catches Err, not panics). Python wraps every backend call in except Exception; Rust has nothing equivalent. Fix: wrap backend.evaluate(text) in std::panic::catch_unwind and record EvidenceSignal::with_error(backend.name(), "backend_error") on panic, symmetric with the Python path.
[BUG] Rust detect_without_audit silently skips evidence collection
detect_without_audit calls detect_impl directly with no call to collect_evidence. Any caller on this path always gets evidence: Vec::new() with no log, no error, no indication. Either call collect_evidence there too, or add a doc comment explicitly stating evidence is intentionally omitted on the audit-free path.
[BUG] REST API _detection_result_to_response does not include the new evidence field
_detection_result_to_response in server/app.py manually maps onto DetectionResponse without evidence. API consumers will never see evidence signals even with backends configured — silent data loss. Add evidence to both DetectionResponse and the extractor.
[PLAUSIBLE] EvidenceSignal.score can be nan or inf
No finite-value check before constructing EvidenceSignal. If a pluggable embedder returns vectors with nan/inf, those propagate unchecked (nan > threshold is False; inf > threshold is True). Validate math.isfinite(score) or document the embedder contract.
[PLAUSIBLE] kNN margin scores in the audit log are an evasion oracle
Evidence with raw scores lands in every audit record. An operator with audit log access can use per-request score feedback to iteratively tune a prompt toward lower margins. Consider bucketing scores (high/medium/low) in the audit record, with raw scores only in aggregated telemetry.
[PLAUSIBLE] EvidenceSignal.blocks is a convention, not a runtime invariant
frozen=True prevents mutation but not EvidenceSignal(backend="x", blocks=True) at construction. _collect_evidence appends signals without validating signal.blocks. The Rust constructor already enforces blocks: false; the Python path should match — add a __post_init__ guard or make blocks a ClassVar[bool] = False.
[PLAUSIBLE] signal: object annotation defeats static analysis
Use TYPE_CHECKING to annotate signal: EmbeddingSignal. The signal: object annotation forces # type: ignore[attr-defined] on the .score() call and means mypy silently accepts EmbeddingSignalBackend(42).
[PLAUSIBLE, minor] Rust struct/impl bound mismatch
EmbeddingSignalBackend<E: Embedder> struct vs E: Embedder + Send + Sync on the trait impl. Move the Send + Sync constraint to the struct definition so contributors get a clear error at construction, not at the coercion site.
…ence backend Resolve the three confirmed bugs and five plausible issues from review. Confirmed bugs: - Rust: wrap backend.evaluate() in catch_unwind so a panicking backend (e.g. the embedding signal's cosine() asserting on a dimension mismatch) is recorded as a static backend_error instead of unwinding through detect() and bypassing the Err-only fail-closed path. Symmetric with Python's except-guard. - Rust: detect_without_audit now also runs collect_evidence, so the audit-free path is consistent with detect() (only the audit write is skipped). - REST API: DetectionResponse gains an evidence field and _detection_result_to_response maps it, so configured backends actually surface through /api/v1/detect/injection. Plausible issues: - EvidenceSignal rejects non-finite (NaN/inf) scores: Python raises in __post_init__; Rust drops to a non_finite_score error code. - Evidence-only invariant is enforced, not conventional: blocks=true is rejected (Python) / forced false (Rust) at the boundary. - Audit oracle: raw evidence scores are stripped from the durable audit copy so the margin cannot be used as a per-request evasion oracle; the live DetectionResult keeps raw scores for telemetry. - Python EmbeddingSignalBackend annotates signal: EmbeddingSignal (via TYPE_CHECKING) instead of object, dropping the type: ignore. - Rust EmbeddingSignalBackend moves the Send + Sync bound onto the struct so a non-Send/Sync embedder fails at construction, not at the coercion site. Tests: +7 Python (prompt_injection), +2 Python (server REST evidence), +3 Rust integration (panic guard, blocks/NaN coercion, audit score strip), +1 Rust unit (detect_without_audit evidence). Full Python prompt_injection + server suites and cargo test --release --workspace all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Imran Siddique (@imran-siddique) thanks for the thorough review — all three confirmed bugs and the five plausible issues are addressed in 6c78fa6. Confirmed bugs
Plausible issues
Tests: +7 Python ( Out of scope (non-blocking, noted for follow-up) — a few pre-existing items I deliberately left alone so this stays a review-fix commit:
Happy to file 1–2 as separate issues if you'd like. |
Imran Siddique (imran-siddique)
left a comment
There was a problem hiding this comment.
The fixes hold up: EvidenceSignal.__post_init__ now enforces both invariants at construction time (blocks=False raised, non-finite score rejected), so a misbehaving backend cannot smuggle an enforcing signal past the detector. The @runtime_checkable Protocol and EmbeddingSignalBackend adapter are clean. Approving.
8a1cc12
into
microsoft:main
…Python + Rust) (microsoft#3014) * feat(agent-os): optional evidence-only detection backend for prompt injection Add a pluggable, default-off evidence backend to the prompt-injection detector, following the pluggable-backend pattern of ADR-0015. When a backend is registered via evidence_backends, its advisory EvidenceSignal is appended to DetectionResult.evidence after the deterministic verdict is computed. Evidence never influences is_injection/threat_level/injection_type/confidence/ matched_patterns and never blocks on its own. EmbeddingSignalBackend adapts the existing prompt_injection_embedding kNN signal to this interface, connecting it to the detection pipeline (see microsoft#2918); content normalization (microsoft#2957) remains upstream of any backend. With no backend registered the detector output is unchanged. A backend that raises is recorded as a static error code and never breaks detection; evidence carries no raw input text. Tests: verdict-parity (backend on vs off), evidence-never-blocks, backend-failure isolation, no-raw-text, and adapter default-off / fake-embedder cases. * feat(agentmesh): optional evidence-only detection backend for prompt injection Mirror the agent-os wiring in the Rust SDK, following ADR-0015's pluggable-backend pattern. PromptInjectionDetector gains an optional, default-off with_evidence_backends(...); each backend's advisory EvidenceSignal is appended to DetectionResult.evidence after the deterministic verdict is computed. Evidence never influences is_injection/threat_level/injection_type/confidence/ matched_patterns and never blocks on its own. DetectionResult is marked #[non_exhaustive] so the additive `evidence` field is non-breaking; EmbeddingSignalBackend adapts the existing prompt_injection_embedding kNN signal to the backend trait (see microsoft#2918). With no backend registered the detector output is unchanged. Tests: 6 new integration cases (verdict parity backend on vs off, evidence-never-blocks, adapter default-off / fake-embedder). agentmesh prompt_injection: 50 integration + 17 lib unit tests pass. * docs(adr): optional embedding evidence backend (ADR 0031) + crate re-export Document the optional, default-off evidence-backend design (pluggable ADR-0015 pattern, evidence-only, normalization upstream) and re-export EvidenceSignal / DetectionEvidenceBackend / EmbeddingSignalBackend from the agentmesh crate root for parity with the other prompt-injection types. Refs microsoft#2918 microsoft#2957. * fix(prompt-injection): address PR microsoft#3014 review — harden evidence backend Resolve the three confirmed bugs and five plausible issues from review. Confirmed bugs: - Rust: wrap backend.evaluate() in catch_unwind so a panicking backend (e.g. the embedding signal's cosine() asserting on a dimension mismatch) is recorded as a static backend_error instead of unwinding through detect() and bypassing the Err-only fail-closed path. Symmetric with Python's except-guard. - Rust: detect_without_audit now also runs collect_evidence, so the audit-free path is consistent with detect() (only the audit write is skipped). - REST API: DetectionResponse gains an evidence field and _detection_result_to_response maps it, so configured backends actually surface through /api/v1/detect/injection. Plausible issues: - EvidenceSignal rejects non-finite (NaN/inf) scores: Python raises in __post_init__; Rust drops to a non_finite_score error code. - Evidence-only invariant is enforced, not conventional: blocks=true is rejected (Python) / forced false (Rust) at the boundary. - Audit oracle: raw evidence scores are stripped from the durable audit copy so the margin cannot be used as a per-request evasion oracle; the live DetectionResult keeps raw scores for telemetry. - Python EmbeddingSignalBackend annotates signal: EmbeddingSignal (via TYPE_CHECKING) instead of object, dropping the type: ignore. - Rust EmbeddingSignalBackend moves the Send + Sync bound onto the struct so a non-Send/Sync embedder fails at construction, not at the coercion site. Tests: +7 Python (prompt_injection), +2 Python (server REST evidence), +3 Rust integration (panic guard, blocks/NaN coercion, audit score strip), +1 Rust unit (detect_without_audit evidence). Full Python prompt_injection + server suites and cargo test --release --workspace all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: jlaportebot <jlaportebot@gmail.com>
Summary
Connects the existing local kNN/embedding signal (
prompt_injection_embedding) to thePromptInjectionDetectorpipeline as an optional, default-off, evidence-only backend, in both the Python (agent-os) and Rust (agentmesh) SDKs. This is the "connect the signal to the detection pipeline as an optional backend" gap noted in #2918; content normalization (RFC #2957 / #2991) remains upstream. Design is recorded in ADR 0031 and follows the pluggable-backend pattern of ADR-0015.What changes
Protocol, Rusttrait(DetectionEvidenceBackend) — with a stablenameandevaluate(text).PromptInjectionDetectorconsults registered backends after the deterministic verdict and appends theirEvidenceSignals to a new additiveDetectionResult.evidencefield.EmbeddingSignalBackendadapts the existing kNN signal to the interface.evidence_backends=, Rustwith_evidence_backends(...)); default-off.Invariants (tested)
is_injection/threat_level/injection_type/confidence/matched_patterns, and never blocks (blocks == false). Verdict fields are byte-identical with a backend on vs off.detect()output is unchanged.DetectionResultis#[non_exhaustive]; the Python field has a default. No new default dependency — the embedding model/runtime is optional, used only when a backend is enabled.EvidenceSignalcarries a static backend id, a score, and a static error code — never raw input.Tests
agent-os: fullprompt_injectionsuite green (new: verdict-parity, evidence-never-blocks, backend-failure isolation, no-raw-text, adapter default-off / fake-embedder).agentmesh: 50 integration + 17 lib unit tests pass (6 new evidence-backend cases).Notes
Draft — opening for design review of the interface and the evidence-only invariant before finalising. Related: #2918, #2957, #2991;
docs/benchmarks/prompt-injection-methodology.md.