Trusted execution hooks¶
SchemaRouter supports ordered local callbacks immediately around validated tool execution.
Hooks are intended for organization-specific audit, policy integration, metrics, local vetoes, and other trusted middleware that should not become part of the model-visible tool contract.
Configure hooks¶
from schemarouter import ExecutionHooks, SchemaRouter
def before(tool, endpoint, call):
audit_call(call)
def after(tool, endpoint, call, result):
audit_result(result)
router = SchemaRouter(
execution_hooks=ExecutionHooks(
before_call=[before],
after_call=[after],
)
)
Both sync and async callables are supported. Hooks run in declaration order. Async hooks are
bounded by the remaining ExecutionBudget.max_elapsed_seconds budget; synchronous hooks cannot be
preempted, but elapsed time is checked immediately after they return.
Execution order¶
For one logical tool call, the relevant boundary is:
plan
-> current schema + policy validation
-> elapsed-budget clock
-> optional trusted approval
-> schema/binding revalidation
-> budget logical-call accounting
-> before hooks
-> schema/binding revalidation
-> invoker attempts / retries
-> raw output JSON Schema validation
-> field projection + ToolResult construction
-> after hooks
-> return ToolResult
A before hook cannot bypass schema validation, execution policy, approval, or binding checks. Because a hook may await, SchemaRouter refreshes executable state after all before hooks finish.
Snapshot-only contract¶
Hooks receive deep copies of the models supplied to them.
A before hook receives:
ToolSpec;EndpointSpec;ToolCall.
An after hook additionally receives the final projected ToolResult.
Mutating those objects does not mutate the executable call, registry schema, or result returned to the caller.
Hooks must return None. A non-None return from a before hook raises
ExecutionHookError. A non-None return from an after hook raises
PostInvocationHookError, because the external operation and result validation have already
succeeded. This avoids an implicit transformation API that could alter arguments, fields, schemas,
or execution authority.
Failure behavior¶
Hook failures are fail-closed, but pre- and post-invocation failures have different semantics.
- A failing before hook prevents the invoker from running and is surfaced as
ExecutionHookError. - A failing after hook withholds the normal return path, but the successful projected
ToolResultis preserved onPostInvocationHookError.result. PostInvocationHookError.execution_succeededis alwaysTrue.- Post-invocation failures are non-retryable, including when trusted code explicitly enabled retries for non-read-only endpoints. A successful mutation is therefore never repeated because an audit/metrics hook failed.
- Event streams record the successful operation as
tool.end, then surfacerun.errorwithstage="post_invocation_hook"; they do not rewrite that operation astool.error.
Before-hook elapsed-budget expiration remains an ExecutionBudgetExceededError. Once the tool has
successfully produced a validated/projected result, an exception, cancellation, invalid return, or
elapsed-budget failure in an after hook is wrapped as PostInvocationHookError so callers can
distinguish tool success from post-processing failure.
Privacy¶
Execution hooks are trusted local code, not redacted telemetry.
Before hooks receive the validated call arguments. After hooks receive the projected result payload. Do not register third-party or remote callbacks as hooks unless they are trusted to receive that data.
For privacy-preserving observability across less-trusted sinks, prefer the redacted RunEvent
stream or the OpenTelemetry exporter.
Relationship to approval¶
Approval and execution hooks serve different purposes.
- Approval is an explicit boolean authority gate controlled by
ExecutionPolicy.approval_mode. - Hooks are ordered middleware around already-authorized execution and may only observe or fail closed.
Hooks cannot approve an operation that policy or the approval callback rejected.
Direct executor use¶
executor = RegistryExecutor(
registry,
hooks=ExecutionHooks(
before_call=[before],
after_call=[after],
),
)
The same contract applies whether hooks are configured through SchemaRouter or directly on
RegistryExecutor.