Repository navigation
Python: feat(core): load the agent-hooks middleware from a host declaration - #9405
Open
MohammadHaroonAbuomar wants to merge 2 commits into
Open
MohammadHaroonAbuomar wants to merge 2 commits into
MohammadHaroonAbuomar wants to merge 2 commits into
Conversation
…ration Add three factories that build the agent-hooks middleware bundle from a host declaration document (agent-hooks spec section 7.7, contract agent-hooks-declaration/1.0): from a mapping or a built HostDeclaration, from a file path, and from JSON text. Add create_agent_hooks_host_registry, which returns an agent_hooks.HostRegistry over the surface this middleware implements (all eight points, the claimed capabilities, posture terminate, buffered streaming), so hosts can register their own binding kinds, identity providers, approval resolvers and redactors. The factory loads the document once at construction so a refused document raises agent_hooks.DeclarationError before any run. Each run then loads a fresh sealed emitter from the same document and registry, which keeps the one-session-per-run model of create_agent_hooks_middleware. Passing interceptors registers the framework's own binding kind, agent_framework.interceptor, which binds a supplied interceptor by name or binding id and refuses any other config without echoing it. The existing factories and the three middleware classes are unchanged. The new tests skip when the installed agent-hooks-sdk predates the loader. Move the dev pin to agent-hooks-sdk 0.1.0b2, the first release with the loader, and refresh uv.lock so the tests run in CI. Fixes microsoft#9329 Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
MohammadHaroonAbuomar
requested review from
SergeyMenshykh,
Tao Chen (TaoChenOSU),
Eduard van Valkenburg (eavanvalkenburg),
Giles Odigwe (giles17),
Jose Alvarez (jpalvarezl),
Evan Mattson (moonbox3),
Roger Barreto (rogerbarreto),
sophia-ramsey and
westey (westey-m)
as code owners
October 15, 2026 08:37
MohammadHaroonAbuomar
deployed
to
github-app-auth
October 15, 2026 08:37 — with
GitHub Actions
Active
MohammadHaroonAbuomar
deployed
to
github-app-auth
October 15, 2026 08:37 — with
GitHub Actions
Active
MohammadHaroonAbuomar
deployed
to
github-app-auth
October 15, 2026 08:42 — with
GitHub Actions
Active
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Supplied registries can bypass framework-surface validation, and valid non-dict mapping inputs are rejected.
2 open findings
What changed in this PR
Adds declaration-based configuration to Python’s Agent Hooks middleware while preserving the existing factories and per-run sessions.
Changes:
- Adds value, JSON, and file factories plus a public host registry.
- Adds declaration validation, binding, session, and streaming tests.
- Updates exports, experimental-feature documentation, and the development SDK dependency.
| File | Description |
|---|---|
| python/uv.lock | Locks the updated Agent Hooks SDK. |
| python/pyproject.toml | Raises the development SDK minimum. |
| python/packages/core/tests/core/test_agent_hooks.py | Tests declaration-based middleware. |
| python/packages/core/agent_framework/_agent_hooks.py | Implements factories, registry, and per-run loading. |
| python/packages/core/agent_framework/__init__.pyi | Exposes typed public entry points. |
| python/packages/core/agent_framework/__init__.py | Adds lazy public exports. |
| python/PACKAGE_STATUS.md | Documents new experimental APIs and SDK requirements. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
MohammadHaroonAbuomar
deployed
to
github-app-auth
October 15, 2026 08:44 — with
GitHub Actions
Active
…surface and accept any Mapping The declaration factories handed a caller-supplied registry to the SDK as given, and the SDK validates a document against that registry's own surface. A registry built over another surface (the SDK default, with posture continue, or one that streams unbuffered) therefore accepted documents this middleware cannot honour: a document that omits the posture, or one that declares buffered_output: false. The factories now refuse a registry whose surface differs from the one the middleware implements, with a ValueError that names the differing members and points at create_agent_hooks_host_registry(). A registry over the same surface keeps its registered kinds, identity providers, approval resolvers and redactors, so a document is always checked against the middleware's surface and the caller's names. create_agent_hooks_middleware_from_declaration accepts a Mapping, but HostDeclaration.from_value serializes with json.dumps, which takes a dict and refuses other mappings such as MappingProxyType or UserDict with declaration_error:malformed. The factory now copies a top-level Mapping into a dict before handing it to the SDK. Anything that is not a mapping still reaches the SDK as given, so its own refusal applies. Tests cover a registry with another posture, one that advertises unbuffered streaming, a hand-built registry over the framework surface that keeps its registered names, MappingProxyType and UserDict documents, and non-mapping values that get the SDK's refusal. Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
MohammadHaroonAbuomar
deployed
to
github-app-auth
October 15, 2026 09:00 — with
GitHub Actions
Active
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation & Context
Agent Hooks 0.1.0-beta.2 adds the host declaration document (spec section 7.7, contract
agent-hooks-declaration/1.0). One JSON file states a host's enforcement configuration(mode, composition, identity provider, approval resolver and redactor, timeouts, record
bound), its declared surface and its interceptor bindings. The loader checks the document
against what the host's code can honour and refuses anything else before any emission.
Records from an emitter built this way carry the contract version, so an audit can tell a
declared host from one configured in code.
The Python middleware (#7515) takes its configuration from factory arguments only. This
change lets an operator hand the agent a document instead, from a file, from JSON text or
from a value built in code. The existing factories and their behaviour stay as they are.
Fixes #9329.
Description & Review Guide
What are the major changes?
New public functions in
agent_framework/_agent_hooks.py, exported fromagent_frameworkand marked experimental underAGENT_HOOKSlike the two existingfactories (so
ExperimentalWarningfires on each call, as it does today):create_agent_hooks_middleware_from_declaration(declaration, *, interceptors=None, registry=None, record_sink=None)takes the document as a mapping or an
agent_hooks.HostDeclaration(whatagent_hooks.DeclarationBuilder.build()returns).create_agent_hooks_middleware_from_declaration_path(path, *, ...)reads the fileonce through
agent_hooks.HostDeclaration.from_path.create_agent_hooks_middleware_from_declaration_json(json_str, *, ...)goes throughagent_hooks.HostDeclaration.from_json.create_agent_hooks_host_registry()returns anagent_hooks.HostRegistryover thesurface this middleware implements: all eight points;
model_calls,tool_calls,int64_json,bigint_json,host_declaration; posturetool_seam_host_error: terminate; buffered streaming. Hosts register their ownbinding kinds, identity providers, approval resolvers and redactors on it and pass
it as
registry. The conformance harness also needs the surface.The names follow the declarative package (
create_agent_from_yaml_path,create_agent_from_yaml,create_agent_from_dict).How it works:
InterceptionEmitter.from_declaration. A refused document raisesagent_hooks.DeclarationErrorthere, before any run, with the spec's refusal class(
unknown_field,version_unsupported,kind_unknown,binding_rejectedand soon).
HostDeclarationandHostRegistry. That keeps the one-session-per-run model ofcreate_agent_hooks_middleware:agent_startupandagent_shutdownbracket everyrun, and session ids and sequences restart per run. Kind resolvers run once at
construction and once per run; the spec asks them to be cheap and to do no I/O
before the interceptor's first call. A host that wants one session across runs
builds the emitter with
InterceptionEmitter.from_declaration_path(...)and passesit to
create_agent_hooks_middleware_from_emitter; a test pins that path.interceptors={"egress": EgressGuard()}registers the framework's ownbinding kind,
agent_framework.interceptor. A binding{"id": "egress", "kind": "agent_framework.interceptor"}runs the interceptorsupplied under its id;
config: {"name": "..."}picks another key. The resolverrefuses any other config shape and never echoes the config in its message. The bound
instances are the ones the caller supplied, so they stay shared across runs as today.
A kind is a lookup key only; the loader never imports a module or class named in a
document.
configuration.posturegets the spec default,continue,and the loader refuses it against this framework's surface (
surface_unsupported:the host implements
terminate). Documents for Agent Framework write"posture": {"tool_seam_host_error": "terminate"}. The docstrings say so.agent-hooks-sdkthat predates the loader gets anImportErrorwithan upgrade hint (
agent-hooks-sdk0.1.0b2 or later).agent-hooks-sdk>=0.1.0a4,<0.2to>=0.1.0b2,<0.2anduv.lockfollows (uv lock --upgrade-package agent-hooks-sdk),so the new tests run in CI instead of skipping. The runtime dependency is unchanged:
the SDK stays optional.
Smallest document that enforces one interceptor:
{ "declaration": "agent-hooks-declaration/1.0", "configuration": {"posture": {"tool_seam_host_error": "terminate"}}, "bindings": [{"id": "egress", "kind": "agent_framework.interceptor"}] }What is the impact of these changes?
Additive.
create_agent_hooks_middlewareandcreate_agent_hooks_middleware_from_emitterare unchanged, and so are the threemiddleware classes. The only change on the run path is one branch in
_new_run_statethat calls the loader when the config holds a declaration.PACKAGE_STATUS.mdlists the new entry points.tests/core/test_agent_hooks.pygains a "Host declaration" region with 17 testfunctions (24 cases) plus a third case of the telemetry test. They run offline against
the mock chat client and cover: the value, JSON and path factories produce the same
records; every record carries
declaration: "agent-hooks-declaration/1.0"and thebinding id as the verdict name; one session per run; each refusal class raises at
construction; the posture requirement;
agent_framework.interceptorbinds by name orid and refuses bad config without echoing it; zero bindings fail closed
(
host_error:no_interceptor, no model call); per-pointatsets dispatch only wherebound;
modeandapproval.resolvercome from the document; a host-registered kindreceives the binding context (id, kind,
at, timeout, contract version); a registrythat already holds the framework kind is rejected; a built
HostDeclarationisaccepted; a missing file is
unreadable; streaming stays buffered; adeclaration-built emitter works with the host-owned-session factory.
The new tests skip with a clear reason when the installed SDK lacks the loader, so an
environment on an older pin still passes.
What do you want reviewers to focus on?
The per-run reload in
_new_run_state(a sealed emitter cannot be reused for a newsession, so each run loads one from the same document and registry), the
agent_framework.interceptorkind resolver (config validation, no config echo inrefusals), and whether
create_agent_hooks_host_registry()should be public or staybehind the three factories.
Related Issue
Fixes #9329
A maintainer ran
/planon the issue on 2026-10-13 and this change follows the generatedplan. It differs in one place: the plan proposed one factory that accepts a document, a
path or JSON text; this PR splits those into three factories, named after the declarative
package's
create_agent_from_yaml_path,create_agent_from_yamlandcreate_agent_from_dict. The plan left open whether the default registry's capabilitylist should mirror the existing conformance claim; it does.
AI Assistance
AI assistance details: Implementation, tests and this description were drafted with AI assistance and reviewed, run and verified locally.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.