Skip to content

fix(memory-wiki): route bridge CLI through gateway#72615

Merged
vincentkoc merged 1 commit into
mainfrom
clownfish/ghcrawl-156579-autonomous-smoke
Apr 28, 2026
Merged

fix(memory-wiki): route bridge CLI through gateway#72615
vincentkoc merged 1 commit into
mainfrom
clownfish/ghcrawl-156579-autonomous-smoke

Conversation

@vincentkoc

Copy link
Copy Markdown
Member

Summary

  • Route active Memory Wiki bridge-mode wiki status, wiki doctor, and wiki bridge import CLI operations through the Gateway RPC path so they share the runtime memory plugin context.
  • Keep local/offline behavior for configurations where bridge import is disabled or memory artifact reads are not requested.
  • Add focused CLI regression coverage and update docs/changelog.

Credit

Based on the Gateway-routing direction from #67208 by @moorsecopers99 and the scoped maintainer draft #71479 by @vincentkoc. Related CLI-metadata capability work in #70185 by @prasad-yashdeep informed the issue analysis.

Validation

  • pnpm -s vitest run extensions/memory-wiki/src/cli.test.ts
  • pnpm check:changed

Fixes #65722
Fixes #65976
Fixes #66082
Fixes #67979
Fixes #68371
Fixes #68828
Fixes #69019
Fixes #70181
Fixes #70242
Fixes #70842

ProjectClownfish replacement details:

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation extensions: memory-wiki size: M maintainer Maintainer-authored PR labels Apr 27, 2026
@vincentkoc vincentkoc added the clawsweeper Tracked by ClawSweeper automation label Apr 27, 2026
@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Routes wiki status, wiki doctor, and wiki bridge import through Gateway RPC when bridge mode is active and bridge.readMemoryArtifacts is enabled, while preserving the existing local/offline path for all other configurations. Tests and docs are included.

Confidence Score: 4/5

Safe to merge; one minor concern about redundant json forwarding to the Gateway that is unlikely to cause problems in practice.

All changes are well-scoped, have focused regression tests, and the offline fallback path is preserved. The only finding is a P2: callWikiGateway passes params.json to the Gateway even though the CLI applies its own JSON/render formatting locally, which could break rendering if the Gateway ever returns pre-serialized text for json: true. No P0/P1 issues found.

extensions/memory-wiki/src/cli.ts — specifically the callWikiGateway helper and whether json forwarding is intentional and safe for the Gateway implementation.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-wiki/src/cli.ts
Line: 162-168

Comment:
**`json` flag forwarded to Gateway but also applied locally**

`callWikiGateway` passes `params.json` to `callGatewayFromCli`, then `runWikiStatus`/`runWikiDoctor` still apply their own JSON-vs-render logic to whatever the Gateway returns. The CLI always formats the result locally (lines 287–290, 313–315), so the Gateway never needs to know the `json` preference. If `callGatewayFromCli` ever honours the flag and returns a serialized string instead of a structured object when `json: true`, the subsequent `renderMemoryWikiStatus(status)` call (when `--json` is absent) would receive a string and produce incorrect output. The new tests only cover the `json: true` gateway path, leaving the `json: undefined` / `json: false` gateway path untested.

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

Reviews (1): Last reviewed commit: "fix(memory-wiki): route bridge CLI throu..." | Re-trigger Greptile

Comment thread extensions/memory-wiki/src/cli.ts Outdated
@prasad-yashdeep

Copy link
Copy Markdown

Is there anything else required in this PR to get it merged. Happy to help.

@vincentkoc
vincentkoc force-pushed the clownfish/ghcrawl-156579-autonomous-smoke branch 4 times, most recently from 4c82f9c to a1d1ce3 Compare April 28, 2026 09:46
@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: keeping this open for maintainer follow-up; there is still a little grit to resolve.

Keep open. This PR is protected by maintainer signals, and current main still has the Memory Wiki CLI status, doctor, and bridge import paths running local sync instead of the Gateway RPC route proposed here. The PR remains an active implementation candidate for the linked bridge/runtime mismatch reports, carrying forward #67208 and #71479 with related context from #70185.

Best possible solution:

Keep this PR open for maintainer review. The best path is to review or land the Gateway-routing implementation after addressing the remaining security/review discussion and validating the focused Memory Wiki CLI tests plus the changed gate; current main does not yet solve the intended CLI/runtime context mismatch.

