mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix(memory): enforce wiki session visibility (#75722)
* fix(memory): enforce wiki session visibility Co-authored-by: zsx <git@zsxsoft.com> * fix(memory): cover wiki visibility follow-ups # Conflicts: # CHANGELOG.md * fix(memory): tighten wiki session visibility reads * docs(changelog): add memory wiki visibility entry --------- Co-authored-by: zsx <git@zsxsoft.com> Co-authored-by: Devin Robison <drobison@nvidia.com> Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
This commit is contained in:
@@ -369,6 +369,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/messaging: surface CLI subprocess watchdog/turn timeout messages to chat users when verbose failures are off, instead of collapsing them into generic external-run failure copy. Fixes #77007. (#77015) Thanks @neeravmakwana.
|
||||
- Agents/sessions: after embedded Pi runs, append assistant-visible reply text to session JSONL only when Pi did not already persist an equivalent tail assistant entry, without re-mirroring the user prompt Pi owns. Fixes #77823. (#77839) Thanks @neeravmakwana.
|
||||
- Plugins/CLI: load the install-records ledger when listing channel-catalog entries, so npm-installed third-party channel plugins resolve through `openclaw channels login`/`channels add` instead of failing with `Unsupported channel`. (#77269) Thanks @pumpkinxing1.
|
||||
- Memory wiki/Security: enforce session visibility on shared-memory `wiki_search` and `wiki_get` so sandboxed subagents cannot read transcript content from sibling or parent sessions. Fixes GHSA-72fw-cqh5-f324. Thanks @zsxsoft.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export default definePluginEntry({
|
||||
createWikiSearchTool(config, api.config, {
|
||||
agentId: ctx.agentId,
|
||||
agentSessionKey: ctx.sessionKey,
|
||||
sandboxed: ctx.sandboxed,
|
||||
}),
|
||||
{ name: "wiki_search" },
|
||||
);
|
||||
@@ -41,6 +42,7 @@ export default definePluginEntry({
|
||||
createWikiGetTool(config, api.config, {
|
||||
agentId: ctx.agentId,
|
||||
agentSessionKey: ctx.sessionKey,
|
||||
sandboxed: ctx.sandboxed,
|
||||
}),
|
||||
{ name: "wiki_get" },
|
||||
);
|
||||
|
||||
@@ -6,17 +6,22 @@ import type { OpenClawConfig } from "../api.js";
|
||||
import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { MemoryWikiPluginConfig } from "./config.js";
|
||||
import { renderWikiMarkdown } from "./markdown.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
import { getMemoryWikiPage, isSessionMemoryPath, searchMemoryWiki } from "./query.js";
|
||||
import { createMemoryWikiTestHarness } from "./test-helpers.js";
|
||||
|
||||
const { getActiveMemorySearchManagerMock, resolveDefaultAgentIdMock, resolveSessionAgentIdMock } =
|
||||
vi.hoisted(() => ({
|
||||
getActiveMemorySearchManagerMock: vi.fn(),
|
||||
resolveDefaultAgentIdMock: vi.fn(() => "main"),
|
||||
resolveSessionAgentIdMock: vi.fn(({ sessionKey }: { sessionKey?: string }) =>
|
||||
sessionKey === "agent:secondary:thread" ? "secondary" : "main",
|
||||
),
|
||||
}));
|
||||
const {
|
||||
getActiveMemorySearchManagerMock,
|
||||
loadCombinedSessionStoreForGatewayMock,
|
||||
resolveDefaultAgentIdMock,
|
||||
resolveSessionAgentIdMock,
|
||||
} = vi.hoisted(() => ({
|
||||
getActiveMemorySearchManagerMock: vi.fn(),
|
||||
loadCombinedSessionStoreForGatewayMock: vi.fn(),
|
||||
resolveDefaultAgentIdMock: vi.fn(() => "main"),
|
||||
resolveSessionAgentIdMock: vi.fn(({ sessionKey }: { sessionKey?: string }) =>
|
||||
sessionKey === "agent:secondary:thread" ? "secondary" : "main",
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({
|
||||
getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
|
||||
@@ -27,6 +32,15 @@ vi.mock("openclaw/plugin-sdk/memory-host-core", () => ({
|
||||
resolveSessionAgentId: resolveSessionAgentIdMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/session-transcript-hit", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("openclaw/plugin-sdk/session-transcript-hit")>();
|
||||
return {
|
||||
...actual,
|
||||
loadCombinedSessionStoreForGateway: loadCombinedSessionStoreForGatewayMock,
|
||||
};
|
||||
});
|
||||
|
||||
const { createVault } = createMemoryWikiTestHarness();
|
||||
let suiteRoot = "";
|
||||
let caseIndex = 0;
|
||||
@@ -34,6 +48,8 @@ let caseIndex = 0;
|
||||
beforeEach(() => {
|
||||
getActiveMemorySearchManagerMock.mockReset();
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager: null, error: "unavailable" });
|
||||
loadCombinedSessionStoreForGatewayMock.mockReset();
|
||||
loadCombinedSessionStoreForGatewayMock.mockReturnValue({ storePath: "(test)", store: {} });
|
||||
resolveDefaultAgentIdMock.mockClear();
|
||||
resolveSessionAgentIdMock.mockClear();
|
||||
});
|
||||
@@ -68,6 +84,36 @@ function createAppConfig(): OpenClawConfig {
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createSessionVisibilityAppConfig(): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: { sandbox: { sessionToolsVisibility: "all" } },
|
||||
list: [{ id: "main", default: true }],
|
||||
},
|
||||
tools: {
|
||||
sessions: { visibility: "self" },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function mockSessionTranscriptStore() {
|
||||
loadCombinedSessionStoreForGatewayMock.mockReturnValue({
|
||||
storePath: "(test)",
|
||||
store: {
|
||||
"agent:main:child-session": {
|
||||
sessionId: "child-session",
|
||||
updatedAt: 1,
|
||||
sessionFile: "/tmp/openclaw/child-session.jsonl",
|
||||
},
|
||||
"agent:main:sibling-session": {
|
||||
sessionId: "sibling-session",
|
||||
updatedAt: 2,
|
||||
sessionFile: "/tmp/openclaw/sibling-session.jsonl",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createMemoryManager(overrides?: {
|
||||
searchResults?: Array<{
|
||||
path: string;
|
||||
@@ -95,6 +141,29 @@ function createMemoryManager(overrides?: {
|
||||
};
|
||||
}
|
||||
|
||||
describe("isSessionMemoryPath", () => {
|
||||
it("classifies all current session storage layouts", () => {
|
||||
for (const relPath of [
|
||||
"sessions/child-session.jsonl",
|
||||
"qmd/sessions/child-session.md",
|
||||
"qmd/sessions-main/child-session.md",
|
||||
"qmd\\sessions-main\\child-session.md",
|
||||
"qmd/sessions",
|
||||
]) {
|
||||
expect(isSessionMemoryPath(relPath)).toBe(true);
|
||||
}
|
||||
|
||||
for (const relPath of [
|
||||
"sessionsx/child-session.jsonl",
|
||||
"qmd/sessionsxxx",
|
||||
"wiki/sessions/foo.md",
|
||||
"wiki\\sessions\\foo.md",
|
||||
]) {
|
||||
expect(isSessionMemoryPath(relPath)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchMemoryWiki", () => {
|
||||
it("finds wiki pages by title and body", async () => {
|
||||
const { rootDir, config } = await createQueryVault({
|
||||
@@ -634,6 +703,132 @@ describe("searchMemoryWiki", () => {
|
||||
expect(manager.search).toHaveBeenCalledWith("alpha", { maxResults: 5 });
|
||||
});
|
||||
|
||||
it("filters session memory hits outside the caller visibility policy", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
mockSessionTranscriptStore();
|
||||
const manager = createMemoryManager({
|
||||
searchResults: [
|
||||
{
|
||||
path: "sessions/child-session.jsonl",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 30,
|
||||
snippet: "caller transcript",
|
||||
source: "sessions",
|
||||
},
|
||||
{
|
||||
path: "qmd/sessions-main/sibling-session.md",
|
||||
startLine: 3,
|
||||
endLine: 4,
|
||||
score: 20,
|
||||
snippet: "sibling transcript",
|
||||
source: "sessions",
|
||||
},
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 5,
|
||||
endLine: 6,
|
||||
score: 10,
|
||||
snippet: "durable memory",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
appConfig: createSessionVisibilityAppConfig(),
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: true,
|
||||
query: "transcript",
|
||||
maxResults: 10,
|
||||
});
|
||||
|
||||
expect(results.map((result) => result.path)).toEqual([
|
||||
"sessions/child-session.jsonl",
|
||||
"MEMORY.md",
|
||||
]);
|
||||
expect(results.some((result) => result.path.includes("sibling-session"))).toBe(false);
|
||||
});
|
||||
|
||||
it("filters session memory hits for session-bound non-sandboxed callers", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
mockSessionTranscriptStore();
|
||||
const manager = createMemoryManager({
|
||||
searchResults: [
|
||||
{
|
||||
path: "sessions/child-session.jsonl",
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
score: 30,
|
||||
snippet: "caller transcript",
|
||||
source: "sessions",
|
||||
},
|
||||
{
|
||||
path: "qmd/sessions-main/sibling-session.md",
|
||||
startLine: 3,
|
||||
endLine: 4,
|
||||
score: 20,
|
||||
snippet: "sibling transcript",
|
||||
source: "sessions",
|
||||
},
|
||||
{
|
||||
path: "MEMORY.md",
|
||||
startLine: 5,
|
||||
endLine: 6,
|
||||
score: 10,
|
||||
snippet: "durable memory",
|
||||
source: "memory",
|
||||
},
|
||||
],
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const results = await searchMemoryWiki({
|
||||
config,
|
||||
appConfig: createSessionVisibilityAppConfig(),
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: false,
|
||||
query: "transcript",
|
||||
maxResults: 10,
|
||||
});
|
||||
|
||||
expect(results.map((result) => result.path)).toEqual([
|
||||
"sessions/child-session.jsonl",
|
||||
"MEMORY.md",
|
||||
]);
|
||||
expect(results.some((result) => result.path.includes("sibling-session"))).toBe(false);
|
||||
});
|
||||
|
||||
it("requires appConfig for session-bound shared memory searches", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
searchMemoryWiki({
|
||||
config,
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: true,
|
||||
query: "transcript",
|
||||
}),
|
||||
).rejects.toThrow(/wiki_search requires appConfig/);
|
||||
});
|
||||
|
||||
it("uses the active session agent for shared memory search", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
@@ -902,6 +1097,89 @@ describe("getMemoryWikiPage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips session memory reads outside the caller visibility policy", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
mockSessionTranscriptStore();
|
||||
const manager = createMemoryManager({
|
||||
readResult: {
|
||||
path: "qmd/sessions-main/sibling-session.md",
|
||||
text: "sibling transcript content",
|
||||
},
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const result = await getMemoryWikiPage({
|
||||
config,
|
||||
appConfig: createSessionVisibilityAppConfig(),
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: true,
|
||||
lookup: "qmd/sessions-main/sibling-session.md",
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(manager.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("permits session memory reads inside the caller visibility policy", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
mockSessionTranscriptStore();
|
||||
const manager = createMemoryManager({
|
||||
readResult: {
|
||||
path: "qmd/sessions-main/child-session.md",
|
||||
text: "own transcript content",
|
||||
},
|
||||
});
|
||||
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
|
||||
|
||||
const result = await getMemoryWikiPage({
|
||||
config,
|
||||
appConfig: createSessionVisibilityAppConfig(),
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: true,
|
||||
lookup: "qmd/sessions-main/child-session.md",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
corpus: "memory",
|
||||
path: "qmd/sessions-main/child-session.md",
|
||||
content: "own transcript content",
|
||||
});
|
||||
expect(manager.readFile).toHaveBeenCalledTimes(1);
|
||||
expect(manager.readFile).toHaveBeenCalledWith({
|
||||
relPath: "qmd/sessions-main/child-session.md",
|
||||
from: 1,
|
||||
lines: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("requires appConfig for session-bound shared memory reads", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
config: {
|
||||
search: { backend: "shared", corpus: "memory" },
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
getMemoryWikiPage({
|
||||
config,
|
||||
agentSessionKey: "agent:main:child-session",
|
||||
sandboxed: true,
|
||||
lookup: "sessions/child-session.jsonl",
|
||||
}),
|
||||
).rejects.toThrow(/wiki_get requires appConfig/);
|
||||
});
|
||||
|
||||
it("uses the active session agent for shared memory reads", async () => {
|
||||
const { config } = await createQueryVault({
|
||||
initialize: true,
|
||||
|
||||
@@ -3,6 +3,16 @@ import path from "node:path";
|
||||
import { resolveDefaultAgentId, resolveSessionAgentId } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-host-files";
|
||||
import { getActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search";
|
||||
import {
|
||||
extractTranscriptStemFromSessionsMemoryHit,
|
||||
loadCombinedSessionStoreForGateway,
|
||||
resolveTranscriptStemToSessionKeys,
|
||||
} from "openclaw/plugin-sdk/session-transcript-hit";
|
||||
import {
|
||||
createAgentToAgentPolicy,
|
||||
createSessionVisibilityGuard,
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
} from "openclaw/plugin-sdk/session-visibility";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
import { assessClaimFreshness, isClaimContestedStatus } from "./claim-health.js";
|
||||
@@ -952,6 +962,51 @@ function buildLookupCandidates(lookup: string): string[] {
|
||||
return [...new Set([normalized, withExtension])];
|
||||
}
|
||||
|
||||
function shouldEnforceSessionVisibility(params: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
}): boolean {
|
||||
return params.sandboxed === true || Boolean(params.agentSessionKey?.trim());
|
||||
}
|
||||
|
||||
function shouldSearchSharedMemoryCorpus(config: ResolvedMemoryWikiConfig): boolean {
|
||||
return config.search.corpus === "memory" || config.search.corpus === "all";
|
||||
}
|
||||
|
||||
function shouldUseSharedMemory(config: ResolvedMemoryWikiConfig): boolean {
|
||||
return config.search.backend === "shared" && shouldSearchSharedMemoryCorpus(config);
|
||||
}
|
||||
|
||||
function assertSessionVisibilityAppConfig(params: {
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
operation: string;
|
||||
}): void {
|
||||
if (
|
||||
shouldUseSharedMemory(params.config) &&
|
||||
shouldEnforceSessionVisibility(params) &&
|
||||
!params.appConfig
|
||||
) {
|
||||
throw new Error(
|
||||
`${params.operation} requires appConfig to enforce session visibility for session-bound shared memory calls.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SESSION_MEMORY_PATH_PREFIXES = ["sessions/", "qmd/sessions/", "qmd/sessions-"] as const;
|
||||
const SESSION_MEMORY_ROOT_PATHS = ["qmd/sessions"] as const;
|
||||
|
||||
// Keep these path shapes aligned with source: "sessions" hits in session-search-visibility and session-transcript-hit.
|
||||
export function isSessionMemoryPath(relPath: string): boolean {
|
||||
const normalized = relPath.replace(/\\/g, "/");
|
||||
return (
|
||||
SESSION_MEMORY_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix)) ||
|
||||
SESSION_MEMORY_ROOT_PATHS.some((rootPath) => normalized === rootPath)
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSearchWiki(config: ResolvedMemoryWikiConfig): boolean {
|
||||
return config.search.corpus === "wiki" || config.search.corpus === "all";
|
||||
}
|
||||
@@ -960,11 +1015,7 @@ function shouldSearchSharedMemory(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
): boolean {
|
||||
return (
|
||||
config.search.backend === "shared" &&
|
||||
appConfig !== undefined &&
|
||||
(config.search.corpus === "memory" || config.search.corpus === "all")
|
||||
);
|
||||
return shouldUseSharedMemory(config) && appConfig !== undefined;
|
||||
}
|
||||
|
||||
function resolveActiveMemoryAgentId(params: {
|
||||
@@ -1152,6 +1203,104 @@ function toMemoryWikiSearchResult(
|
||||
};
|
||||
}
|
||||
|
||||
async function filterMemoryWikiSearchHitsBySessionVisibility(params: {
|
||||
cfg: OpenClawConfig;
|
||||
requesterSessionKey: string | undefined;
|
||||
sandboxed: boolean;
|
||||
hits: MemorySearchResult[];
|
||||
}): Promise<MemorySearchResult[]> {
|
||||
if (!params.hits.some((hit) => hit.source === "sessions")) {
|
||||
return params.hits;
|
||||
}
|
||||
|
||||
const canReadSessionPath = await createSessionMemoryPathVisibilityChecker({
|
||||
cfg: params.cfg,
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
sandboxed: params.sandboxed,
|
||||
});
|
||||
return filterMemoryWikiSearchHitsWithSessionVisibility({
|
||||
canReadSessionPath,
|
||||
hits: params.hits,
|
||||
});
|
||||
}
|
||||
|
||||
type SessionMemoryPathVisibilityChecker = (relPath: string) => boolean;
|
||||
|
||||
async function createSessionMemoryPathVisibilityChecker(params: {
|
||||
cfg: OpenClawConfig;
|
||||
requesterSessionKey: string | undefined;
|
||||
sandboxed: boolean;
|
||||
}): Promise<SessionMemoryPathVisibilityChecker> {
|
||||
const visibility = resolveEffectiveSessionToolsVisibility({
|
||||
cfg: params.cfg,
|
||||
sandboxed: params.sandboxed,
|
||||
});
|
||||
const a2aPolicy = createAgentToAgentPolicy(params.cfg);
|
||||
const guard = params.requesterSessionKey
|
||||
? await createSessionVisibilityGuard({
|
||||
action: "history",
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
visibility,
|
||||
a2aPolicy,
|
||||
})
|
||||
: null;
|
||||
if (!guard) {
|
||||
return () => false;
|
||||
}
|
||||
|
||||
const { store: combinedSessionStore } = loadCombinedSessionStoreForGateway(params.cfg);
|
||||
return (relPath) => {
|
||||
const stem = extractTranscriptStemFromSessionsMemoryHit(relPath);
|
||||
if (!stem) {
|
||||
return false;
|
||||
}
|
||||
const keys = resolveTranscriptStemToSessionKeys({
|
||||
store: combinedSessionStore,
|
||||
stem,
|
||||
});
|
||||
return keys.some((key) => guard.check(key).allowed);
|
||||
};
|
||||
}
|
||||
|
||||
function filterMemoryWikiSearchHitsWithSessionVisibility(params: {
|
||||
canReadSessionPath: SessionMemoryPathVisibilityChecker;
|
||||
hits: MemorySearchResult[];
|
||||
}): MemorySearchResult[] {
|
||||
const next: MemorySearchResult[] = [];
|
||||
for (const hit of params.hits) {
|
||||
if (hit.source !== "sessions") {
|
||||
next.push(hit);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (params.canReadSessionPath(hit.path)) {
|
||||
next.push(hit);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function canReadSessionMemoryPath(params: {
|
||||
canReadSessionPath: SessionMemoryPathVisibilityChecker;
|
||||
relPath: string;
|
||||
}): boolean {
|
||||
// Reuses the search filter with a synthetic hit; update this if the filter needs more than path/source.
|
||||
const filtered = filterMemoryWikiSearchHitsWithSessionVisibility({
|
||||
canReadSessionPath: params.canReadSessionPath,
|
||||
hits: [
|
||||
{
|
||||
path: params.relPath,
|
||||
startLine: 1,
|
||||
endLine: 1,
|
||||
score: 0,
|
||||
snippet: "",
|
||||
source: "sessions",
|
||||
},
|
||||
],
|
||||
});
|
||||
return filtered.length > 0;
|
||||
}
|
||||
|
||||
async function searchWikiCorpus(params: {
|
||||
rootDir: string;
|
||||
query: string;
|
||||
@@ -1223,6 +1372,7 @@ export async function searchMemoryWiki(params: {
|
||||
appConfig?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
query: string;
|
||||
maxResults?: number;
|
||||
searchBackend?: WikiSearchBackend;
|
||||
@@ -1230,6 +1380,13 @@ export async function searchMemoryWiki(params: {
|
||||
mode?: WikiSearchMode;
|
||||
}): Promise<WikiSearchResult[]> {
|
||||
const effectiveConfig = applySearchOverrides(params.config, params);
|
||||
assertSessionVisibilityAppConfig({
|
||||
config: effectiveConfig,
|
||||
appConfig: params.appConfig,
|
||||
agentSessionKey: params.agentSessionKey,
|
||||
sandboxed: params.sandboxed,
|
||||
operation: "wiki_search",
|
||||
});
|
||||
await initializeMemoryWikiVault(effectiveConfig);
|
||||
const maxResults = Math.max(1, params.maxResults ?? 10);
|
||||
const mode = params.mode ?? "auto";
|
||||
@@ -1250,11 +1407,22 @@ export async function searchMemoryWiki(params: {
|
||||
agentSessionKey: params.agentSessionKey,
|
||||
})
|
||||
: null;
|
||||
const memoryResults = sharedMemoryManager
|
||||
? (await sharedMemoryManager.search(params.query, { maxResults })).map((result) =>
|
||||
toMemoryWikiSearchResult(result, mode),
|
||||
)
|
||||
let rawMemoryResults = sharedMemoryManager
|
||||
? await sharedMemoryManager.search(params.query, { maxResults })
|
||||
: [];
|
||||
if (
|
||||
params.appConfig &&
|
||||
shouldEnforceSessionVisibility(params) &&
|
||||
rawMemoryResults.some((hit) => hit.source === "sessions")
|
||||
) {
|
||||
rawMemoryResults = await filterMemoryWikiSearchHitsBySessionVisibility({
|
||||
cfg: params.appConfig,
|
||||
requesterSessionKey: params.agentSessionKey,
|
||||
sandboxed: params.sandboxed === true,
|
||||
hits: rawMemoryResults,
|
||||
});
|
||||
}
|
||||
const memoryResults = rawMemoryResults.map((result) => toMemoryWikiSearchResult(result, mode));
|
||||
|
||||
return mergeWikiSearchCorpusResults({
|
||||
wikiResults,
|
||||
@@ -1269,6 +1437,7 @@ export async function getMemoryWikiPage(params: {
|
||||
appConfig?: OpenClawConfig;
|
||||
agentId?: string;
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
lookup: string;
|
||||
fromLine?: number;
|
||||
lineCount?: number;
|
||||
@@ -1276,6 +1445,13 @@ export async function getMemoryWikiPage(params: {
|
||||
searchCorpus?: WikiSearchCorpus;
|
||||
}): Promise<WikiGetResult | null> {
|
||||
const effectiveConfig = applySearchOverrides(params.config, params);
|
||||
assertSessionVisibilityAppConfig({
|
||||
config: effectiveConfig,
|
||||
appConfig: params.appConfig,
|
||||
agentSessionKey: params.agentSessionKey,
|
||||
sandboxed: params.sandboxed,
|
||||
operation: "wiki_get",
|
||||
});
|
||||
await initializeMemoryWikiVault(effectiveConfig);
|
||||
const fromLine = Math.max(1, params.fromLine ?? 1);
|
||||
const lineCount = Math.max(1, params.lineCount ?? 200);
|
||||
@@ -1327,7 +1503,30 @@ export async function getMemoryWikiPage(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const relPath of buildLookupCandidates(params.lookup)) {
|
||||
const lookupCandidates = buildLookupCandidates(params.lookup);
|
||||
const canReadSessionPath =
|
||||
params.appConfig &&
|
||||
shouldEnforceSessionVisibility(params) &&
|
||||
lookupCandidates.some((relPath) => isSessionMemoryPath(relPath))
|
||||
? await createSessionMemoryPathVisibilityChecker({
|
||||
cfg: params.appConfig,
|
||||
requesterSessionKey: params.agentSessionKey,
|
||||
sandboxed: params.sandboxed === true,
|
||||
})
|
||||
: null;
|
||||
|
||||
for (const relPath of lookupCandidates) {
|
||||
if (
|
||||
canReadSessionPath &&
|
||||
isSessionMemoryPath(relPath) &&
|
||||
!canReadSessionMemoryPath({
|
||||
canReadSessionPath,
|
||||
relPath,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await manager.readFile({
|
||||
relPath,
|
||||
|
||||
@@ -89,6 +89,7 @@ async function syncImportedSourcesIfNeeded(
|
||||
type WikiToolMemoryContext = {
|
||||
agentId?: string;
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
};
|
||||
|
||||
export function createWikiStatusTool(
|
||||
@@ -139,6 +140,7 @@ export function createWikiSearchTool(
|
||||
appConfig,
|
||||
agentId: memoryContext.agentId,
|
||||
agentSessionKey: memoryContext.agentSessionKey,
|
||||
sandboxed: memoryContext.sandboxed,
|
||||
query: params.query,
|
||||
maxResults: params.maxResults,
|
||||
...(params.backend ? { searchBackend: params.backend } : {}),
|
||||
@@ -255,6 +257,7 @@ export function createWikiGetTool(
|
||||
appConfig,
|
||||
agentId: memoryContext.agentId,
|
||||
agentSessionKey: memoryContext.agentSessionKey,
|
||||
sandboxed: memoryContext.sandboxed,
|
||||
lookup: params.lookup,
|
||||
fromLine: params.fromLine,
|
||||
lineCount: params.lineCount,
|
||||
|
||||
Reference in New Issue
Block a user