Skip to content

feat(plugins): add full conversation context to before_tool_call hook#58079

Closed
Reapor-Yurnero wants to merge 10 commits into
openclaw:mainfrom
GraySwanAI:feat/before-tool-call-context
Closed

feat(plugins): add full conversation context to before_tool_call hook#58079
Reapor-Yurnero wants to merge 10 commits into
openclaw:mainfrom
GraySwanAI:feat/before-tool-call-context

Conversation

@Reapor-Yurnero

@Reapor-Yurnero Reapor-Yurnero commented Mar 31, 2026

Copy link
Copy Markdown

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: before_tool_call only exposed toolName / params, which is not enough for an external policy / guardrail plugin to evaluate a tool call in context to look out for prompt injections / policy-violated actions etc.
  • Why it matters: contextual inspection needs the same model-facing view that produced the tool call: effective system prompt, conversation snapshot, and available tool definitions.
  • What changed: before_tool_call now optionally includes systemPrompt, messages, and tools, wired from the embedded runner into the existing hook payload.
  • What did NOT change (scope boundary): no new hook, no change to hook timing/order, no compatibility issue, no change to hook actions/returns, no bundled guardrail framework, no config changes.

Change Type (select all)

  • Feature

Scope (select all touched areas)

  • API / contracts

Linked Issue/PR

User-visible / Behavior Changes

  • Plugin authors can now read systemPrompt, messages, and tools from before_tool_call.
  • Existing plugins remain compatible; the added fields are optional and additive.
  • No user-facing behavior/config changes in the default runtime.

Diagram (if applicable)

Before:
[model emits tool call] 
  -> [before_tool_call gets toolName + params only]

After:
[model emits tool call]
  -> [before_tool_call gets toolName + params + systemPrompt + messages + tools]
  -> [plugin decides allow/block/param rewrite]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) Yes
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) Yes
  • If any Yes, explain risk + mitigation:
    • This expands what a before_tool_call plugin can observe by adding context (systemPrompt, messages, tools).
    • The hook action space is unchanged: plugins can still only allow, block, or modify params as before.
    • The change is additive and limited to already-installed plugin code running on the same hook path.

Repro + Verification

Environment

  • OS: Ubuntu/Linux
  • Runtime/container: local pnpm dev build
  • Model/provider: openai/gpt-5.4
  • Integration/channel (if any): none
  • Relevant config (redacted): local standalone external plugin linked via openclaw plugins install --link, using beforeToolCall only

Steps

  1. Install a standalone external plugin that listens to before_tool_call and forwards the payload to a local dummy HTTP endpoint.
  2. Start the gateway and run an agent prompt that causes a tool call.
  3. Inspect the captured payload at the dummy endpoint.

Expected

  • The plugin receives systemPrompt, messages, and tools alongside the existing toolName / params.

Actual

  • Verified locally: the captured payload includes full conversation context, effective system prompt, and tool definitions before tool execution.

Evidence

Attach at least one:

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

End-to-end local test with an external standalone plugin posting hook payloads to a dummy HTTP serve

Request 1
roles: system,user,assistant
tool_count: 27
metadata_keys: cygnal_bypass, openclaw_hook, openclaw_tool_call_id, openclaw_tool_name
last_tool_call.name: read
last_tool_call.arguments: {"path":"/home/autoswan/.openclaw/workspace/AGENTS.md","offset":1,"limit":1}

Request 2
roles: system,user,assistant,tool,assistant,user,assistant
tool_count: 27
metadata_keys: cygnal_bypass, openclaw_hook, openclaw_tool_call_id, openclaw_tool_name
last_tool_call.name: read
last_tool_call.arguments: {"path":"/home/autoswan/.openclaw/workspace/TOOLS.md","offset":1,"limit":1}

Gateway trace from the same run:

[plugins] [hooks] running before_tool_call (1 handlers, sequential)
[plugins] stage=before_tool_call monitor:start url=http://127.0.0.1:18888/dummy
[plugins] stage=before_tool_call monitor:response-headers status=200
[plugins] stage=before_tool_call monitor:success status=200

