Skip to content

openclaw: patch plugins.allow at install time + drop blocking auto-recall#124

Open
kaghni wants to merge 1 commit into
mainfrom
fix/openclaw-plugins-allow-121
Open

openclaw: patch plugins.allow at install time + drop blocking auto-recall#124
kaghni wants to merge 1 commit into
mainfrom
fix/openclaw-plugins-allow-121

Conversation

@kaghni
Copy link
Copy Markdown
Collaborator

@kaghni kaghni commented May 12, 2026

Fixes #121.

Why

Two related bugs surfaced in a user report (the original symptom: "openclaw plugin installed but auto-capture silently does nothing"):

  1. plugins.allow gating — the installer copies files into ~/.openclaw/extensions/hivemind/ but never patches ~/.openclaw/openclaw.json. If the gateway's plugins.allow is an explicit array missing "hivemind", the plugin never registers, agent_end never fires, and /hivemind_setup is unreachable from inside the agent (chicken-and-egg: the slash command needs the plugin to load first).
  2. Blocking auto-recallbefore_agent_start ran a Deeplake sessions-table search on every user turn. Sessions queries currently take 11.7s / 10.8s / 11.2s (three sequential probes) against a 10s SDK timeout, so every turn paid the full timeout before the agent could read the message. Other agents (claude-code/codex/cursor/hermes/pi) don't do this — they intercept the agent's Grep tool and let the agent decide when to search.

What changed

