Skip to content

fix(memory): add qmd mcporter search tool override#57363

Merged
vincentkoc merged 8 commits intomainfrom
fix/qmd-search-tool-override
Mar 30, 2026
Merged

fix(memory): add qmd mcporter search tool override#57363
vincentkoc merged 8 commits intomainfrom
fix/qmd-search-tool-override

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

  • Problem: memory.qmd.searchMode is intentionally validated to a small semantic enum, but mcporter-backed QMD deployments can expose custom MCP tool names such as hybrid_search.
  • Why it matters: users currently have to patch OpenClaw to target custom QMD MCP tools even when the runtime wiring is otherwise compatible.
  • What changed: add optional memory.qmd.searchTool as an exact mcporter tool override, keep searchMode semantic, and use the override only for mcporter calls.
  • What did NOT change (scope boundary): direct CLI QMD search behavior, the validated searchMode enum, and the normal built-in mcporter tool mapping when no override is set.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • 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

Root Cause / Regression History (if applicable)

  • Root cause: searchMode was doing two jobs at once: semantic retrieval mode selection and mcporter tool-name binding.
  • Missing detection / guardrail: config validation allowed only the semantic enum, but there was no separate surface for exact mcporter tool names.
  • Prior context (git blame, prior PR, issue, or refactor if known): fix: QMD 1.1+ mcporter compatibility with legacy fallback [AI-assisted] #54728 fixed the built-in QMD MCP tool migration path, but custom tool names remained impossible without patching core.
  • Why this regressed now: QMD MCP usage has expanded and custom tool deployments are now a real compatibility path.
  • If unknown, what was ruled out: N/A.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: packages/memory-host-sdk/src/host/backend-config.test.ts, extensions/memory-core/src/memory/qmd-manager.test.ts
  • Scenario the test should lock in: resolved config preserves a trimmed searchTool, and mcporter searches call the explicit tool with flat query args instead of forcing the built-in tool mapping.
  • Why this is the smallest reliable guardrail: the bug is config-resolution plus mcporter request-shape wiring; both are deterministic and do not need a full external QMD runtime.
  • Existing test that already covers this (if any): existing mcporter query/v1-fallback tests cover the default path when searchTool is unset.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

  • New optional config key: memory.qmd.searchTool
  • When memory.qmd.mcporter.enabled=true, users can target an exact mcporter tool name such as hybrid_search
  • When unset, behavior stays on the existing validated searchMode mapping

Diagram (if applicable)

Before:
mcporter-enabled search -> searchMode enum -> hardcoded built-in tool mapping only

After:
mcporter-enabled search -> searchTool override if set -> exact MCP tool call
                                  else -> existing searchMode mapping

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (Yes)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:
    The command surface change is config-gated and mcporter-only. The override is an explicit exact tool name, so users opt into a narrower, auditable behavior instead of broadening searchMode validation to arbitrary strings.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): N/A
  • Relevant config (redacted): memory.backend=qmd, memory.qmd.mcporter.enabled=true, memory.qmd.searchTool=hybrid_search

Steps

  1. Configure OpenClaw with memory.backend=qmd, memory.qmd.mcporter.enabled=true, and a custom memory.qmd.searchTool.
  2. Trigger a memory search through the QMD backend.
  3. Inspect the mcporter selector and args.

Expected

  • OpenClaw calls the exact configured mcporter tool name.
  • Default behavior remains unchanged when searchTool is unset.

Actual

  • Before this change, config validation and the hardcoded mapping forced only the built-in tool names.

Evidence

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

Human Verification (required)

  • Verified scenarios:
    • pnpm test -- packages/memory-host-sdk/src/host/backend-config.test.ts
    • pnpm test -- extensions/memory-core/src/memory/qmd-manager.test.ts
    • pnpm build
    • pnpm config:docs:check
  • Edge cases checked:
    • trimmed searchTool config values
    • default mcporter path still uses the existing query/v1 fallback behavior when no override is set
  • What you did not verify:
    • a live external QMD/mcporter deployment with a real custom MCP tool

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.

Compatibility / Migration

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

Risks and Mitigations

  • Risk:
    • Custom tools may expect a different argument schema than the built-in flat query contract.
  • Mitigation:
    • Scope the override to exact mcporter tool selection only; keep built-in tools on the existing mapped behavior, and document the override as intended for compatible custom tools.

