Skip to content

hooks: unify registry, fix 5 systemic bugs across both hook systems#30860

Closed
mcaxtr wants to merge 6 commits intoopenclaw:mainfrom
mcaxtr:unified-hook-registry
Closed

hooks: unify registry, fix 5 systemic bugs across both hook systems#30860
mcaxtr wants to merge 6 commits intoopenclaw:mainfrom
mcaxtr:unified-hook-registry

Conversation

@mcaxtr
Copy link
Copy Markdown
Contributor

@mcaxtr mcaxtr commented Mar 1, 2026

Summary

  • Problem: Two independent hook systems (internal HOOK.md + plugin typed HookRunner) use module-level singletons duplicated by Rolldown bundler. clearInternalHooks() wipes plugin hooks. gateway:startup races with hook loading. sessions.reset never fires before_reset. after_compaction missing sessionKey.
  • Why it matters: 30+ bugs filed over 2 months trace back to these shared root causes
  • What changed: Single globalThis[Symbol.for()]-backed registry with source-tagged entries, source-scoped clearing, unified dispatch helpers, missing hook event fixes
  • What did NOT change (scope boundary): HookRunner facade, all 24 typed hook definitions, existing hook handler signatures, plugin API surface

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

None. All changes are internal to hook dispatch infrastructure. Existing hooks fire at the same times with the same arguments.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22+ / pnpm
  • Model/provider: N/A
  • Integration/channel (if any): All (hook system is channel-agnostic)
  • Relevant config (redacted): hooks.internal.enabled: true

Steps

  1. Register a plugin hook via api.registerHook("message:received", handler)
  2. Trigger a gateway SIGUSR1 restart (hot-reload)
  3. Send a message to trigger message_received

Expected

  • Plugin hook fires after restart (source-scoped clearing preserves plugin hooks)

Actual

  • Before this PR: plugin hook is wiped by handlers.clear() and never fires again

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

28 new tests: hook-registry.test.ts (18 tests), dispatch-unified.test.ts (10 tests). All 229 existing tests pass.

Human Verification (required)

  • Verified scenarios: pnpm build (type-check), pnpm check (lint/format), pnpm test (229 tests pass), 3 rounds of codex review
  • Edge cases checked: hookRunner null during early startup, clearInternalHooks idempotent, empty registry dispatch, handler throws (others still execute), metadata mutation isolation between dispatch paths
  • What you did not verify: Live gateway with real plugin hooks (no staging environment)

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert the PR; module-level singletons restored
  • Files/config to restore: N/A
  • Known bad symptoms reviewers should watch for: Hooks not firing after restart, duplicate hook execution, gateway:startup timing changes

Risks and Mitigations

  • Risk: clearAllHooks() on SIGUSR1 restart clears plugin-source hooks from the unified registry

    • Mitigation: Matches original handlers.clear() behavior exactly. Plugin typed hooks on HookRunner are stored separately and unaffected. Plugin internal hooks are re-registered by loadGatewayPlugins() at the start of each gateway iteration.
  • Risk: Dispatch helpers change hook firing order for overlapping events

    • Mitigation: Plugin hooks fire before internal hooks in all 3 helpers, matching the original call-site ordering. 10 dispatch tests verify both systems fire.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 1, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Hook source spoofing allows file-loaded code to register persistent hooks that evade hot-reload clearing

1. 🔵 Hook source spoofing allows file-loaded code to register persistent hooks that evade hot-reload clearing

Property Value
Severity Low
CWE CWE-20
Location src/hooks/internal-hooks.ts:164-175

Description

registerInternalHook() accepts an unvalidated string source and casts it to HookRegistrySource at runtime. This allows any code running in-process (including file-discovered internal hook handler modules) to register hooks tagged as "plugin" (or an arbitrary/unknown tag) so they survive clearInternalHooks() on hot reload.

Impact:

  • clearInternalHooks() clears only FILE_HOOK_SOURCES (bundled,workspace,managed,config) and intentionally preserves plugin hooks.
  • Because registerInternalHook() will accept caller-provided source strings without runtime validation, a file-based hook handler can call registerInternalHook(event, handler, "plugin") (or e.g. "evil").
  • Such hooks will not be removed by hook reload (clearInternalHooks()), and unknown tags will also not be cleared by plugin reload (clearHooksBySource(["plugin"])).
  • Result: hooks can persist in memory after the originating hook files/config are removed or hot-reloaded, creating an unexpected persistence mechanism.

Vulnerable code:

const registrySource: HookRegistrySource =
  (source ? (FILE_SOURCE_TO_REGISTRY[source] ?? (source as HookRegistrySource)) : undefined) ??
  "config";

Notes on data/control flow:

  • src/hooks/loader.ts passes entry.hook.source for discovered hooks (safe, fixed set), but any other caller (including a loaded hook module) can pass arbitrary strings.
  • src/hooks/hook-registry.ts enforces HookRegistrySource only at the TypeScript type level; runtime values are stored as-is and clearing is string-match based.

Recommendation

Enforce a strict runtime allowlist for source and prevent file-discovered hooks from ever selecting "plugin" (or arbitrary strings).

Suggested fix (runtime validation + narrower API):

import type { HookSource } from "./types";

const ALLOWED_REGISTRY_SOURCES = new Set<HookRegistrySource>([
  "bundled",
  "workspace",
  "managed",
  "config",
  "plugin",
]);