What I checked:

  • Protected PR state: Live GitHub API data shows the PR is open, unmerged, authored by a MEMBER, and labeled maintainer; cleanup policy says maintainer-authored or maintainer-labeled items must stay open for explicit maintainer handling. (705b50b34cdd)
  • Current main still uses local CLI sync: On current main, runWikiStatus, runWikiDoctor, and runWikiBridgeImport call syncMemoryWikiImportedSources directly; there is no callGatewayFromCli import or routing predicate in the current Memory Wiki CLI implementation. (extensions/memory-wiki/src/cli.ts:252, 5915489631d7)
  • Gateway target already exists: The Memory Wiki plugin already registers Gateway methods for wiki.status, wiki.doctor, and wiki.bridge.import, which are the runtime-context methods the PR wants the CLI to call. (extensions/memory-wiki/src/gateway.ts:103, 5915489631d7)
  • SDK route is available: openclaw/plugin-sdk/gateway-runtime exports callGatewayFromCli, so the proposed implementation uses an existing plugin-facing SDK helper rather than a core-internal import. (src/plugin-sdk/gateway-runtime.ts:4, 5915489631d7)
  • PR diff scope and security pass: The live PR diff changes only CHANGELOG.md, docs/cli/wiki.md, docs/plugins/memory-wiki.md, extensions/memory-wiki/src/cli.ts, and extensions/memory-wiki/src/cli.test.ts; it does not touch workflows, lockfiles, package metadata, install/build/release scripts, dependency sources, or publishing surfaces. The diff adds Gateway routing and focused tests/docs, while bot discussion still flags security-sensitive review points around gateway-exposed imports and response handling. (extensions/memory-wiki/src/cli.ts, 705b50b34cdd)
  • Existing import write path is security-relevant: The bridge import path ultimately writes imported source pages with fs.writeFile(pageAbsPath, rendered, "utf8") after constructing pageAbsPath from the vault root and page path; this supports keeping the PR in human review rather than sweeping it closed while Gateway execution changes the context for those writes. (extensions/memory-wiki/src/source-page-shared.ts:24, 5915489631d7)

Remaining risk / open question:

  • Closing would discard an active maintainer PR even though current main has not implemented the requested Gateway routing behavior.
  • The PR changes CLI execution context for bridge-mode status, doctor, and imports, so maintainers should review Gateway availability UX, response validation/rendering, and the bot-reported symlink/path handling concern before merge.

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

@vincentkoc
vincentkoc force-pushed the clownfish/ghcrawl-156579-autonomous-smoke branch 4 times, most recently from 705b50b to 33b3fbd Compare April 28, 2026 10:01
@aisle-research-bot

aisle-research-bot Bot commented Apr 28, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

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

# Severity Title
1 🟡 Medium DoS via unbounded JSON serialization of Gateway responses with extra properties
2 🟡 Medium TOCTOU symlink race in atomic vault writes can move written file outside vault
1. 🟡 DoS via unbounded JSON serialization of Gateway responses with extra properties
Property Value
Severity Medium
CWE CWE-400
Location extensions/memory-wiki/src/cli.ts:232-391

Description

When the CLI routes bridge operations through the Gateway (callGatewayFromCli), it validates only known fields on the returned object but does not reject or strip additional properties.

As a result:

  • An attacker controlling (or able to tamper with) the Gateway response can add very large extra fields (e.g., huge nested objects/arrays) that still pass isMemoryWikiStatus / isMemoryWikiDoctorReport / isMemoryWikiImportResult.
  • With --json, formatGatewayJsonOrText will JSON.stringify(result, null, 2) the entire object, including the oversized extra fields.
  • This can cause excessive CPU/memory usage and/or massive terminal output (local DoS), despite per-field bounds on the expected schema.

Vulnerable code paths:

  • Validation ignores unknown keys (no key whitelist / no deep size or depth cap)
  • JSON.stringify serializes the full response object for --json

Vulnerable snippets:

function isMemoryWikiStatus(value: unknown): value is MemoryWikiStatus {
  if (!isRecord(value)) {
    return false;
  }// ...checks only expected fields...
  return (
    isBoundedGatewayString(value.vaultMode, GATEWAY_RESPONSE_MAX_CODE_CHARS) &&// ...
    isWarningList(value.warnings)
  );
}

function formatGatewayJsonOrText<T>(result: T, json: boolean | undefined, render: (result: T) => string): string {
  return json
    ? escapeGatewayJsonForTerminal(JSON.stringify(result, null, 2))
    : sanitizeGatewayStringForTerminal(render(result));
}

Recommendation

Harden Gateway response handling so unexpected properties cannot balloon serialization cost.

Recommended fixes (any combination):

  1. Enforce an exact/whitelisted schema (reject unknown keys) before accepting the response.

    • For nested objects, also validate their keys.
  2. Sanitize/normalize the response by constructing a new object containing only expected keys, and stringify that.

Example (pick/normalize before rendering):

function pickMemoryWikiStatus(value: MemoryWikiStatus): MemoryWikiStatus {
  return {
    vaultMode: value.vaultMode,
    renderMode: value.renderMode,
    vaultPath: value.vaultPath,
    vaultExists: value.vaultExists,
    bridgePublicArtifactCount: value.bridgePublicArtifactCount,
    bridge: { enabled: value.bridge.enabled },
    obsidianCli: {
      enabled: value.obsidianCli.enabled,
      requested: value.obsidianCli.requested,
      available: value.obsidianCli.available,
      command: value.obsidianCli.command,
    },
    unsafeLocal: {
      allowPrivateMemoryCoreAccess: value.unsafeLocal.allowPrivateMemoryCoreAccess,
      pathCount: value.unsafeLocal.pathCount,
    },
    pageCounts: value.pageCounts,
    sourceCounts: value.sourceCounts,
    warnings: value.warnings,
  };
}