@vincentkoc vincentkoc self-assigned this Mar 30, 2026
@openclaw-barnacle openclaw-barnacle bot added docs Improvements or additions to documentation extensions: memory-core Extension: memory-core size: S maintainer Maintainer-authored PR labels Mar 30, 2026
@vincentkoc vincentkoc marked this pull request as ready for review March 30, 2026 00:42
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR introduces an optional memory.qmd.searchTool config key that lets users with mcporter-backed QMD deployments target a custom MCP tool name (e.g. hybrid_search) without relaxing the validated searchMode enum or patching core code. The change is well-scoped: the override is mcporter-only, the default path is entirely unchanged, and the v1 fallback / v2 version-probe logic is correctly bypassed when an explicit tool is supplied.

Key changes:

  • MemoryQmdConfig / ResolvedQmdConfig gain searchTool?: string; Zod schema and generated baseline updated accordingly
  • resolveSearchTool trims whitespace and returns undefined for blank values (clean normalisation)
  • explicitToolOverride: boolean flag threads from the call site down through runMcporterAcrossCollectionsrunQmdSearchViaMcporter, gating the v1 fallback retry, the searches-array format, and the v2 version probe
  • New unit tests cover config resolution (whitespace trimming) and single-collection dispatch with a custom tool name

Minor observations (all P2):

  • The tool parameter type in runQmdSearchViaMcporter / runMcporterAcrossCollections was widened from a known-tool union to string, which reduces compile-time safety on the non-override code path
  • The multi-collection branch with explicitToolOverride=true is not covered by tests
  • The Zod schema accepts empty strings for searchTool; runtime resolution normalises them away but a z.string().min(1) constraint would surface the mistake earlier

Confidence Score: 5/5

Safe to merge — all remaining findings are style/test-coverage suggestions with no impact on runtime correctness

The core logic is correct: explicitToolOverride cleanly separates the override path from the default path, v1-fallback and v2-probe are correctly skipped for custom tools, and flat query args are correctly used. Default behavior is fully preserved when searchTool is unset. The three open findings are all P2 and none affect present behavior.

No files require special attention beyond the P2 suggestions on zod-schema.ts and the test files

Important Files Changed

Filename Overview
extensions/memory-core/src/memory/qmd-manager.ts Adds explicitToolOverride flag throughout the mcporter search path. Logic for bypassing v1-fallback, the typed searches-array format, and the v2 version probe is correctly gated on the flag. Minor: tool param type widened to string.
packages/memory-host-sdk/src/host/backend-config.ts Adds resolveSearchTool helper (trims whitespace, returns undefined for blank values) and threads the result into ResolvedQmdConfig. Clean and correct.
packages/memory-host-sdk/src/host/backend-config.test.ts New test verifies that a whitespace-padded searchTool value is trimmed and preserved in resolved config while searchMode is unchanged.
extensions/memory-core/src/memory/qmd-manager.test.ts New test validates single-collection flat-query dispatch for the hybrid_search override. Multi-collection override path is not covered.
src/config/zod-schema.ts Adds searchTool as an optional string; accepts empty strings at the schema level even though resolveSearchTool normalises them away.
src/config/types.memory.ts Minimal addition of searchTool?: string to MemoryQmdConfig.

Comments Outside Diff (2)

  1. extensions/memory-core/src/memory/qmd-manager.test.ts, line 1052-1091 (link)

    P2 Multi-collection override path not covered by tests

    The test uses a single paths entry, so it exercises only the single-collection branch (runQmdSearchViaMcporter directly). The multi-collection branch via runMcporterAcrossCollections — which also has the new explicitToolOverride param threaded through it — is not tested.

    While the code path looks correct from the diff alone, adding a second paths entry to this test (or a new dedicated test) would lock in the multi-collection flat-query behavior and protect against future regressions in runMcporterAcrossCollections.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/memory-core/src/memory/qmd-manager.test.ts
    Line: 1052-1091
    
    Comment:
    **Multi-collection override path not covered by tests**
    
    The test uses a single `paths` entry, so it exercises only the single-collection branch (`runQmdSearchViaMcporter` directly). The multi-collection branch via `runMcporterAcrossCollections` — which also has the new `explicitToolOverride` param threaded through it — is not tested.
    
    While the code path looks correct from the diff alone, adding a second paths entry to this test (or a new dedicated test) would lock in the multi-collection flat-query behavior and protect against future regressions in `runMcporterAcrossCollections`.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/config/zod-schema.ts, line 1368 (link)

    P2 searchTool accepts empty strings at the Zod validation layer

    searchTool: z.string().optional(),

    An empty-string value passes Zod validation and is only normalised to undefined later in resolveSearchTool. This is harmless at runtime, but the schema will silently accept memory.qmd.searchTool: "" without surfacing a validation error to the user. Consider aligning schema and resolution:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/config/zod-schema.ts
    Line: 1368
    
    Comment:
    **`searchTool` accepts empty strings at the Zod validation layer**
    
    ```ts
    searchTool: z.string().optional(),
    ```
    
    An empty-string value passes Zod validation and is only normalised to `undefined` later in `resolveSearchTool`. This is harmless at runtime, but the schema will silently accept `memory.qmd.searchTool: ""` without surfacing a validation error to the user. Consider aligning schema and resolution:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/qmd-manager.ts