export function registerInternalHook(
  eventKey: string,
  handler: InternalHookHandler,
  source?: HookSource | HookRegistrySource,
): void {
  const mapped = source && source in FILE_SOURCE_TO_REGISTRY
    ? FILE_SOURCE_TO_REGISTRY[source as HookSource]
    : source;

  const registrySource: HookRegistrySource =
    mapped && ALLOWED_REGISTRY_SOURCES.has(mapped as HookRegistrySource)
      ? (mapped as HookRegistrySource)
      : "config";// Optional: if called from file loader, explicitly disallow "plugin".
  registerHook(eventKey, handler as (...args: unknown[]) => unknown, { source: registrySource });
}

Additionally:

  • Consider splitting APIs: registerFileInternalHook(...) (no plugin source allowed) vs registerPluginInternalHook(...).
  • Consider defaulting unknown values to "config" and logging a warning, or throwing to fail fast in dev/test.

Analyzed PR: #30860 at commit b77dc60

Last updated on: 2026-03-02T19:04:56Z

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime agents Agent runtime and tooling size: XL experienced-contributor labels Mar 1, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70b9b1d47a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR successfully unifies two independent hook systems (internal HOOK.md + plugin HookRunner) into a single globalThis[Symbol.for()]-backed registry, fixing 5 systemic bugs that have plagued the codebase for months.

Key Changes:

  • Replaced module-level singleton Maps with globalThis[Symbol.for("openclaw:hookRegistry")] to survive bundler chunk splitting
  • Added source tagging ("bundled", "workspace", "managed", "config", "plugin") to enable source-scoped clearing
  • Implemented unified dispatch helpers (emitMessageReceived, emitMessageSent, emitGatewayStartup) to consolidate dual-dispatch logic
  • Fixed sessions.reset to properly fire before_reset hook with full message history
  • Fixed after_compaction to include missing sessionKey parameter
  • Fixed gateway:startup race condition by calling synchronously after hook loading (removed setTimeout(250))

Architecture:

  • The unified registry maintains backward compatibility - both hook facades (registerInternalHook and HookRunner.run*()) continue to work unchanged
  • Plugin hooks now survive hot-reload by being tagged with "plugin" source while internal hooks are cleared
  • Priority ordering ensures deterministic execution order
  • Comprehensive documentation added to AGENTS.md explaining the architecture and rules

Testing:

  • 28 new tests added (18 for registry, 10 for dispatch)
  • Tests verify source-scoped clearing, priority ordering, metadata isolation, and dual-dispatch behavior
  • All 229 existing tests reportedly pass

The refactor is well-architected with clear separation of concerns, defensive programming (metadata cloning), and thorough test coverage.

Confidence Score: 4/5

  • Safe to merge with minor validation recommended for production gateway restart scenarios
  • Excellent architectural design with comprehensive tests (28 new, 229 passing total), but lacks live gateway validation with real plugin hooks as noted in PR description. The core logic is sound, source-scoped clearing properly preserves plugin hooks, and all stated bugs are fixed. Deducted one point solely due to untested staging environment scenario.
  • No files require special attention - the implementation is consistent and well-tested across all changed files

Last reviewed commit: 7cd3939

@mcaxtr
Copy link
Copy Markdown
Contributor Author

mcaxtr commented Mar 1, 2026

@greptile please re-review this PR.

Since the initial review (70b9b1d), commit 7cd3939 addresses two issues:

  1. clearAllHooks() replaced with clearHooksBySource(["bundled", "workspace", "managed", "config"]) in server-startup.ts — plugin hooks now properly survive startup clearing. clearHooksBySource(["plugin"]) added in server.impl.ts before loadGatewayPlugins() to prevent duplicates on SIGUSR1 restart.

  2. sessions.reset before_reset hook is now fire-and-forget (matching commands-core.ts pattern) so slow plugin handlers don't block the RPC response.

@mcaxtr
Copy link
Copy Markdown
Contributor Author

mcaxtr commented Mar 1, 2026

Re: Aisle security finding — the unbounded readFile pattern in sessions.reset is identical to the existing code in commands-core.ts:100 which has the same readFile + split("\n") + JSON.parse pattern without size guards. This is pre-existing behavior, not introduced by this PR. Both paths are fire-and-forget and gated behind hookRunner.hasHooks("before_reset"), and sessions.reset is an authenticated admin RPC. Adding a size cap across both code paths would be a reasonable hardening follow-up but is out of scope for this hook unification PR.

@mcaxtr mcaxtr force-pushed the unified-hook-registry branch from 7cd3939 to bc6182c Compare March 2, 2026 16:01
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc6182cb6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@mcaxtr mcaxtr force-pushed the unified-hook-registry branch from bc6182c to 8006f37 Compare March 2, 2026 17:48
@mcaxtr mcaxtr force-pushed the unified-hook-registry branch from 8006f37 to b77dc60 Compare March 2, 2026 17:51
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8006f37f6e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b77dc60298

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@mcaxtr
Copy link
Copy Markdown
Contributor Author

mcaxtr commented Mar 2, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b77dc60298

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@mcaxtr
Copy link
Copy Markdown
Contributor Author

mcaxtr commented Mar 2, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b77dc60298

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 3, 2026

Thanks for the PR! Multiple PRs address the same fix for #30784. Keeping #30853 as the earlier submission. Closing to reduce noise. This is an AI-assisted triage review. If we got this wrong, feel free to reopen — happy to revisit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment