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
21 changes: 21 additions & 0 deletions python/packages/agent-hooks/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
59 changes: 59 additions & 0 deletions python/packages/agent-hooks/MAPPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Mapping AGENT-HOOKS-0.1 onto Agent Framework middleware

[AGENT-HOOKS-0.1](https://github.com/responsibleai/agent-hooks) defines
eight interception points a host emits around the agent loop, a
three-verdict control contract (`allow` / `deny`, optionally liftable
by an approval seam / `transform`), and fail-closed host obligations.
This package implements that contract on Agent Framework's Python
middleware pipeline.

## Seam mapping

| Interception point | Agent Framework seam | Fit |
| --- | --- | --- |
| `agent_startup` | `AgentMiddleware.process`, before `call_next` | Synthesized: emitted at run start; the run is the session (below) |
| `input` | `AgentMiddleware.process`, before `call_next` (`context.messages`) | Clean |
| `pre_model_call` | `ChatMiddleware.process`, before `call_next` (`context.messages`, `context.options`) | Clean |
| `post_model_call` | `ChatMiddleware.process`, after `call_next` (`context.result`) | Clean (non-streaming) |
| `pre_tool_call` | `FunctionMiddleware.process`, before `call_next` (`context.function`, `context.arguments`) | Clean |
| `post_tool_call` | `FunctionMiddleware.process`, after `call_next` (`context.result`) | Clean |
| `output` | `AgentMiddleware.process`, after `call_next` (`context.result`) | Clean (non-streaming) |
| `agent_shutdown` | `AgentMiddleware.process`, `finally` | Synthesized: emitted at run end with `completed` / `error` |

**Session scope.** Agent Framework middleware wraps *invocations*, not
agent lifecycle: there is no construction/disposal seam. This adapter
therefore scopes one agent-hooks session to one agent run —
`agent_startup` and `agent_shutdown` bracket the run, and `session.id`
is a per-run identifier. Multi-turn state above the run (an
`AgentSession`) has no middleware seam today; a session-scoped bracket
would need a small upstream seam (agent-level `on_session_open/close`
or middleware around `AgentSession`).

**Control semantics.** A block verdict maps to
`MiddlewareTermination`, the framework's documented early-termination
mechanism; the deny reason travels in the exception message and the
interception record. For post-action points (`post_model_call`,
`post_tool_call`, `output`) the adapter clears `context.result` before
terminating, matching the spec's discard-the-result obligation.
Transforms write back through the context (`context.arguments` for
tool calls), so the framework executes exactly the value the
interceptors approved.

**Fail-closed.** Errors inside the emitter, an interceptor, or this
adapter's own marshalling terminate the run; they never fall through
to execution. This is the inverse of observe-only callback surfaces
that log and continue.

## Known gaps (documented, not hidden)

1. **Streaming runs.** For `context.stream == True`, `output` and
`post_model_call` content is not available until the stream is
consumed. The adapter enforces all pre-action points on streaming
runs but does not currently buffer streams to enforce post-action
points; a finalizer-hook integration (`stream_result_hooks`) is the
natural follow-up. The spec's `buffered_output: false` declaration
covers this honestly in a conformance claim.
2. **Session-scoped brackets** (above).
3. **`post_model_call` tool-call extraction** is best-effort across
client result shapes; unrecognized shapes degrade to an empty
`tool_calls` list rather than failing the run.
53 changes: 53 additions & 0 deletions python/packages/agent-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Agent Framework Agent Hooks Middleware

Implements [AGENT-HOOKS-0.1](https://github.com/responsibleai/agent-hooks),
a framework-neutral control contract for AI agents, on Agent Framework's
middleware pipeline. Any agent-hooks interceptor (policy engine, approval
flow, egress guard, audit pipeline) plugs into an Agent Framework agent
without framework-specific glue, with the contract's fail-closed
semantics: a deny stops the action, a transform rewrites exactly what
executes, and errors terminate rather than fall through.

## Installation

```bash
pip install agent-framework-agent-hooks
```

## Usage

```python
from agent_framework import Agent
from agent_framework_agent_hooks import agent_hooks_middleware
from agent_hooks import Decision, Verdict


class EgressGuard:
def intercept(self, context):
if context["interception_point"] != "pre_tool_call":
return {"decision": "allow"}
if "confidential" in str(context["target"]):
return {"decision": "deny", "reason": "egress_blocked"}
return {"decision": "allow"}
Comment on lines +22 to +31


agent = Agent(
client=client,
name="assistant",
middleware=agent_hooks_middleware([EgressGuard()]),
)
```

One agent run is one agent-hooks session: `agent_startup` and
`agent_shutdown` bracket the run, `input`/`output` wrap it, and every
model and tool call gets its pre/post interception point. Composition
profiles, the approval seam, identity providers, and interception
records follow the published `agent-hooks-sdk` package; see
[MAPPING.md](MAPPING.md) for the seam mapping and known gaps
(streaming post-action points, session-scoped brackets).

Trust model: agent-hooks is a cooperative contract, not a security
boundary; the host process and registered interceptors are fully
trusted. See the
[specification](https://github.com/responsibleai/agent-hooks) for the
normative statement.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
"""AGENT-HOOKS-0.1 control-contract middleware for Agent Framework.

See https://github.com/responsibleai/agent-hooks for the specification
and MAPPING.md in this package for the seam mapping.
"""

from ._middleware import (
AgentHooksAgentMiddleware,
AgentHooksChatMiddleware,
AgentHooksFunctionMiddleware,
agent_hooks_middleware,
)

__all__ = [
"AgentHooksAgentMiddleware",
"AgentHooksChatMiddleware",
"AgentHooksFunctionMiddleware",
"agent_hooks_middleware",
]
Loading
Loading