Python: Samples: deterministic action-boundary validation middleware (#5366)#6528
Python: Samples: deterministic action-boundary validation middleware (#5366)#6528eeee2345 wants to merge 6 commits into
Conversation
…ary validation, microsoft#5366) Adds python/samples/02-agents/middleware/atr_validation_middleware.py: a FunctionMiddleware that validates tool arguments at the execution boundary and raises MiddlewareTermination before call_next() when they match an attack pattern, so the tool never runs. This is the deterministic, single-enforcement- point pattern named in microsoft#5366 and answers its open follow-up about a recommended validation-at-execution-boundary sample. The check is a small self-contained deny-list mirroring Agent Threat Rules (ATR) intent (prompt injection, exfiltration, credential access in tool args); a docstring notes how to swap in the full open ruleset via pyatr. No external dependency, so the sample stays import-clean. Updates the middleware README Files table. Signed-off-by: Adam Lin <adam@agentthreatrule.org>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new middleware sample demonstrating deterministic validation of tool arguments at the tool-execution boundary (ATR-style deny-list), and documents it in the middleware samples index.
Changes:
- Introduces
ATRValidationMiddlewaresample that blocks suspicious tool calls viaMiddlewareTerminationbefore tool execution. - Adds a small regex-based deny-list to detect common prompt-injection/exfil/credential-access patterns in tool arguments.
- Updates the middleware README to include the new sample.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| python/samples/02-agents/middleware/atr_validation_middleware.py | New sample middleware + demo agent that blocks tool calls when arguments match ATR-like patterns |
| python/samples/02-agents/middleware/README.md | Adds an entry describing the new ATR validation middleware sample |
| from dotenv import load_dotenv | ||
| from pydantic import Field | ||
|
|
||
| # Load environment variables from .env file | ||
| load_dotenv() |
| r"\b(?:ignore|disregard|forget|override)\b.{0,40}" | ||
| r"\b(?:previous|prior|above|earlier)\b.{0,40}\binstructions?\b", |
|
|
||
| def _matches_attack_pattern(arguments: dict[str, object]) -> str | None: | ||
| """Return the first matched pattern string, or None when the arguments look benign.""" | ||
| text = " ".join(str(value) for value in arguments.values()) |
| print( | ||
| f"[ATRValidationMiddleware] Blocked tool '{context.function.name}': " | ||
| f"arguments matched an ATR-style attack pattern." | ||
| ) |
| # Raise BEFORE call_next() so the tool is never executed. | ||
| raise MiddlewareTermination(f"ATR validation blocked tool '{context.function.name}'") | ||
|
|
||
| print(f"[ATRValidationMiddleware] Tool '{context.function.name}' passed ATR validation.") |
| f"arguments matched an ATR-style attack pattern." | ||
| ) | ||
| # Raise BEFORE call_next() so the tool is never executed. | ||
| raise MiddlewareTermination(f"ATR validation blocked tool '{context.function.name}'") |
|
@eavanvalkenburg yes — pulling in the real ATR ruleset is the right move, and it makes the sample much stronger than the illustrative deny-list it has now. The rules ship in the I'll update the sample to load ATR directly, and while I'm in there I'll fix the points from the review: the |
Address review on microsoft#6528: - Load and run the real ATR ruleset via pyatr (ATREngine + AgentEvent tool_call event) instead of re-implementing a regex deny-list; the built-in deny-list is now only a fallback when pyatr is not installed. - Add re.DOTALL (and a whole-text scan) to the fallback patterns so multiline injection payloads are not missed. - Move load_dotenv() into main() so importing the module has no side effects. - Route the middleware block/allow messages through a module logger instead of print(). - Include the matched ATR rule id in the log and in the MiddlewareTermination message for auditability. - Update the middleware README entry to match.
|
@eavanvalkenburg done — pushed the fixes. The sample now loads the published ATR ruleset and runs the real pyatr engine over the tool args (with a deny-list fallback only when pyatr isn't installed), plus the review points: DOTALL on the fallback patterns, load_dotenv() moved into main(), a module logger instead of print(), and the matched ATR rule id surfaced in both the log line and the MiddlewareTermination message. The in-repo engines/python path is still interface-only, so it uses the published pyatr package for now. |
eavanvalkenburg
left a comment
There was a problem hiding this comment.
with the addition of using pyatr we should simplify the sample!
| pyatr is not installed. | ||
| """ | ||
| try: | ||
| from pyatr import AgentEvent, ATREngine |
There was a problem hiding this comment.
we should have a header in this script with the dependencies, including pyatr, then we can just do a normal import and get rid of the fallback, or only keep some of the fallback rules as a comment to allow people to understand the types of rules.
Resolve the three type-checker errors flagged on the samples typing jobs (ty + pyrefly, reportMissingImports/reportAttributeAccessIssue via pyright): - pyatr is an optional, unstubbed runtime dependency that is not installed in the typing CI env; mark its imports with `# type: ignore` so the unresolved-import error is suppressed while keeping the graceful ImportError -> deny-list fallback intact. - Replace the function-attribute engine cache (`_detect_with_atr._engine`), which ty/pyrefly reject, with a clean `functools.lru_cache`-backed `_load_atr_engine()` loader. - Type the argument-scanning helpers to accept the real `FunctionInvocationContext.arguments` type (`BaseModel | Mapping[str, Any]`) and normalise a pydantic model via `model_dump()` before scanning, fixing the invalid-argument-type error. ty / pyrefly / pyright (samples config) / ruff check + format all clean on the file; runtime block/allow behaviour verified for both dict and BaseModel arguments.
Address review feedback (@eavanvalkenburg): now that the sample runs the real pyatr engine, drop the optional-import scaffolding. - Add a dependency header declaring pyatr (pip install pyatr). - Switch to a plain top-level `import pyatr` and remove the try/except ImportError fallback path. - Remove the regex deny-list (_FALLBACK_PATTERNS, _detect_with_fallback); keep 2-3 representative pattern shapes inline as a reference comment so readers still see the kind of rules ATR encodes. Detection is now a single straight-line engine call. - Keep the prior typing fixes: `# type: ignore` on the pyatr import (unstubbed, absent in the typing CI env), the functools.lru_cache engine loader, and the BaseModel | Mapping[str, Any] signatures.
|
@eavanvalkenburg agreed — the fallback makes more sense to drop now that the sample runs the real engine. Pushed: a dependency header declaring Typing stays green with pyatr absent (the env CI uses): ty / pyrefly / pyright-samples / ruff all clean. Runtime checks out too — benign args pass through, the injection arg trips a rule and the middleware blocks the tool call. Disclosure: I maintain ATR/pyatr. |
eavanvalkenburg
left a comment
There was a problem hiding this comment.
Looks good, just fix the script dependency syntax, I'll get someone on the team to also have a look
| @@ -0,0 +1,177 @@ | |||
| # Copyright (c) Microsoft. All rights reserved. | |||
|
|
|||
| # Dependencies (beyond agent-framework + azure-identity): | |||
There was a problem hiding this comment.
this is not the syntax for a PEP 723 compatible definition of script dependencies, there are a bunch of those in the repo already, just search for PEP 723
This adds a function-middleware sample that answers #5366: a single deterministic enforcement point that validates a tool call right before it executes.
ATRValidationMiddleware subclasses FunctionMiddleware, inspects the validated arguments in FunctionInvocationContext.arguments, and raises MiddlewareTermination before calling call_next() when the arguments match a known attack pattern, so the tool never runs. The decision is deterministic (no model call, no network), which is what #5366 asks for at the execution boundary.
The check is a small, self-contained deny-list illustrating the pattern. To enforce a full, maintained ruleset instead of the illustrative subset, install the open-source engine (pip install pyatr) and swap the matcher; the sample documents this inline.
Files
Local checks
Disclosure: the deny-list patterns mirror the open-source Agent Threat Rules (ATR) project, which I maintain. The sample has no ATR dependency; the reference is for users who want the full ruleset.