const validated = validateWikiGatewayResult(method, result);
const normalized = method === "wiki.status" ? pickMemoryWikiStatus(validated) : validated;
return formatGatewayJsonOrText(normalized, json, render);
  1. Add a global size/depth guard for --json output (e.g., limit total serialized bytes or use a safe stringifier with max depth / max array length).
2. 🟡 TOCTOU symlink race in atomic vault writes can move written file outside vault
Property Value
Severity Medium
CWE CWE-367
Location extensions/memory-wiki/src/source-page-shared.ts:96-121

Description

writeFileAtomicInVault() attempts to prevent writing outside the vault by checking realpath(pageDir) before/after operations. However, the function still performs filesystem operations (fs.open() and especially fs.rename()) using string paths that can be redirected if an attacker can swap pageDir (or an intermediate component) to a symlink between the check and the operation.

Key issues:

  • TOCTOU window: assertWritablePageDir() validates realpath(pageDir) and that it is inside vaultRealPath, but then fs.rename(tempPath, filePath) occurs later using tempPath/filePath string paths.
  • Intermediate symlink not prevented: O_NOFOLLOW only protects the final component of tempPath, not any intermediate directory component. If pageDir is replaced with a symlink after validation, rename() can place the final page outside the vault.
  • Even if a subsequent assertWritablePageDir() fails, the file may already have been renamed to an attacker-controlled location.

This is exploitable in scenarios where the vault directory (or its subdirectories like sources/) is writable by another local user/process (e.g., shared vault directory, CI workspace, or multi-user host).

Vulnerable code:

await assertWritablePageDir(params);
const tempPath = path.join(params.pageDir, `.openclaw-wiki-${process.pid}-${randomUUID()}.tmp`);
...
await assertWritablePageDir(params);
await validateDestinationForReplace(params.filePath, params.pagePath);
await fs.rename(tempPath, params.filePath);

Recommendation

Eliminate path-based TOCTOU by anchoring operations to a trusted directory file descriptor and using *at()-style syscalls where available.

On POSIX, use a directory FD for the resolved pageDirRealPath (opened with O_DIRECTORY) and then:

  • create temp files with openat(dirfd, tmpName, O_CREAT|O_EXCL|O_NOFOLLOW, 0o600)
  • perform renameat(dirfd, tmpName, dirfd, destName)

If Node.js APIs don’t provide safe *at primitives in your supported versions, consider:

  • writing into a temp file in the vault root opened as a directory FD and using relative names only, or
  • using a native addon / well-reviewed library that supports openat/renameat semantics, or
  • as a partial mitigation, re-realpath and compare the full destination path immediately after rename and roll back if it escaped (still not race-free).

Also document/enforce that the vault directory must not be writable by untrusted users/processes if strong containment is required.


Analyzed PR: #72615 at commit e72e350

Last updated on: 2026-04-28T10:24:40Z

@vincentkoc
vincentkoc force-pushed the clownfish/ghcrawl-156579-autonomous-smoke branch 4 times, most recently from f3ac28c to b7516b5 Compare April 28, 2026 10:15
@vincentkoc
vincentkoc force-pushed the clownfish/ghcrawl-156579-autonomous-smoke branch from b7516b5 to e72e350 Compare April 28, 2026 10:17
@vincentkoc

Copy link
Copy Markdown
Member Author

Maintainer follow-up is done here.

  • removed redundant json forwarding to the Gateway; the CLI owns JSON/text formatting locally
  • added Gateway response validation before rendering, including bounded strings/arrays before text or JSON output
  • hardened Gateway human output against terminal control/bidi spoofing while preserving JSON semantics
  • hardened imported-source page writes: vault path checks, symlink/non-file refusal, temp-file + rename replacement, and hardlink regression coverage
  • added focused CLI + bridge regressions

Review notes:

  • Greptile's json forwarding finding is fixed.
  • Aisle's latest visible comment is stale on d94885c; current head is e72e3508c7cf2a045ada0d3e8b6961d82235a0e3 with the hardlink/temp-rename and bounded Gateway string fixes.

Proof:

  • Testbox tbx_01kq9rnr93byq25x8m0zmr50a9 on e72e3508c7cf2a045ada0d3e8b6961d82235a0e3: pnpm format:check extensions/memory-wiki/src/cli.ts extensions/memory-wiki/src/cli.test.ts extensions/memory-wiki/src/bridge.test.ts extensions/memory-wiki/src/source-page-shared.ts, pnpm test:serial extensions/memory-wiki/src/cli.test.ts extensions/memory-wiki/src/bridge.test.ts (18 tests), and pnpm check:changed all passed.
  • GitHub CI run 25047206103 is green on e72e3508c7cf2a045ada0d3e8b6961d82235a0e3; Workflow Sanity run 25047206185 is green too.

Thanks @prasad-yashdeep — nothing else needed from you on this one.

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