Line: 1503

Comment:
**Tool type widened to `string` without guard for built-in names**

The `tool` parameter type was changed from the union `"query" | "search" | "vector_search" | "deep_search"` to plain `string`. TypeScript no longer enforces the built-in tool constraint for the non-override path, so a caller passing `explicitToolOverride: false` with an arbitrary string would silently bypass `resolveQmdMcpTool` without the compiler catching it.

This is only a concern at the non-override call-site, since both callers always derive `tool` from `resolveQmdMcpTool` when the override is absent. Still worth noting for future maintenance: consider a discriminated union like `{ tool: string; explicitToolOverride: true } | { tool: "query" | "search" | "vector_search" | "deep_search"; explicitToolOverride: false }` to preserve compile-time safety on the default path.

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

---

This is a comment left during a code review.
Path: extensions/memory-core/src/memory/qmd-manager.test.ts
Line: 1052-1091

Comment:
**Multi-collection override path not covered by tests**

The test uses a single `paths` entry, so it exercises only the single-collection branch (`runQmdSearchViaMcporter` directly). The multi-collection branch via `runMcporterAcrossCollections` — which also has the new `explicitToolOverride` param threaded through it — is not tested.

While the code path looks correct from the diff alone, adding a second paths entry to this test (or a new dedicated test) would lock in the multi-collection flat-query behavior and protect against future regressions in `runMcporterAcrossCollections`.

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/config/zod-schema.ts
Line: 1368

Comment:
**`searchTool` accepts empty strings at the Zod validation layer**

```ts
searchTool: z.string().optional(),
```

An empty-string value passes Zod validation and is only normalised to `undefined` later in `resolveSearchTool`. This is harmless at runtime, but the schema will silently accept `memory.qmd.searchTool: ""` without surfacing a validation error to the user. Consider aligning schema and resolution:

```suggestion
    searchTool: z.string().min(1).optional(),
```

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

Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/qmd-search-..." | Re-trigger Greptile

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: 4c734186a0

ℹ️ 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".

@openclaw-barnacle openclaw-barnacle bot removed the docs Improvements or additions to documentation label Mar 30, 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: 1e3c76b64b

ℹ️ 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".

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: 6e85162b77

ℹ️ 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".

@vincentkoc vincentkoc merged commit da35718 into main Mar 30, 2026
32 of 37 checks passed
@vincentkoc vincentkoc deleted the fix/qmd-search-tool-override branch March 30, 2026 03:07
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 30, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High Arbitrary MCP tool invocation via configurable memory.qmd.searchTool override
2 🔵 Low Log/telemetry injection via unvalidated mcporter selector component (memory.qmd.searchTool)
Vulnerabilities

1. 🟠 Arbitrary MCP tool invocation via configurable memory.qmd.searchTool override

Property Value
Severity High
CWE CWE-284
Location extensions/memory-core/src/memory/qmd-manager.ts:1577-1619

Description

memory.qmd.searchTool allows a configuration-provided string to be used as the MCP tool name for mcporter calls, without an allowlist or capability check.

  • Input: memory.qmd.searchTool (resolved by resolveSearchTool via trim() only)
  • Sink: mcporter CLI invocation mcporter call <serverName>.<tool>
  • Issue: Any tool exposed by the configured MCP server can be invoked during a normal “search” flow when searchTool is set. If an attacker can influence configuration (e.g., untrusted workspace settings, tenant-controlled config, env var injection), they can trigger unexpected tools with side effects (filesystem access, network exfiltration, admin actions) under the host’s privileges.