Automated Verification

  • pnpm build, pnpm test:contracts:plugins, pnpm check passes
  • pnpm test failed exactly the same as the upstream main current state.
  • Ran codex review --base upstream/main and incorporated the suggestion to propagate before_tool_call context to bundled MCP/LSP tools so the hook behavior is consistent across various tool sources.

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • focused unit test for before_tool_call context forwarding
    • full local build
    • end-to-end local test with an external standalone plugin posting hook payloads to a dummy HTTP server
  • Edge cases checked:
    • multi-step session where the second tool call payload included prior conversation/tool history
    • existing hook behavior remained unchanged apart from added optional fields
  • What you did not verify:
    • non-embedded runner paths
    • actually published marketplace packaging/distribution flow

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: plugins on before_tool_call can now observe more runtime context than before.
    • Mitigation: the change is additive and optional only, and is therefore fully backward-compatible.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 31, 2026
@greptile-apps

greptile-apps Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the before_tool_call plugin hook to include the effective system prompt, a conversation snapshot, and the list of available tool definitions — giving guardrail/policy plugins the full model-facing context needed to evaluate tool calls for prompt-injection or policy violations. The change is additive (all three fields are optional), backward-compatible, and correctly wired through every tool path (openclaw built-in tools via createOpenClawCodingTools, MCP/LSP tools via toToolDefinitions/splitSdkTools, and client tools via toClientToolDefinitions). Two P2 issues were found:

  • Live mutable reference for messages (attempt.ts:874): activeSession.messages is handed directly to the getMessages closure. For async external plugins that spend time awaiting an HTTP call, the array may have been mutated by the time they inspect it, violating the "snapshot" semantics documented in PluginHookBeforeToolCallEvent. A shallow clone ([...activeSession.messages]) at getter-call time would fix this.
  • systemPrompt: undefined key inconsistency (pi-tools.before-tool-call.ts:208): The guard args.ctx?.getSystemPrompt && { systemPrompt: args.ctx.getSystemPrompt() } only checks that the function exists, not that its return value is defined. If getSystemPrompt() returns undefined, in-process plugins see { systemPrompt: undefined } (key present), while JSON-serialized external plugins see the key omitted — an inconsistent API surface.

Confidence Score: 4/5

Safe to merge with minor polishing; no data loss or security regression introduced.

The implementation is clean, additive, and well-tested. The two findings are P2: the live-array reference for messages is a documentation/contract mismatch unlikely to cause runtime failures in practice (JavaScript is single-threaded and tool execution is sequential), and the systemPrompt undefined spread inconsistency only surfaces in a very narrow early-initialization window. Neither blocks merge but both are worth addressing before the plugin API stabilises.

src/agents/pi-embedded-runner/run/attempt.ts (messages provider at line 874) and src/agents/pi-tools.before-tool-call.ts (systemPrompt spread guard at line 208).

Important Files Changed

Filename Overview
src/agents/pi-tools.before-tool-call.ts Adds getSystemPrompt/getMessages/getTools lazy getters to HookContext and expands runBeforeToolCallHook payload. Two minor issues: undefined spread inconsistency for systemPrompt and live array reference semantics for messages.
src/agents/pi-embedded-runner/run/attempt.ts Wires beforeToolCallContext (lazy getters for systemPrompt, messages, tools) into tools and hooks. The messages provider at line 874 hands a live mutable array to the hook payload rather than a snapshot.
src/agents/pi-embedded-runner/tool-split.ts Threads optional hookContext through to toToolDefinitions so MCP/LSP tools receive full conversation context in before_tool_call. Clean, minimal change.
src/agents/pi-tool-definition-adapter.ts Adds optional hookContext parameter to toToolDefinitions and passes it when invoking runBeforeToolCallHook for unwrapped tools. Consistent with existing pattern.
src/agents/pi-tools.ts Exposes beforeToolCallContext as an optional parameter on createOpenClawCodingTools and spreads it into the wrapToolWithBeforeToolCallHook call. Omits identity/session fields as documented.
src/plugins/types.ts Adds PluginHookToolInfo type and three optional fields (systemPrompt, messages, tools) to PluginHookBeforeToolCallEvent. Fully additive, backward-compatible.
src/agents/pi-tools.before-tool-call.context.test.ts New unit test validating that systemPrompt/messages/tools are forwarded through runBeforeToolCallHook. Does not cover the getSystemPrompt-returns-undefined edge case.
src/agents/pi-tool-definition-adapter.after-tool-call.test.ts Adds a test verifying that hookContext fields are passed through toToolDefinitions correctly. Straightforward coverage.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-tools.before-tool-call.ts
Line: 208-210

