#21224: Introduce optional runtime lifecycle hooks for tool and model boundaries
gateway
agents
size: M
Cluster:
Security Enhancements and Guardrails
# Introduce Optional Runtime Lifecycle Hooks for Tool and Model Boundaries
## Summary
This PR introduces a minimal, optional runtime lifecycle interception surface at two existing execution seams:
* Tool execution boundary (`src/gateway/tools-invoke-http.ts`)
* Model invocation boundary (`src/agents/pi-embedded-runner/run/attempt.ts`)
When no runtime policy is registered, behavior is identical to current `main`.
This change provides a stable interception contract for advanced instrumentation and runtime enforcement use cases, without altering default behavior or expanding public API surface.
---
## Motivation
OpenClaw already supports:
* Tool allow/deny policies
* Plugin tool registration
* Gateway-level restrictions
* Provider payload logging wrappers
* Cache tracing wrappers
However, there is currently no unified lifecycle hook surface for:
* Pre/post tool execution
* Pre/post model invocation
Advanced use cases (e.g., audit logging, runtime validation, compliance enforcement, research instrumentation) currently require patching core execution seams or maintaining forks.
This PR introduces a minimal interception contract to reduce fork pressure while keeping core lean and unopinionated.
---
## Design Principles
### 1. Optional by Default
* No runtime policy is active unless explicitly registered.
* When no policy is registered, behavior is unchanged.
* Injection points are guarded by cheap null checks.
### 2. Zero Breaking Changes
* No public APIs modified.
* No config schema changes.
* No CLI flags added.
* No environment variables added.
* All existing tests pass unchanged.
### 3. Minimal Surface Area
Added:
* `src/runtime/runtime-policy.ts`
* `src/runtime/runtime-policy-registry.ts`
Modified:
* `src/gateway/tools-invoke-http.ts`
* `src/agents/pi-embedded-runner/run/attempt.ts`
The injection footprint is intentionally small and localized to confirmed execution seams.
### 4. Pattern Alignment
The implementation follows the same wrapper pattern used by existing internal instrumentation layers (e.g., cache tracing and provider payload logging), preserving existing semantics and error mapping.
---
## What This Adds
### RuntimePolicy Interface
`src/runtime/runtime-policy.ts`
Optional hooks:
* `beforeToolInvoke`
* `afterToolInvoke`
* `beforeModelCall`
* `afterModelCall`
Hooks may:
* Inspect arguments
* Optionally transform arguments/results
* Throw errors to block execution
Hooks do not:
* Modify routing logic
* Modify authentication
* Modify session resolution
* Alter gateway policy enforcement
* Introduce new network surfaces
---
### RuntimePolicy Registry
`src/runtime/runtime-policy-registry.ts`
Provides:
* `registerRuntimePolicy(policy)`
* `getRuntimePolicy()`
* `clearRuntimePolicy()`
By default, `getRuntimePolicy()` returns `undefined`.
No dynamic loading.
No remote configuration.
No plugin auto-registration.
---
## Seam Injection Details
### Tool Execution
Location: `src/gateway/tools-invoke-http.ts`
Hook calls are injected at the confirmed execution seam:
```ts
const result = await (tool as any).execute?.(...)
```
The hooks run:
1. After existing allow/deny checks
2. Before tool execution
3. After successful execution
Error mapping remains unchanged:
* `ToolInputError` → HTTP 400
* Unexpected errors → HTTP 500
* Deny rules → HTTP 404
---
### Model Invocation
Location: `src/agents/pi-embedded-runner/run/attempt.ts`
Hooks are injected at the provider boundary where streaming/model execution is initiated.
Streaming guarantees preserved:
* Async iterator integrity unchanged
* No buffering introduced
* Abort signal propagation preserved
* No modification to backpressure behavior
When no policy is registered, the execution path is identical.
---
## Contract Guarantees
This runtime policy surface guarantees:
* Hooks execute after existing routing and policy checks.
* Hooks do not alter session resolution or gateway authentication.
* Hooks execute in the same async context as the original operation.
* When no policy is registered, there is no behavior or output change.
* The hook contract is internal and may evolve if needed.
---
## Error Propagation
If a policy hook throws:
* ToolInputError behavior remains unchanged.
* Unexpected errors propagate consistently with current mapping.
* No policy failure masks or transforms existing error categories.
---
## Performance Considerations
Injection is guarded by:
```ts
const policy = getRuntimePolicy();
if (policy?.beforeToolInvoke) { ... }
```
When no policy is registered, this results in only a property access and null check.
Existing performance-sensitive streaming behavior is unchanged.
---
## Security Considerations
* Policy registration is code-level only.
* No HTTP exposure.
* No config-driven runtime injection.
* No new plugin loading surface.
* Hooks run in the same trust context as core instrumentation wrappers.
This does not expand the untrusted attack surface.
---
## Tests
Added:
* Unit tests for runtime policy hook invocation.
* Tests verifying inert behavior when no policy is registered.
* Tests ensuring error mapping remains consistent.
Existing tests pass unchanged.
Local verification:
* `pnpm check` : PASS
* `pnpm test` : PASS
---
## Non-Goals
This PR does NOT:
* Introduce a governance framework
* Add configuration schema changes
* Add CLI flags
* Add environment variables
* Expose policy registration via HTTP
* Modify tool routing behavior
* Modify provider selection logic
It strictly introduces a minimal lifecycle interception surface.
---
## Why Core Instead of Plugin?
The current plugin system does not expose lifecycle hooks at the tool execution or model provider boundary.
Without this seam, advanced instrumentation requires patching core or maintaining forks.
This PR introduces a stable interception surface while preserving all default behavior.
---
## Implementation Scope
* 2 new files
* Minimal injection at confirmed execution seams
* No refactors
* No behavior change when unused
---
Feedback is welcome regarding naming, hook granularity, or scope adjustments.
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge with minimal risk - it introduces an optional hook system that is inert by default
- The implementation is clean, minimal, and follows existing patterns in the codebase. The hooks are completely optional and zero-impact when not registered. However, there is one minor concern: if hook functions throw errors, they will propagate and potentially break tool execution or model calls. While this might be intentional, it could benefit from explicit error handling or documentation about hook exception behavior.
- No files require special attention - the implementation is straightforward and well-tested
<sub>Last reviewed commit: e628342</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#6095: feat(gateway): support modular guardrails extensions for securing a...
by Reapor-Yurnero · 2026-02-01
79.7%
#18889: feat(hooks): add agent and tool lifecycle boundaries
by vincentkoc · 2026-02-17
76.1%
#6405: feat(security): Add HTTP API security hooks for plugin scanning
by masterfung · 2026-02-01
75.8%
#15571: feat: infrastructure foundation — hooks, model failover, sessions, ...
by tangcruz · 2026-02-13
75.1%
#12082: feat: implement plugin lifecycle interception hook architecture
by tomismeta · 2026-02-08
74.3%
#20580: feat(hooks): bridge after_tool_call to internal hook handler system
by CryptoKrad · 2026-02-19
73.7%
#17667: feat: tool-hooks extension — run shell commands on tool calls
by FaradayHunt · 2026-02-16
73.5%
#22873: fix(tools): enforce global inline-secret blocking for tool inputs
by Kansodata · 2026-02-21
73.1%
#17930: fix: evaluate tool_result_persist hooks lazily to avoid race condition
by TheArkifaneVashtorr · 2026-02-16
72.7%
#22068: Add tool:before/tool:after internal hook events
by yhindy · 2026-02-20
72.6%