@@ -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+
405597def 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