Comment:
**`systemPrompt: undefined` spreads key with undefined value**

The guard `args.ctx?.getSystemPrompt && { ... }` checks only that the *function* exists, not that it returns a defined value. If `getSystemPrompt()` returns `undefined` (e.g. during the brief window before `beforeToolCallSystemPrompt` is first assigned), this spreads `{ systemPrompt: undefined }` into the payload. In-process plugins would see the key present (with value `undefined`); JSON-serialized external plugins would silently drop the key. This creates an inconsistent API surface between plugin delivery modes.

A consistent pattern would be:

```suggestion
        ...(args.ctx?.getSystemPrompt?.() !== undefined && { systemPrompt: args.ctx.getSystemPrompt() }),
        ...(args.ctx?.getMessages && { messages: args.ctx.getMessages() }),
        ...(args.ctx?.getTools && { tools: args.ctx.getTools() }),
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 874

Comment:
**`messages` is a live mutable reference, not a snapshot**

`activeSession.messages` is the runtime's internal array, updated in place as the conversation progresses. By the time an async plugin hook returns (e.g. after an HTTP round-trip to a guardrail service), the array may already contain new entries that weren't present when the hook was invoked. The `PluginHookBeforeToolCallEvent` type documents this field as "Current conversation snapshot," but the live reference violates that contract for any plugin that either (a) stores the reference or (b) awaits an async operation before reading it.

A shallow clone at the time the getter is called would satisfy the "snapshot" contract without any functional overhead in the common case:

```typescript
beforeToolCallMessagesProvider = () => [...activeSession.messages];
```

Note: the earlier provider set at line 736–737 (`buildSessionContext().messages`) already returns a freshly built array on each call, so it is consistent with snapshot semantics. This line should match that behaviour.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(plugins): propagate before_tool_call..." | Re-trigger Greptile

Comment thread src/agents/pi-tools.before-tool-call.ts Outdated
Comment thread src/agents/pi-embedded-runner/run/attempt.ts

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

Copy link
Copy Markdown
Contributor

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: 804e632c6a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-tools.before-tool-call.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord channel: telegram Channel integration: telegram size: M and removed size: S labels Mar 31, 2026

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

Copy link
Copy Markdown
Contributor

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: 44ef0440f8

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-tools.before-tool-call.ts
Comment thread src/agents/pi-tool-definition-adapter.ts
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 30, 2026
@clawsweeper

clawsweeper Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge.

What this changes:

The PR adds optional systemPrompt, messages, and tools fields to before_tool_call hook payloads and threads lazy context through embedded-runner tool wrappers with focused tests.

Maintainer follow-up before merge:

This is a public plugin API and security-sensitive hook-context change on an unmergeable, unmodifiable contributor branch; a maintainer should decide whether to request a rebase, ask for a replacement PR, or narrow the API before automation attempts code changes.

Security review:

Security review needs attention: The diff needs security attention because PR head can silently bypass before_tool_call guardrail/approval hooks when snapshot cloning throws.

Review findings:

  • [P2] Keep hooks from being skipped when snapshot cloning fails — src/agents/pi-tools.before-tool-call.ts:222-230
  • [P2] Move hook event fields to the current contract file — src/plugins/types.ts:2150-2169
  • [P3] Add the required changelog entry — src/plugins/types.ts:2164-2169
Review details

Best possible solution:

Pursue a rebased or replacement implementation that updates the current hook-types contract, docs, changelog, and contract tests, while preserving fail-closed hook behavior or dropping only uncloneable optional snapshot fields without skipping before_tool_call handlers.

Do we have a high-confidence way to reproduce the issue?

Yes for the blocking review finding: on PR head, make getMessages/getTools return a value structuredClone cannot clone and run a tool call with a before_tool_call hook; the hookEvent construction throws, the catch logs, and execution falls through as allowed. Current-main behavior was verified by source inspection rather than executing tests because this was a read-only review.

Is this the best way to solve the issue?

No as-is. The narrow additive hook-context direction is plausible, but the current PR branch is stale, unmergeable, missing current hook contract placement, and can bypass the very policy hooks it is meant to empower.

Full review comments:

  • [P2] Keep hooks from being skipped when snapshot cloning fails — src/agents/pi-tools.before-tool-call.ts:222-230
    The new hookEvent is built by cloning params/messages/tools with structuredClone inside the same try that dispatches before_tool_call. If any snapshot field is not cloneable, the catch only logs and the function falls through to return blocked:false, so guardrail or approval hooks never run and the tool proceeds. Drop the bad optional snapshot field or fail closed; do not skip hook enforcement.
    Confidence: 0.91
  • [P2] Move hook event fields to the current contract file — src/plugins/types.ts:2150-2169
    Current main moved plugin hook contracts into src/plugins/hook-types.ts and src/plugins/types.ts now re-exports them. Adding systemPrompt/messages/tools to the old types.ts location leaves the branch dirty and will not update the public api.on("before_tool_call") contract after rebase. Move PluginHookToolInfo and the event fields to hook-types.ts with the matching tests/baseline updates.
    Confidence: 0.86
  • [P3] Add the required changelog entry — src/plugins/types.ts:2164-2169
    This is a plugin-author visible feature, but the PR files do not include CHANGELOG.md. Repo policy requires user-facing feat/fix/perf changes to add a single-line changelog entry in the active Changes section with contributor credit.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.88

Security concerns:

  • [medium] Snapshot clone failure can bypass policy hooks — src/agents/pi-tools.before-tool-call.ts:222
    The proposed full-context snapshot is cloned before dispatching before_tool_call, but clone errors are caught by the hook failure path that logs and then allows the original tool call. A non-cloneable value in params/messages/tools can therefore skip installed policy hooks that would otherwise block or require approval.
    Confidence: 0.9

What I checked:

Likely related people:

  • vincentkoc: Recent commits on plugin hook contract files and docs include adding sanitized model-call hooks, hook correlation fields, before-agent-finalize, and legacy hook deprecation notes. (role: recent hook contract maintainer; confidence: high; commits: 275c128e99bc, 3bd2ee78b6ac, 1b25dcf57a9f; files: src/plugins/hook-types.ts, docs/plugins/hooks.md)
  • steipete: Recent main history shows repeated maintenance in the embedded runner and before_tool_call path, including run-scoped loop detection and many adjacent embedded-runner fixes. (role: recent embedded-runner and hook-path maintainer; confidence: high; commits: 6a55a00da461, 1a2228d29151, 470098bd26f3; files: src/agents/pi-tools.before-tool-call.ts, src/agents/pi-embedded-runner/run/attempt.ts)
  • 100yenadmin: Authored the recent generic plugin host-hook contracts commit that touched before_tool_call files and moved relevant contract ownership into the current hook surface. (role: plugin host-hook contract introducer; confidence: medium; commits: 1adaa28dc86b; files: src/agents/pi-tools.before-tool-call.ts, src/plugins/hook-types.ts)
  • pgondhi987: Authored the current-main hardening that makes before_tool_call hook failures fail closed, which is directly relevant to the PR head's stale warn-and-allow behavior. (role: before_tool_call fail-closed behavior author; confidence: medium; commits: e19dce0aedbc; files: src/agents/pi-tools.before-tool-call.ts)

Remaining risk / open question:

  • The source branch is dirty and maintainer_can_modify is false, so normal maintainer repair on the contributor branch is blocked.
  • The change intentionally expands data visible to before_tool_call plugins, so it needs explicit plugin API and security review even though installed plugins are already privileged.
  • Current main has since moved hook contracts into src/plugins/hook-types.ts, so the PR's src/plugins/types.ts contract edit is stale.
  • No tests were run in this read-only review; conclusions are based on source, PR diff, API metadata, docs, and discussion inspection.

Codex review notes: model gpt-5.5, reasoning high; reviewed against e47a7448e90f.

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

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord channel: telegram Channel integration: telegram size: M stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant