Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions python/samples/02-agents/middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This folder contains focused middleware samples for `Agent`, chat clients, tools
| [`decorator_middleware.py`](./decorator_middleware.py) | Demonstrates middleware registration with decorators. |
| [`exception_handling_with_middleware.py`](./exception_handling_with_middleware.py) | Shows how middleware can handle failures and recover cleanly. |
| [`function_based_middleware.py`](./function_based_middleware.py) | Shows function-based agent and function middleware. |
| [`ismalicious_content_gate.py`](./ismalicious_content_gate.py) | Checks the destination before one operator-approved text-fetch tool executes, then scans its complete result before another model call. |
| [`middleware_termination.py`](./middleware_termination.py) | Demonstrates stopping a middleware pipeline early. |
| [`message_injection_middleware.py`](./message_injection_middleware.py) | Demonstrates `MessageInjectionMiddleware` with a real Foundry chat client: enqueueing a follow-up message into the active session while a long-running async tool is awaiting. |
| [`override_result_with_middleware.py`](./override_result_with_middleware.py) | Shows how middleware registers result transforms and buffered re-derivation on its context before execution, then post-processes regular and streaming responses. |
Expand Down Expand Up @@ -44,6 +45,97 @@ The sample forces a tool call so you can see middleware output for each inner mo

## Security Considerations

### Running the IsMalicious content gate sample

This example uses `FunctionInvocationContext` and `call_next()` around one local
fetch tool. A separate operator decision authorizes an exact URL and its hostname.
The middleware and fetch tool share a validator for that exact URL, HTTPS scheme,
hostname, port and absence of user information. Invalid operator configuration is
rejected before starting a run, and invocation scope is checked again before any
reputation request or fetch. The middleware refuses any different URL before
calling either remote service. It
checks its URL with `GET /gate/url`, executes the tool only when policy permits,
then sends the complete UTF-8 result and source URL to `POST /gate/scan`.
Both gate requests go to `https://api.ismalicious.com`. The selected document's
text and URL are transmitted to that external service; these calls consume its
separate scan quota. Obtain credentials at [IsMalicious account settings](https://ismalicious.com/app/account).

Install the Python development environment following [DEV_SETUP.md](../../../DEV_SETUP.md),
or run the self-contained script with its PEP 723 dependencies using `uv run`.
The script uses `agent-framework-foundry`, `httpx`, and `python-dotenv`; it does not
change the framework's dependency set. Run `az login`, then configure:

```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com"
export FOUNDRY_MODEL="your-model-deployment"
export ISMALICIOUS_API_KEY="your-api-key"
export ISMALICIOUS_API_SECRET="your-api-secret"
export CONTENT_GATE_HOST="learn.microsoft.com"
export CONTENT_GATE_URL="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview"
uv run samples/02-agents/middleware/ismalicious_content_gate.py
```

Use managed secrets for deployments. Credentials are sent as
`X-API-KEY: Base64(apiKey:apiSecret)` on a separate client and never attached to
the document fetch. HTTPS verification stays enabled, redirects are refused,
requests have a 15-second timeout, and quota failures are not retried.
Decision logs contain only verdicts. Do not enable sensitive framework telemetry
when handling documents that must not appear in model/tool traces.

`block` and `warn` stop the run by default. An operator can explicitly set
`IsMaliciousContentGate(..., allow_warn=True)` to release `warn` results.
Timeouts, HTTP failures, redirects, malformed or unsupported verdicts, and
incomplete link inspection also stop the run. The middleware replaces its current
result with a fixed withholding message before raising `MiddlewareTermination`,
since termination alone can preserve a result that has already been produced.
The native graceful stop still returns an `AgentResponse`; the sample recognizes
its fixed withholding result to print the stopped status rather than treating a
non-null response as successful completion.

Register the gate before result-transforming function middleware, for example
`middleware=[content_gate, trusted_transformer]`. It then scans the complete result
after inner middleware finish transforming it. Argument-repair middleware belongs
before policy inspection, but must not rewrite the returned result. Middleware
registered before the gate can replace a result after it has been scanned; do not
use an outer result rewriter with this sample. Changes at other agent/chat/provider
boundaries remain outside this selected function policy. Native graceful
termination stops the next model turn after the current accepted batch completes;
it does not cancel sibling tool calls in that batch.

An `allow` result means the service did not object under its current rules;
it does not prove content or a destination is benign. The service currently maps
unknown link reputation to `allow`. The gate response models retain unknown records
and additive fields, but the middleware stores only bounded decisions and counts
(including unknown links) in `context.metadata["ismalicious_scan"]`. It retains
no URLs or `sanitized_content` there; this sample refuses `links_truncated=True`.
Original allowed text is released unchanged, without using `sanitized_content` as a replacement.
The request uses `mode="fast"`; the reserved `thorough` mode currently behaves as
`fast` and is not a stronger inspection path.

The example covers only non-streaming `Agent.run()` with this exact selected
local tool, returning a complete UTF-8 `text/plain` or `text/html` string. It
refuses objects, binary content, invalid UTF-8 and documents whose complete
serialized scan request exceeds 1 MiB, without truncation. The configured URL
should identify a public document approved by the operator, and its hostname
must match `CONTENT_GATE_HOST`. This example is not a
general-purpose network sandbox. MCP tools and tools executed by a provider are
outside its tested scope. Gate requests are direct client calls, so they do not
re-enter the tool middleware.

Expected output includes destination/content verdict logs and an agent-run status
line. Actual verdicts depend on the selected document. To check enforcement
without keys or live HTTP/model services, run from `python/`:

```bash
uv run pytest tests/samples/middleware/test_ismalicious_content_gate.py -q
```

These tests use the actual `Agent` function-calling loop and middleware pipeline
with simulated model and HTTP responses. They verify ordering, withholding,
unmodified allowed text, explicit warning policy, request encoding, body limits,
response validation and credential-free decision logs. They do not test detector
accuracy or live Foundry/IsMalicious availability.

`AgentLoopMiddleware.with_judge` (used by `agent_loop_middleware_judge.py` and
`agent_loop_middleware_report.py`) is an explicit opt-in to sending the original request and the
agent's latest response to a second, external judge chat client on every iteration. A compromised
Expand Down
Loading
Loading