Vulnerable code:

const selector = `${params.mcporter.serverName}.${effectiveTool}`;
...
result = await this.runMcporter([
  "call",
  selector,
  "--args",
  JSON.stringify(callArgs),
  ...
]);

This is a form of unsafe capability exposure: the search API is expected to perform retrieval, but can be redirected to arbitrary server tools.

Recommendation

Constrain memory.qmd.searchTool to a safe allowlist of read-only search tools, and reject/ignore any other value.

Options:

  1. Prefer an enum in schema/config (most robust):
// e.g. only allow built-ins plus a vetted custom list
const AllowedSearchTools = ["query", "search", "vector_search", "deep_search", "hybrid_search"] as const;

searchTool: z.enum(AllowedSearchTools).optional();
  1. Runtime allowlist check (if tools differ per server): fetch advertised tools from the MCP server and allow only those tagged/known as safe (read-only), otherwise fall back to the built-in mapping.

Also consider making searchTool admin-only (not user/workspace controllable) if this runs in a multi-tenant/SaaS context.


2. 🔵 Log/telemetry injection via unvalidated mcporter selector component (memory.qmd.searchTool)

Property Value
Severity Low
CWE CWE-117
Location extensions/memory-core/src/memory/qmd-manager.ts:1582-1560

Description

memory.qmd.searchTool is only trim()ed and then used to build the mcporter selector string. That selector is embedded into commandSummary (used in thrown error messages) without any neutralization.

  • Input: searchTool from config (resolveSearchTool) can contain control characters (e.g., \n, \r, tabs) or other log-breaking bytes.
  • Propagation: searchTool becomes part of selector = ${serverName}.${effectiveTool}``.
  • Sink: selector becomes part of commandSummary via spawnInvocation.argv.join(" ") and is included in error messages thrown by runCliCommand.

This enables log forging / log injection if a malicious repository/workspace can influence the config value, potentially:

  • creating fake log lines (newline injection),
  • obscuring real errors,
  • poisoning downstream telemetry/metrics that parse command summaries.

Vulnerable code:

const selector = `${params.mcporter.serverName}.${effectiveTool}`;
...
commandSummary: `${spawnInvocation.command} ${spawnInvocation.argv.join(" ")}`,

Recommendation

Validate and/or neutralize searchTool (and other selector components) before incorporating them into command summaries/logs.

Recommended defense-in-depth:

  1. Constrain allowed tool names (best): allow only a safe character set and a reasonable length.
  2. Escape control characters when building strings used for logs/errors.

Example (validation + escaping):

function validateToolName(tool: string): string {
  const trimmed = tool.trim();
  if (!/^[a-zA-Z0-9_\-]{1,64}$/.test(trimmed)) {
    throw new Error("Invalid searchTool; expected 1-64 chars of [a-zA-Z0-9_-]");
  }
  return trimmed;
}

function escapeForLog(s: string): string {
  return s.replace(/[\r\n\t]/g, (ch) => ({"\r":"\\r","\n":"\\n","\t":"\\t"}[ch]!));
}

const selector = `${serverName}.${validateToolName(tool)}`;
const commandSummary = `${cmd} ${argv.map(escapeForLog).join(" ")}`;

Analyzed PR: #57363 at commit c4989e2

Last updated on: 2026-03-30T09:57:42Z

alexjiang1 pushed a commit to alexjiang1/openclaw that referenced this pull request Mar 31, 2026
* fix(memory): add qmd mcporter search tool override

* fix(memory): tighten qmd search tool override guards

* chore(config): drop generated docs baselines from qmd pr

* fix(memory): keep explicit qmd query override on v2 args

* docs(changelog): normalize qmd search tool attribution

* fix(memory): reuse v1 qmd tool after query fallback
pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
* fix(memory): add qmd mcporter search tool override

* fix(memory): tighten qmd search tool override guards

* chore(config): drop generated docs baselines from qmd pr

* fix(memory): keep explicit qmd query override on v2 args

* docs(changelog): normalize qmd search tool attribution

* fix(memory): reuse v1 qmd tool after query fallback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: memory-core Extension: memory-core maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory.qmd.searchMode config validation rejects valid mcporter tool names

1 participant