← Back to PRs

#21224: Introduce optional runtime lifecycle hooks for tool and model boundaries

by mikeholownych open 2026-02-19 20:09 View on GitHub →
gateway agents size: M
# 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