plugins.allow patch in the installer

  • src/cli/install-openclaw.ts now calls the same ensureHivemindAllowlisted helper that /hivemind_setup uses
  • Mirrors openclaw's own ensurePluginAllowlisted semantics (ext/openclaw/src/config/plugins-allowlist.ts:7): only patches plugins.allow when it's an explicit non-empty array. Absent / empty → left alone (default-allow stays default-allow; we don't flip the user into restrictive mode and break their other plugins)
  • Same logic for tools.alsoAllow
  • Atomic write (tmp + rename) with timestamped backup
  • Installer prints: which arrays were patched, restart hint, and a "capture starts on the next turn — no backfill" caveat so users aren't confused when older turns don't appear

Blocking auto-recall removed

  • openclaw/src/index.ts:before_agent_start no longer runs searchDeeplakeTables
  • Recall flows exclusively through the agent-callable hivemind_search / hivemind_read / hivemind_index tools (already registered), with the SKILL.md body in the system prompt directing the agent to use them
  • Side-effect win: the recursive <recalled-memories> capture bug (old recall context was getting captured as part of the user's prompt and stored) is gone too
  • The hook still handles the login nudge and post-auth welcome banner — those don't touch Deeplake
  • extractKeywords + RECALL_STOPWORDS removed as dead code

Tests

  • openclaw/tests/setup-command.test.ts+9 cases for plugins.allow patching: idempotency, default-allow semantics, both-arrays patched, detectAllowlistMissing extension
  • openclaw/tests/install-openclaw.test.ts (new, 12 cases) — drives the real installer against tmpdir fixtures: file copy, both patches, idempotency, default-allow preservation, malformed config tolerance, restart hint, no-duplicate
  • openclaw/tests/auto-recall.test.tsrewritten: asserts no Deeplake call on a normal turn, no <recalled-memories> injection, tools still registered, login-nudge path preserved

All 2214 hivemind tests pass.

Real-world E2E on a live openclaw gateway (2026.4.21)

  • Bundle installed → Hivemind plugin registered in gateway log
  • Idempotency: re-running installer logs allowlist already covers hivemind, no extra backup
  • Zero Auto-recall failed log lines after the restart with the new bundle (previous bundle emitted them on every turn that hit Deeplake slowness)
  • hivemind_search / hivemind_read / hivemind_index tools confirmed registered in before_prompt_build

Caveats

  • Capture (write path) still hits the same Deeplake timeout intermittently — Auto-capture failed: Query timeout after 10000ms lines appear in the gateway log. agent_end runs after the agent responds, so this doesn't block the user, but it does drop some turns from memory until Deeplake's sessions-table query latency improves. Separate issue, not addressed here.
  • The exact host state the original reporter hit (where adding "hivemind" to plugins.allow was needed even though tools were allowlisted) can't be reproduced on openclaw 2026.4.21 — that version has an "auto-promote plugins when their tools are in alsoAllow" feature that masks the gating. Older openclaw versions without that feature are the broken-state case the installer's plugins.allow patch is for.

Confidence

Confidence: high, because the unit tests cover every edge case of the new config-patching logic, the local fixture E2E drove the real installer end-to-end against multiple fixture shapes, and the live gateway confirms the bundle loads + the recall hook no longer fires.

Untested: openclaw versions older than 2026.4.21 (no host available; the installer's plugins.allow patch is the right fix for them but unverified end-to-end on those versions).

Test plan

  • CI green on this branch
  • Reviewer reads through openclaw/src/setup-config.ts semantics (esp. isPluginsAllowMissingHivemind only-patch-explicit-arrays rule)
  • Reviewer skims openclaw/tests/install-openclaw.test.ts and confirms test coverage matches the design intent in the description
  • After merge: cut a release and verify users on default-restrictive openclaw configs see the plugin register after a fresh install

Summary by CodeRabbit

  • New Features

    • OpenClaw installation now validates and patches configuration to ensure the hivemind plugin is properly allowlisted, with backup creation before changes.
    • Setup command displays which allowlist fields were updated during configuration patching.
  • Changes

    • Removed automatic memory recall that occurred on every turn; recall is now available only through direct tool invocation.
    • Improved installation and cleanup processes for extension management.

Review Change Stack

…call

Two related fixes for openclaw users (issue #121):

1. plugins.allow gating: the installer drops files into
   ~/.openclaw/extensions/hivemind/ but never touches the gateway's
   openclaw.json. If plugins.allow is an explicit array missing
   "hivemind", openclaw refuses to load the plugin — auto-capture
   silently does nothing because agent_end never fires, and
   /hivemind_setup is unreachable from inside the agent
   (chicken-and-egg: the slash command needs the plugin to load).

   src/cli/install-openclaw.ts now calls the same
   ensureHivemindAllowlisted helper that /hivemind_setup uses, so the
   installer patches plugins.allow + tools.alsoAllow at install time
   (when each is an explicit array). Mirrors openclaw's own
   ensurePluginAllowlisted semantics: never creates the array if
   absent, never touches it if empty — default-allow stays
   default-allow so we don't flip the user into restrictive mode and
   break other plugins.

   Adds a restart hint + "capture starts on the next turn — no
   backfill" caveat to installer output so users aren't confused
   when older turns don't appear.

2. blocking auto-recall removed: before_agent_start used to run a
   searchDeeplakeTables query across memory + sessions on every user
   turn. With Deeplake's sessions-table latency varying from 200ms
   to 10s+ (measured 11.7s/10.8s/11.2s across three sequential
   probes), every openclaw turn was paying up to 10s of blocking
   recall before the agent could even read the message. Other
   agents (claude-code/codex/cursor/hermes/pi) don't do this — they
   intercept the agent's own Grep tool calls and let the agent
   decide when to search.

   openclaw now matches that pattern: recall flows exclusively
   through the registered hivemind_search/_read/_index tools, with
   the SKILL.md body in the system prompt directing the agent to
   call them. Side effect: the recursive <recalled-memories>
   capture (the old recall context was getting captured as part of
   the user's prompt and stored) is gone too.

   The before_agent_start hook still handles the login nudge and
   the post-auth welcome banner — those don't touch Deeplake.

Tests:
 - openclaw/tests/setup-command.test.ts: 9 new cases covering
   plugins.allow patching, idempotency, default-allow semantics,
   both-arrays patched, detectAllowlistMissing extension
 - openclaw/tests/install-openclaw.test.ts (new): 12 cases driving
   the real installer against tmpdir fixtures — file copy, both
   patches, idempotency, default-allow preservation, malformed
   config tolerance, restart hint, no-duplicate
 - openclaw/tests/auto-recall.test.ts: rewritten — asserts no
   Deeplake call on a normal turn, no <recalled-memories>
   injection, hivemind_search/_read/_index tools still registered,
   login nudge path preserved

Real-world E2E on a live openclaw gateway:
 - Bundle installed → "Hivemind plugin registered" in gateway log
 - Idempotency: re-running installer logs "allowlist already covers
   hivemind", no extra backup
 - Zero "Auto-recall failed" log lines after the restart with the
   new bundle (previous bundle emitted them on every turn that hit
   Deeplake slowness)

Caveat: capture (agent_end → INSERT into sessions) still hits the
same Deeplake-side timeout intermittently. That's a separate
performance issue and runs after the agent responds, so it doesn't
block the user — but it does drop some turns from memory until
Deeplake's sessions queries are faster.

Confidence: high, because the unit tests cover every edge case of
the new config-patching logic, the local fixture E2E drove the real
installer end-to-end against multiple fixture shapes, and the live
gateway confirms the bundle loads + the recall hook no longer fires.

Untested: openclaw versions older than 2026.4.21 (where the
auto-promote-via-tools.alsoAllow heuristic doesn't exist) — the
manual fix from the original reporter implies they would have been
the broken-state case; the installer's plugins.allow patch is the
right fix for them, but we couldn't reproduce that exact host state
on the 2026.4.21 gateway available for testing.

Fixes #121
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

This PR implements issue #121 fix (OpenClaw installer now patches plugins.allow to register the hivemind plugin) and removes expensive proactive per-turn auto-recall from the plugin hook. Supporting infrastructure refactors fs/path imports to use node: protocol variants throughout the CLI bundle.

Changes

OpenClaw allowlist patching and plugin hook simplification

Layer / File(s) Summary
Allowlist contract and detection helpers
openclaw/src/setup-config.ts
Adds isPluginsAllowMissingHivemind() to detect explicit allowlists missing hivemind, introduces AllowlistDelta type describing which allowlists were patched, updates SetupResult to include delta. Refactors ensureHivemindAllowlisted() to independently detect and patch both plugins.allow and tools.alsoAllow, and expands detectAllowlistMissing() to check both arrays.
OpenClaw installer patching implementation
bundle/cli.js, src/cli/install-openclaw.ts
OpenClaw install flow validates and patches openclaw.json to ensure hivemind appears in explicit-allowlist plugins.allow (if present as array) and tools.alsoAllow. Writes timestamped backup of original config, uses atomic temp-file + rename, and logs patch status. Installer entry point imports and invokes ensureHivemindAllowlisted() after copying files to disk.
Plugin hook and auto-recall removal
openclaw/src/index.ts
before_agent_start hook no longer issues proactive Deeplake queries on every turn. Removed keyword extraction logic and multi-table UNION ALL memory search. Hook now handles only login URL nudging (unauthenticated path) and one-time welcome banner, with explicit logger.error() for exceptions. hivemind_setup command output now builds and displays touched allowlist array showing which fields were patched, plus updated messaging about capture timing and backup path.
Comprehensive installer and setup command tests
openclaw/tests/install-openclaw.test.ts, openclaw/tests/setup-command.test.ts
New test suite mocks homedir() and openclaw.json fixtures, verifying bundle copy, config patching (both allowlists and only when needed), idempotency, timestamped backup creation, and stdout guidance. Extended setup-command tests cover plugins.allow gating: patching explicit arrays missing hivemind, preserving default-allow when absent, handling empty arrays, combined patching, and detectAllowlistMissing() edge cases.
Auto-recall test refactoring
openclaw/tests/auto-recall.test.ts
Auto-recall tests refactored to document and verify post-removal behavior: before_agent_start no longer calls Deeplake or injects <recalled-memories> tags, continues short-circuit on empty/short prompts, registers hivemind_search/read/index tools, and safely handles unauthenticated/missing-credential paths without throwing. Test helper extracted buildMockApi() for clarity.

Node imports and path infrastructure refactoring

Layer / File(s) Summary
Index marker and cursor installation paths
bundle/cli.js
Index marker store updates node:fs/node:path imports and revises workspace/org/table keying helpers. Cursor installer updates imports, path constants, hook command construction, bundle copy checks, and uninstall operations.
Hermes installation and configuration wiring
bundle/cli.js
Hermes installer updates fs/path imports, introduces path constants for home/hivemind/bundle/config directories, refactors config read/write (uses updated existsSync for fallback), install flow validation and bundle copy, and expands uninstall to remove skill dir, hivemind dir, and gateway config with logging.
MCP shared server and Pi agent installation
bundle/cli.js
MCP shared-server installer introduces path constants for ~/.hivemind/mcp layout and wires ensureMcpServerInstalled flow. Pi installer updates imports, declares extension/worker/version-path constants, refactors install/uninstall to remove legacy skill dir, rewrite AGENTS.md hivemind block, copy extension and optional worker bundles, and prune orphaned blocks.
Embeddings support and auth credential/config wiring
bundle/cli.js
Embeddings shared-deps updates linked-state detection, manifest installation, and daemon bundle copy. Embeddings status adds daemon/shared-deps state reporting and optional shared-dir pruning. Auth credential handling rewrites ~/.deeplake/credentials.json path derivation and load/save helpers. Config loader and debug logger update credential file reading and LOG path construction.
Skillify scope-config, manifest, and backup wiring
bundle/cli.js
Skillify top-level imports, scope-config paths and loaders, and pull/skill-writer module imports updated to use node: variants. Manifest loading preserves same validation/fallback. saveManifest refactored to atomic temp-file + rename with permissions. Pull flow now backs up existing SKILL.md to SKILL.md.bak before overwriting.
Skillify destination resolution and root detection
bundle/cli.js
Pull destination and fanOut symlink helpers updated for global vs project root resolution. Unpull root resolution, agent-roots detection, backfill canonical-path check, and orphan detection refactored to use updated join/exists import aliases. Version read and manifest operations use new readFileSync and existsSync imports.
Skillify status display and project promotion
bundle/cli.js
showStatus state directory resolution and tracked-project existence checks refactored. Tracked project state file reads updated to new readFileSync import. setInstall and promoteSkill destination path computation and existence checks refactored. Pull and unpull dest string computation updated to use join/homedir helpers.
Update and version detection helpers
bundle/cli.js
Update and version-check module imports updated to use node: variants. detectInstallKind() package.json reading and .git existence check refactored to use new import aliases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A plugin once lost in the config so deep,
Now patched with allowlists both promises to keep,
Auto-recall gone—let the tools take the lead,
With node: imports fresh, we're fast-freed indeed! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: patching plugins.allow at install time and removing the blocking auto-recall behavior.
Description check ✅ Passed The PR description comprehensively covers the motivation, detailed changes, test coverage, and real-world E2E validation; it aligns well with the template structure and includes all critical context.
Linked Issues check ✅ Passed All primary objectives from issue #121 are implemented: installer now patches plugins.allow and tools.alsoAllow with safe semantics, blocking auto-recall is removed, comprehensive tests added, and real-world E2E validation confirms the fix.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing issue #121: installer patching, auto-recall removal, related test additions, and setup-config extensions. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/openclaw-plugins-allow-121

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 100.00% (🎯 90%) 29 / 29
🟢 Statements 100.00% (🎯 90%) 34 / 34
🟢 Functions 100.00% (🎯 90%) 2 / 2
🟢 Branches 94.44% (🎯 90%) 17 / 18
File Coverage — 1 file changed
File Stmts Branches Functions Lines
src/cli/install-openclaw.ts 🟢 100.0% 🟢 94.4% 🟢 100.0% 🟢 100.0%

Generated for commit 8fb1fa1.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openclaw/src/index.ts (1)

1009-1032: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

before_agent_start should no longer be gated by autoRecall (Line 1009).

Now that proactive recall was removed, this gate unintentionally disables the login nudge + post-auth welcome paths for users who already have autoRecall: false.

Suggested fix
-    if (config.autoRecall !== false) {
-      hook("before_agent_start", async (event: { prompt?: string }) => {
+    hook("before_agent_start", async (event: { prompt?: string }) => {
         if (!event.prompt || event.prompt.length < 5) return;
         try {
           const dl = await getApi();
@@
         } catch (err) {
           logger.error(`before_agent_start failed: ${err instanceof Error ? err.message : String(err)}`);
         }
       });
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openclaw/src/index.ts` around lines 1009 - 1032, The before_agent_start hook
is incorrectly wrapped by the config.autoRecall check which prevents the login
nudge and post-auth welcome for users with autoRecall: false; remove this gating
so the hook registration always runs (i.e., register the hook unconditionally
instead of inside the if (config.autoRecall !== false) block) while keeping the
existing logic that checks getApi(), authUrl, and justAuthenticated handling
inside the hook callback (referencing the before_agent_start hook, getApi(),
justAuthenticated, loadCredentials(), and authUrl).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bundle/cli.js`:
- Around line 456-458: The current coercion of tools.alsoAllow to [] forces
patching even when the field is absent; change the logic so you preserve
"missing" vs "explicit empty" by not defaulting to [], e.g. keep alsoAllowRaw as
undefined when tools.alsoAllow is not an array and compute
toolsAlsoAllowNeedsPatch only when an actual array was provided (e.g.
toolsAlsoAllowNeedsPatch = Array.isArray(alsoAllowRaw) ?
!isAllowlistCoveringHivemind(alsoAllowRaw) : false); apply the same pattern to
the analogous block around lines 470-474 so patching only occurs for an
explicitly supplied non-empty allowlist.
- Line 3313: The command string built for Hermes is not quoting the executable
path which will break on paths with spaces; update the assignment that creates
command (currently using `node ${join8(BUNDLE_DIR, bundleFile)}`) to wrap the
resolved path in quotes (e.g. `node "..."`) so the `join8(BUNDLE_DIR,
bundleFile)` result is quoted, ensuring paths with spaces are handled correctly;
locate the `command` property assignment in bundle/cli.js and modify it to
include the quotes around the template substitution.
- Around line 516-529: The installer currently ignores result.status === "error"
from ensureHivemindAllowlisted(), so surface failures by handling that branch:
when ensureHivemindAllowlisted() returns result.status === "error", log a clear
error including result.error, result.configPath and result.backupPath (like the
other branches), and ensure the installer returns non-zero/aborts or otherwise
indicates failure (same behavior as on other fatal install errors). Update the
code path around ensureHivemindAllowlisted() to check result.status for "error"
and emit a descriptive processLogger/log error message using those result
fields.
- Around line 453-456: The code dereferences parsed.plugins and parsed.tools
without validating parsed's shape; update the top of the parsing logic to verify
parsed is a non-null object and throw a clear, structured error if not (e.g.,
"Invalid config: expected JSON object"); then guard access to parsed.plugins and
parsed.tools by treating them as objects (e.g., if parsed.plugins is not an
object default to {} before reading plugins.allow) and ensure tools.alsoAllow is
read only after confirming tools is an object (or default to {}), keeping the
existing assignments to pluginsAllowRaw and alsoAllowRaw but using these
validated/defaulted objects.

In `@openclaw/src/index.ts`:
- Line 663: The manual-fix string returned when updating the allowlist (the
returned object that uses result.error and result.configPath) is too
prescriptive; change it to recommend adding "hivemind" to tools' alsoAllow and
only recommend adding it to plugins.allow if an explicit plugin allowlist
already exists (i.e., plugins.allow is present and non-empty). Update the
message built around result.error/result.configPath to conditionally instruct:
if plugins.allow exists suggest adding to both, otherwise suggest adding to
tools.alsoAllow and warn not to create a plugins.allow (to avoid switching to
restrictive explicit-allow mode).

In `@openclaw/tests/auto-recall.test.ts`:
- Line 157: The test calls expect(before({ prompt: "anything that triggered the
path before" })) but uses the wrong matcher; replace .resolves.not.toThrow()
with .resolves.toBeUndefined() so the promise resolution is asserted correctly
(i.e., expect(before(...)).resolves.toBeUndefined()).

---

Outside diff comments:
In `@openclaw/src/index.ts`:
- Around line 1009-1032: The before_agent_start hook is incorrectly wrapped by
the config.autoRecall check which prevents the login nudge and post-auth welcome
for users with autoRecall: false; remove this gating so the hook registration
always runs (i.e., register the hook unconditionally instead of inside the if
(config.autoRecall !== false) block) while keeping the existing logic that
checks getApi(), authUrl, and justAuthenticated handling inside the hook
callback (referencing the before_agent_start hook, getApi(), justAuthenticated,
loadCredentials(), and authUrl).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 609726ec-8d39-46ec-ae3f-586579147896

📥 Commits

Reviewing files that changed from the base of the PR and between 070a5fc and 429b67a.

📒 Files selected for processing (7)
  • bundle/cli.js
  • openclaw/src/index.ts
  • openclaw/src/setup-config.ts
  • openclaw/tests/auto-recall.test.ts
  • openclaw/tests/install-openclaw.test.ts
  • openclaw/tests/setup-command.test.ts
  • src/cli/install-openclaw.ts

Comment thread bundle/cli.js
Comment on lines +453 to +456
const plugins = parsed.plugins ?? {};
const pluginsAllowRaw = plugins.allow;
const tools = parsed.tools ?? {};
const alsoAllowRaw = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate parsed config shape before property access.

JSON.parse can return null, and parsed.plugins/parsed.tools then throws at runtime. Return a structured error before dereferencing.

Suggested fix
   let parsed;
   try {
     const raw = readFileSync4(configPath, "utf-8");
     parsed = JSON.parse(raw);
   } catch (e) {
     return { status: "error", configPath, error: `could not read/parse config: ${e instanceof Error ? e.message : String(e)}` };
   }
+  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+    return { status: "error", configPath, error: "config must be a JSON object" };
+  }
   const plugins = parsed.plugins ?? {};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundle/cli.js` around lines 453 - 456, The code dereferences parsed.plugins
and parsed.tools without validating parsed's shape; update the top of the
parsing logic to verify parsed is a non-null object and throw a clear,
structured error if not (e.g., "Invalid config: expected JSON object"); then
guard access to parsed.plugins and parsed.tools by treating them as objects
(e.g., if parsed.plugins is not an object default to {} before reading
plugins.allow) and ensure tools.alsoAllow is read only after confirming tools is
an object (or default to {}), keeping the existing assignments to
pluginsAllowRaw and alsoAllowRaw but using these validated/defaulted objects.

Comment thread bundle/cli.js
Comment on lines +456 to +458
const alsoAllowRaw = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
const pluginsAllowNeedsPatch = isPluginsAllowMissingHivemind(pluginsAllowRaw);
const toolsAlsoAllowNeedsPatch = !isAllowlistCoveringHivemind(alsoAllowRaw);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

tools.alsoAllow patching currently breaks the “explicit non-empty only” contract.

At Line 456, absent/non-array values are coerced to [], and Line 458 then always flags patching. This creates tools.alsoAllow even when missing/empty, which changes default-allow behavior.

Suggested fix
-  const alsoAllowRaw = Array.isArray(tools.alsoAllow) ? tools.alsoAllow : [];
+  const alsoAllowRaw = tools.alsoAllow;
   const pluginsAllowNeedsPatch = isPluginsAllowMissingHivemind(pluginsAllowRaw);
-  const toolsAlsoAllowNeedsPatch = !isAllowlistCoveringHivemind(alsoAllowRaw);
+  const toolsAlsoAllowNeedsPatch =
+    Array.isArray(alsoAllowRaw) &&
+    alsoAllowRaw.length > 0 &&
+    !isAllowlistCoveringHivemind(alsoAllowRaw);

   ...
   if (toolsAlsoAllowNeedsPatch) {
     updated.tools = {
       ...tools,
       alsoAllow: [...alsoAllowRaw, "hivemind"]
     };
   }

Also applies to: 470-474

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundle/cli.js` around lines 456 - 458, The current coercion of
tools.alsoAllow to [] forces patching even when the field is absent; change the
logic so you preserve "missing" vs "explicit empty" by not defaulting to [],
e.g. keep alsoAllowRaw as undefined when tools.alsoAllow is not an array and
compute toolsAlsoAllowNeedsPatch only when an actual array was provided (e.g.
toolsAlsoAllowNeedsPatch = Array.isArray(alsoAllowRaw) ?
!isAllowlistCoveringHivemind(alsoAllowRaw) : false); apply the same pattern to
the analogous block around lines 470-474 so patching only occurs for an
explicitly supplied non-empty allowlist.

Comment thread bundle/cli.js
Comment on lines +516 to +529
const result = ensureHivemindAllowlisted();
if (result.status === "added") {
const touched = [];
if (result.delta.pluginsAllow)
touched.push("plugins.allow");
if (result.delta.toolsAlsoAllow)
touched.push("tools.alsoAllow");
log(` OpenClaw patched ${touched.join(" + ")} in ${result.configPath}`);
log(` OpenClaw backup: ${result.backupPath}`);
log(` OpenClaw restart the gateway to activate: systemctl --user restart openclaw-gateway.service`);
log(` OpenClaw capture starts on the NEXT turn \u2014 earlier turns are NOT backfilled`);
} else if (result.status === "already-set") {
log(` OpenClaw allowlist already covers hivemind in ${result.configPath}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle and surface allowlist patch failures in installer output.

installOpenclaw() ignores result.status === "error", so failed patch attempts are silent and users won’t know why the plugin still doesn’t load.

Suggested fix
   } else if (result.status === "already-set") {
     log(`  OpenClaw       allowlist already covers hivemind in ${result.configPath}`);
+  } else if (result.status === "error") {
+    warn(`  OpenClaw       could not patch allowlist in ${result.configPath}: ${result.error}`);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = ensureHivemindAllowlisted();
if (result.status === "added") {
const touched = [];
if (result.delta.pluginsAllow)
touched.push("plugins.allow");
if (result.delta.toolsAlsoAllow)
touched.push("tools.alsoAllow");
log(` OpenClaw patched ${touched.join(" + ")} in ${result.configPath}`);
log(` OpenClaw backup: ${result.backupPath}`);
log(` OpenClaw restart the gateway to activate: systemctl --user restart openclaw-gateway.service`);
log(` OpenClaw capture starts on the NEXT turn \u2014 earlier turns are NOT backfilled`);
} else if (result.status === "already-set") {
log(` OpenClaw allowlist already covers hivemind in ${result.configPath}`);
}
const result = ensureHivemindAllowlisted();
if (result.status === "added") {
const touched = [];
if (result.delta.pluginsAllow)
touched.push("plugins.allow");
if (result.delta.toolsAlsoAllow)
touched.push("tools.alsoAllow");
log(` OpenClaw patched ${touched.join(" + ")} in ${result.configPath}`);
log(` OpenClaw backup: ${result.backupPath}`);
log(` OpenClaw restart the gateway to activate: systemctl --user restart openclaw-gateway.service`);
log(` OpenClaw capture starts on the NEXT turn \u2014 earlier turns are NOT backfilled`);
} else if (result.status === "already-set") {
log(` OpenClaw allowlist already covers hivemind in ${result.configPath}`);
} else if (result.status === "error") {
warn(` OpenClaw could not patch allowlist in ${result.configPath}: ${result.error}`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundle/cli.js` around lines 516 - 529, The installer currently ignores
result.status === "error" from ensureHivemindAllowlisted(), so surface failures
by handling that branch: when ensureHivemindAllowlisted() returns result.status
=== "error", log a clear error including result.error, result.configPath and
result.backupPath (like the other branches), and ensure the installer returns
non-zero/aborts or otherwise indicates failure (same behavior as on other fatal
install errors). Update the code path around ensureHivemindAllowlisted() to
check result.status for "error" and emit a descriptive processLogger/log error
message using those result fields.

Comment thread bundle/cli.js
function buildHookEntry(bundleFile, timeout, matcher) {
const entry = {
command: `node ${join7(BUNDLE_DIR, bundleFile)}`,
command: `node ${join8(BUNDLE_DIR, bundleFile)}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote Hermes hook command path.

The command path is unquoted; homes/project paths with spaces will break execution.

Suggested fix
-    command: `node ${join8(BUNDLE_DIR, bundleFile)}`,
+    command: `node "${join8(BUNDLE_DIR, bundleFile)}"`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
command: `node ${join8(BUNDLE_DIR, bundleFile)}`,
command: `node "${join8(BUNDLE_DIR, bundleFile)}"`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundle/cli.js` at line 3313, The command string built for Hermes is not
quoting the executable path which will break on paths with spaces; update the
assignment that creates command (currently using `node ${join8(BUNDLE_DIR,
bundleFile)}`) to wrap the resolved path in quotes (e.g. `node "..."`) so the
`join8(BUNDLE_DIR, bundleFile)` result is quoted, ensuring paths with spaces are
handled correctly; locate the `command` property assignment in bundle/cli.js and
modify it to include the quotes around the template substitution.

Comment thread openclaw/src/index.ts
return { text: `✅ Added:\n • ${touched.join("\n • ")}\n\nOpenclaw will detect the config change and restart. On the next turn, the agent will have access to hivemind_search, hivemind_read, and hivemind_index. **Capture starts on the next turn — earlier turns are NOT backfilled.**\n\nBackup of previous config: ${result.backupPath}${skillifyHint}` };
}
return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to the "alsoAllow" array under "tools".` };
return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to BOTH the "allow" array under "plugins" AND the "alsoAllow" array under "tools".` };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Manual-fix text is overly strict and can break default-allow setups (Line 663).

The current message says to add "hivemind" to BOTH arrays unconditionally; for absent/empty plugins.allow, that can accidentally switch users into restrictive explicit-allowlist mode.

Suggested wording adjustment
-          return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to BOTH the "allow" array under "plugins" AND the "alsoAllow" array under "tools".` };
+          return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to "tools.alsoAllow". If "plugins.allow" already exists as a non-empty explicit allowlist, add "hivemind" there too.` };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to BOTH the "allow" array under "plugins" AND the "alsoAllow" array under "tools".` };
return { text: `⚠️ Could not update allowlist: ${result.error}\n\nManual fix: open ${result.configPath} and add "hivemind" to "tools.alsoAllow". If "plugins.allow" already exists as a non-empty explicit allowlist, add "hivemind" there too.` };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openclaw/src/index.ts` at line 663, The manual-fix string returned when
updating the allowlist (the returned object that uses result.error and
result.configPath) is too prescriptive; change it to recommend adding "hivemind"
to tools' alsoAllow and only recommend adding it to plugins.allow if an explicit
plugin allowlist already exists (i.e., plugins.allow is present and non-empty).
Update the message built around result.error/result.configPath to conditionally
instruct: if plugins.allow exists suggest adding to both, otherwise suggest
adding to tools.alsoAllow and warn not to create a plugins.allow (to avoid
switching to restrictive explicit-allow mode).

// assertion here is conservative: the hook must NOT call query, and
// must NOT throw, when no creds exist.
const before = hooks.get("before_agent_start")!;
await expect(before({ prompt: "anything that triggered the path before" })).resolves.not.toThrow();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Vitest v4, is await expect(promise).resolves.not.toThrow()valid when the promise resolves toundefined (not a function)? What matcher is recommended instead?

💡 Result:

No. In Vitest v4, await expect(promise).resolves.not.toThrow() is not a valid pattern for the case where the promise resolves to undefined (i.e., “not a function”). The root issue is that Vitest’s .resolves unwrapping expects to apply the matcher to the resolved value, but toThrow-style matchers are for asserting that a function throws; they don’t apply meaningfully to non-functions like undefined in the way your expression suggests. Related reports show .resolves is not compatible with Jest’s “function form” behavior for toThrow (the .resolves path doesn’t support that compatibility, unlike .rejects). [1] Recommended matcher instead: assert the resolved value directly, e.g. await expect(promise).resolves.toBeUndefined() (or toEqual(undefined)), rather than using not.toThrow. Vitest’s docs describe .resolves as unwrapping the promise and then applying a normal matcher to the resolved value; if the promise rejects the assertion fails. [2][3] Example: - await expect(promise).resolves.toBeUndefined() [2][3]

Citations:


Use resolves.toBeUndefined() instead of resolves.not.toThrow() on line 157.

The .toThrow() matcher applies only to functions, not to resolved promise values. Since before() resolves to undefined, use a concrete value assertion instead.

Fix
-    await expect(before({ prompt: "anything that triggered the path before" })).resolves.not.toThrow();
+    await expect(before({ prompt: "anything that triggered the path before" })).resolves.toBeUndefined();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await expect(before({ prompt: "anything that triggered the path before" })).resolves.not.toThrow();
await expect(before({ prompt: "anything that triggered the path before" })).resolves.toBeUndefined();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openclaw/tests/auto-recall.test.ts` at line 157, The test calls
expect(before({ prompt: "anything that triggered the path before" })) but uses
the wrong matcher; replace .resolves.not.toThrow() with
.resolves.toBeUndefined() so the promise resolution is asserted correctly (i.e.,
expect(before(...)).resolves.toBeUndefined()).

@kaghni
Copy link
Copy Markdown
Collaborator Author

kaghni commented May 12, 2026

User-flow E2E: drove the real hivemind claw install CLI against the reporter's exact broken-state config

Reviewer asked for proof that the user-facing flow (not just the unit-test-mocked one) actually patches the allowlists. Here's the transcript.

Fixture: reporter's broken state, on a tmp $HOME. No plugins.entries.hivemind (so openclaw can't auto-promote), neither array contains "hivemind":

{
  "meta": { "lastTouchedVersion": "2026.4.14" },
  "agents": { "defaults": { "model": "anthropic/claude-haiku-4-5-20251001" } },
  "channels": { "telegram": { "enabled": true } },
  "plugins": {
    "allow": ["telegram", "anthropic", "openai", "duckduckgo"],
    "entries": {
      "telegram":  { "enabled": true },
      "anthropic": { "enabled": true }
    }
  },
  "tools": {
    "profile":   "coding",
    "alsoAllow": ["memory_store"]
  }
}

Before:

plugins.allow contains 'hivemind'? False
tools.alsoAllow contains 'hivemind'? False
plugins.entries.hivemind present? False

Run the real CLI users invoke:

$ HOME=/tmp/hivemind-claw-install-flow-Y4fLiK node bundle/cli.js claw install
  OpenClaw       installed -> /tmp/hivemind-claw-install-flow-Y4fLiK/.openclaw/extensions/hivemind
  OpenClaw       patched plugins.allow + tools.alsoAllow in /tmp/hivemind-claw-install-flow-Y4fLiK/.openclaw/openclaw.json
  OpenClaw       backup: /tmp/hivemind-claw-install-flow-Y4fLiK/.openclaw/openclaw.json.bak-hivemind-1778620171180
  OpenClaw       restart the gateway to activate: systemctl --user restart openclaw-gateway.service
  OpenClaw       capture starts on the NEXT turn — earlier turns are NOT backfilled

After:

{
  "meta":     { "lastTouchedVersion": "2026.4.14" },
  "agents":   { "defaults": { "model": "anthropic/claude-haiku-4-5-20251001" } },
  "channels": { "telegram": { "enabled": true } },
  "plugins":  {
    "allow":   ["telegram", "anthropic", "openai", "duckduckgo", "hivemind"],
    "entries": { "telegram": { "enabled": true }, "anthropic": { "enabled": true } }
  },
  "tools":    {
    "profile":   "coding",
    "alsoAllow": ["memory_store", "hivemind"]
  }
}

Assertions:

  • plugins.allow contains "hivemind"
  • tools.alsoAllow contains "hivemind"
  • Unrelated keys preserved (meta, agents.defaults.model, channels.telegram.enabled, plugins.entries.{telegram,anthropic}) ✅
  • Backup file written: openclaw.json.bak-hivemind-1778620171180
  • Plugin files on disk at .openclaw/extensions/hivemind/: .hivemind_version, dist/, openclaw.plugin.json, package.json, skills/

Idempotency — re-running on the same fixture:

$ HOME=/tmp/... node bundle/cli.js claw install
  OpenClaw       installed -> .openclaw/extensions/hivemind
  OpenClaw       allowlist already covers hivemind in .openclaw/openclaw.json

Backup file count after re-run: 1 (unchanged — no spurious second backup). ✅

Before this fix, the same hivemind claw install against the same starting config would have left openclaw.json untouched: files would land on disk under extensions/hivemind/, but the gateway would refuse to load the plugin (plugins.entries.hivemind: plugin disabled (not in allowlist) but config is present per openclaw's own loader warning), so auto-capture would silently no-op and /hivemind_setup would be unreachable from inside the agent. That's exactly the symptom in #121.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenClaw installer doesn't patch plugins.allow → plugin loads to disk but never registers; auto-capture silently no-ops

1 participant