Skip to content

Commit 9b13bd6

Browse files
imran-siddiqueNishar
andcommitted
feat(adapters): add native hooks for Anthropic, SK, smolagents, PydanticAI (microsoft#1605)
Complete the native-hooks migration for all four remaining adapters: Anthropic: - Add GovernanceMessageHook + as_message_hook() factory - Pre-execution: content scanning, tool allowlist, token limits - Post-execution: tool_use validation, token tracking, audit - Deprecate wrap() and wrap_client() with migration guidance Semantic Kernel: - Add GovernanceFunctionFilter + as_filter() factory - Uses SK's native add_filter('auto_function_invocation', ...) system - Validates function names, blocked patterns, call counts - Deprecate wrap() and wrap_kernel() with migration guidance Smolagents: - Add GovernanceStepCallback + as_step_callback() factory - Implements step_callbacks protocol: __call__(step, agent) - Validates tool names, blocked patterns, observations - Deprecate wrap() with migration guidance PydanticAI: - Add GovernanceCapability + as_capability() factory - Lifecycle hooks: before/after_run, before/after_tool_execute - Pre-execution policy gating, post-execution drift detection - Deprecate wrap() with migration guidance Package exports: - Export AnthropicGovernanceHook, SKGovernanceFilter, SmolagentsGovernanceCallback, PydanticAIGovernanceCapability from integrations __init__.py Tests: - test_anthropic_hooks.py: 12 tests - test_semantic_kernel_hooks.py: 10 tests - test_smolagents_hooks.py: 14 tests - test_pydantic_ai_hooks.py: 16 tests Part of: microsoft#1571 Co-authored-by: Nishar <you@example.com>
1 parent 3bbc7e9 commit 9b13bd6

9 files changed

Lines changed: 1744 additions & 17 deletions

File tree

agent-governance-python/agent-os/src/agent_os/integrations/__init__.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@
5959
RateLimitError,
6060
)
6161
from agent_os.integrations.a2a_adapter import A2AEvaluation, A2AGovernanceAdapter, A2APolicy
62-
from agent_os.integrations.anthropic_adapter import AnthropicKernel, GovernedAnthropicClient
62+
from agent_os.integrations.anthropic_adapter import (
63+
AnthropicKernel,
64+
GovernedAnthropicClient,
65+
GovernanceMessageHook as AnthropicGovernanceHook,
66+
)
6367
from agent_os.integrations.autogen_adapter import (
6468
AutoGenKernel,
6569
GovernanceInterventionHandler as AutoGenGovernanceHandler,
@@ -101,11 +105,19 @@
101105
from agent_os.integrations.llamaindex_adapter import LlamaIndexKernel
102106
from agent_os.integrations.mistral_adapter import GovernedMistralClient, MistralKernel
103107
from agent_os.integrations.openai_adapter import GovernedAssistant, OpenAIKernel
104-
from agent_os.integrations.pydantic_ai_adapter import PydanticAIKernel
108+
from agent_os.integrations.pydantic_ai_adapter import (
109+
GovernanceCapability as PydanticAIGovernanceCapability,
110+
PydanticAIKernel,
111+
)
105112
from agent_os.integrations.semantic_kernel_adapter import (
113+
GovernanceFunctionFilter as SKGovernanceFilter,
106114
GovernedSemanticKernel,
107115
SemanticKernelWrapper,
108116
)
117+
from agent_os.integrations.smolagents_adapter import (
118+
GovernanceStepCallback as SmolagentsGovernanceCallback,
119+
SmolagentsKernel,
120+
)
109121

110122
from .base import (
111123
AsyncGovernedWrapper,
@@ -183,7 +195,7 @@
183195
# Anthropic Claude
184196
"AnthropicKernel",
185197
"GovernedAnthropicClient",
186-
# Google Gemini
198+
"AnthropicGovernanceHook",
187199
"GeminiKernel",
188200
"GovernedGeminiModel",
189201
# Mistral AI
@@ -192,6 +204,7 @@
192204
# Semantic Kernel
193205
"SemanticKernelWrapper",
194206
"GovernedSemanticKernel",
207+
"SKGovernanceFilter",
195208
# Guardrails
196209
"GuardrailsKernel",
197210
# Google ADK
@@ -215,6 +228,10 @@
215228
"OffensiveIntentDetector",
216229
# PydanticAI
217230
"PydanticAIKernel",
231+
"PydanticAIGovernanceCapability",
232+
# Smolagents
233+
"SmolagentsKernel",
234+
"SmolagentsGovernanceCallback",
218235
# Microsoft Agent Framework (MAF)
219236
"MAFGovernancePolicyMiddleware",
220237
"MAFCapabilityGuardMiddleware",

agent-governance-python/agent-os/src/agent_os/integrations/anthropic_adapter.py

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,48 @@ def __init__(
132132
self._start_time = time.monotonic()
133133
self._last_error: str | None = None
134134

135-
def wrap(self, client: Any) -> GovernedAnthropicClient:
135+
def as_message_hook(self, *, name: str = "anthropic-governance") -> "GovernanceMessageHook":
136+
"""Create a ``GovernanceMessageHook`` for non-invasive integration.
137+
138+
The hook governs ``messages.create()`` calls without wrapping or
139+
proxying the Anthropic client. This is the **recommended**
140+
integration pattern.
141+
142+
Args:
143+
name: Human-readable identifier for audit logging.
144+
145+
Returns:
146+
A ``GovernanceMessageHook`` instance.
147+
148+
Example::
149+
150+
kernel = AnthropicKernel(policy=policy)
151+
hook = kernel.as_message_hook()
152+
response = hook.create(client, model="claude-sonnet-4-20250514", ...)
153+
"""
154+
return GovernanceMessageHook(self, name=name)
155+
156+
def wrap(self, client: Any) -> "GovernedAnthropicClient":
136157
"""Wrap an Anthropic client with governance.
137158
159+
.. deprecated::
160+
Use :meth:`as_message_hook` instead for a non-invasive
161+
integration that does not proxy the client object.
162+
138163
Args:
139164
client: An ``anthropic.Anthropic`` client instance.
140165
141166
Returns:
142167
A ``GovernedAnthropicClient`` that enforces policy on all
143168
``messages.create()`` calls.
144169
"""
170+
import warnings
171+
warnings.warn(
172+
"AnthropicKernel.wrap() is deprecated. Use as_message_hook() "
173+
"for a non-invasive governance pattern that doesn't proxy the client.",
174+
DeprecationWarning,
175+
stacklevel=2,
176+
)
145177
_check_anthropic_available()
146178
client_id = id(client)
147179
ctx = AnthropicContext(
@@ -402,12 +434,176 @@ def __getattr__(self, name: str) -> Any:
402434
return getattr(self._client, name)
403435

404436

437+
# ═══════════════════════════════════════════════════════════════════
438+
# Native Hook: GovernanceMessageHook
439+
# ═══════════════════════════════════════════════════════════════════
440+
#
441+
# Anthropic's Python SDK does not expose a formal middleware/plugin
442+
# system. However, the recommended integration pattern is a
443+
# composable "message hook" that wraps messages.create() calls
444+
# with governance checks — without creating a proxy client object.
445+
#
446+
# Usage:
447+
# kernel = AnthropicKernel(policy=policy)
448+
# hook = kernel.as_message_hook()
449+
#
450+
# # Use the hook to govern individual calls
451+
# response = hook.create(client, model="claude-sonnet-4-20250514", ...)
452+
# ═══════════════════════════════════════════════════════════════════
453+
454+
455+
class GovernanceMessageHook:
456+
"""Stateless governance hook for Anthropic ``messages.create()`` calls.
457+
458+
Unlike ``GovernedAnthropicClient``, this does **not** wrap or proxy the
459+
client object. Instead, it provides a ``create()`` method that governs
460+
a single ``messages.create()`` invocation on any client you pass in.
461+
462+
This is the recommended integration pattern for Anthropic because the
463+
SDK does not expose a native plugin/middleware system.
464+
465+
Example::
466+
467+
kernel = AnthropicKernel(policy=GovernancePolicy(
468+
blocked_patterns=["password"],
469+
allowed_tools=["web_search"],
470+
))
471+
hook = kernel.as_message_hook()
472+
473+
response = hook.create(client, model="claude-sonnet-4-20250514",
474+
max_tokens=1024, messages=[...])
475+
"""
476+
477+
def __init__(self, kernel: AnthropicKernel, *, name: str = "anthropic-governance") -> None:
478+
self._kernel = kernel
479+
self._name = name
480+
self._ctx = AnthropicContext(
481+
agent_id=name,
482+
session_id=f"ant-hook-{int(time.time())}",
483+
policy=kernel.policy,
484+
)
485+
kernel.contexts[name] = self._ctx
486+
487+
@property
488+
def kernel(self) -> AnthropicKernel:
489+
"""Return the governing kernel."""
490+
return self._kernel
491+
492+
@property
493+
def context(self) -> AnthropicContext:
494+
"""Return the execution context."""
495+
return self._ctx
496+
497+
def create(self, client: Any, **kwargs: Any) -> Any:
498+
"""Govern a single ``messages.create()`` call.
499+
500+
Validates message content against blocked patterns, enforces
501+
tool-call allowlists, checks token limits after completion,
502+
and records an audit trail — all without mutating the client.
503+
504+
Args:
505+
client: An ``anthropic.Anthropic`` client instance.
506+
**kwargs: Forwarded to ``client.messages.create()``.
507+
508+
Returns:
509+
The Anthropic message response.
510+
511+
Raises:
512+
PolicyViolationError: If a governance policy is violated.
513+
"""
514+
# --- pre-execution checks ---
515+
messages = kwargs.get("messages", [])
516+
for msg in messages:
517+
content = msg.get("content", "") if isinstance(msg, dict) else str(msg)
518+
allowed, reason = self._kernel.pre_execute(self._ctx, content)
519+
if not allowed:
520+
raise PolicyViolationError(f"Message blocked: {reason}")
521+
522+
# Validate requested tools against policy
523+
tools = kwargs.get("tools")
524+
if tools and self._kernel.policy.allowed_tools:
525+
for tool in tools:
526+
name = tool.get("name") if isinstance(tool, dict) else getattr(tool, "name", None)
527+
if name and name not in self._kernel.policy.allowed_tools:
528+
raise PolicyViolationError(f"Tool not allowed: {name}")
529+
530+
# Enforce max_tokens cap from policy
531+
requested_max = kwargs.get("max_tokens", 0)
532+
if requested_max > self._kernel.policy.max_tokens:
533+
raise PolicyViolationError(
534+
f"Requested max_tokens ({requested_max}) exceeds policy limit "
535+
f"({self._kernel.policy.max_tokens})"
536+
)
537+
538+
# Audit log
539+
logger.info(
540+
"Anthropic hook.create | agent=%s model=%s",
541+
self._name,
542+
kwargs.get("model", "unknown"),
543+
)
544+
545+
# --- execute ---
546+
response = client.messages.create(**kwargs)
547+
548+
# --- post-execution checks ---
549+
response_id = getattr(response, "id", f"msg-{int(time.time())}")
550+
self._ctx.message_ids.append(response_id)
551+
552+
# Track tokens
553+
usage = getattr(response, "usage", None)
554+
if usage:
555+
self._ctx.prompt_tokens += getattr(usage, "input_tokens", 0)
556+
self._ctx.completion_tokens += getattr(usage, "output_tokens", 0)
557+
558+
total = self._ctx.prompt_tokens + self._ctx.completion_tokens
559+
if total > self._kernel.policy.max_tokens:
560+
raise PolicyViolationError(
561+
f"Token limit exceeded: {total} > {self._kernel.policy.max_tokens}"
562+
)
563+
564+
# Validate tool_use blocks in response
565+
content_blocks = getattr(response, "content", [])
566+
for block in content_blocks:
567+
if getattr(block, "type", None) == "tool_use":
568+
tool_name = getattr(block, "name", "")
569+
self._ctx.tool_use_calls.append({
570+
"id": getattr(block, "id", ""),
571+
"name": tool_name,
572+
"input": getattr(block, "input", {}),
573+
"timestamp": datetime.now().isoformat(),
574+
})
575+
self._ctx.tool_calls.append({"name": tool_name})
576+
577+
if len(self._ctx.tool_calls) > self._kernel.policy.max_tool_calls:
578+
raise PolicyViolationError(
579+
f"Tool call limit exceeded: "
580+
f"{len(self._ctx.tool_calls)} > "
581+
f"{self._kernel.policy.max_tool_calls}"
582+
)
583+
584+
if self._kernel.policy.allowed_tools:
585+
if tool_name not in self._kernel.policy.allowed_tools:
586+
raise PolicyViolationError(f"Tool not allowed: {tool_name}")
587+
588+
# Post-execute bookkeeping
589+
self._kernel.post_execute(self._ctx, response)
590+
591+
return response
592+
593+
def __repr__(self) -> str:
594+
return f"GovernanceMessageHook(name={self._name!r})"
595+
596+
405597
def wrap_client(
406598
client: Any,
407599
policy: GovernancePolicy | None = None,
408600
) -> GovernedAnthropicClient:
409601
"""Quick wrapper for Anthropic clients.
410602
603+
.. deprecated::
604+
Use ``AnthropicKernel.as_message_hook()`` instead for a
605+
non-invasive integration that does not proxy the client.
606+
411607
Args:
412608
client: An ``anthropic.Anthropic`` client instance.
413609
policy: Optional governance policy.
@@ -420,4 +616,12 @@ def wrap_client(
420616
>>> governed = wrap_client(my_client)
421617
>>> response = governed.messages.create(model="claude-sonnet-4-20250514", ...)
422618
"""
619+
import warnings
620+
warnings.warn(
621+
"wrap_client() is deprecated. Use AnthropicKernel(policy=...).as_message_hook() "
622+
"for a non-invasive governance pattern that doesn't proxy the client.",
623+
DeprecationWarning,
624+
stacklevel=2,
625+
)
423626
return AnthropicKernel(policy=policy).wrap(client)
627+

0 commit comments

Comments
 (0)