memory-privacy 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/filters.ts +2 -2
- package/identity.ts +5 -2
- package/index.ts +8 -193
- package/package.json +1 -1
- package/utils.ts +1 -1
package/filters.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionIdentity } from "./identity
|
|
1
|
+
import type { SessionIdentity } from "./identity";
|
|
2
2
|
import {
|
|
3
3
|
extractGroupIdFromPath,
|
|
4
4
|
extractDateFromPath,
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isWithinMembershipPeriod,
|
|
7
7
|
loadKnowledgeACL,
|
|
8
8
|
toRelativeMemoryPath,
|
|
9
|
-
} from "./utils
|
|
9
|
+
} from "./utils";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 过滤 memory_search 结果,按身份隔离
|
package/identity.ts
CHANGED
|
@@ -35,10 +35,13 @@ export function resolveSessionIdentity(sessionKey: string | undefined): SessionI
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* 判断 userId 是否是主人
|
|
38
|
-
* 从环境变量
|
|
38
|
+
* 从环境变量 OPENCLAW_GATEWAY_TOKEN 获取并解析主人 ID
|
|
39
|
+
* token 格式如 "oc-user-f76e8c98afb148539963c1b726f2afac",用 "-" 切分取最后一段作为 owner ID
|
|
39
40
|
*/
|
|
40
41
|
export function isOwner(userId: string): boolean {
|
|
41
|
-
const
|
|
42
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
43
|
+
if (!token) return false;
|
|
44
|
+
const ownerUserId = token.split("-").pop() || "";
|
|
42
45
|
if (!ownerUserId) return false;
|
|
43
46
|
return ownerUserId === userId;
|
|
44
47
|
}
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import { resolveSessionIdentity } from "./identity
|
|
4
|
-
import {
|
|
5
|
-
import { getAllowedDirs
|
|
3
|
+
import { resolveSessionIdentity } from "./identity";
|
|
4
|
+
import { isPathAllowed, getTargetDir, isWritePathCorrect } from "./filters";
|
|
5
|
+
import { getAllowedDirs } from "./utils";
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
id: "memory-privacy",
|
|
@@ -10,153 +10,14 @@ export default {
|
|
|
10
10
|
description: "Multi-user memory isolation for IM + OpenClaw",
|
|
11
11
|
|
|
12
12
|
register(api: any) {
|
|
13
|
-
const
|
|
13
|
+
const log = api.logger || console;
|
|
14
14
|
const workspaceDir: string =
|
|
15
15
|
api.runtime?.config?.agents?.defaults?.workspace ??
|
|
16
16
|
api.config?.agents?.defaults?.workspace ??
|
|
17
17
|
process.env.OPENCLAW_WORKSPACE ??
|
|
18
18
|
process.cwd();
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// ====================================================================
|
|
23
|
-
// 钩子 1: before_prompt_build — 用 memory_search 检索相关记忆注入上下文
|
|
24
|
-
// ====================================================================
|
|
25
|
-
api.on("before_prompt_build", async (event: any, ctx: any) => {
|
|
26
|
-
const identity = resolveSessionIdentity(ctx.sessionKey);
|
|
27
|
-
const wsDir = ctx.workspaceDir || workspaceDir;
|
|
28
|
-
|
|
29
|
-
// 从 event.messages 提取用户最新消息作为搜索 query
|
|
30
|
-
const messages = event.messages || [];
|
|
31
|
-
const lastUserMsg = [...messages].reverse().find((m: any) => m.role === "user");
|
|
32
|
-
let query = "";
|
|
33
|
-
if (lastUserMsg) {
|
|
34
|
-
const content = lastUserMsg.content;
|
|
35
|
-
if (typeof content === "string") query = content;
|
|
36
|
-
else if (Array.isArray(content)) {
|
|
37
|
-
query = content.filter((c: any) => c.type === "text").map((c: any) => c.text || "").join(" ");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let memoryBlock = "";
|
|
42
|
-
|
|
43
|
-
if (identity.type === "owner") {
|
|
44
|
-
// owner 使用语义搜索(需要 query)
|
|
45
|
-
if (query) {
|
|
46
|
-
try {
|
|
47
|
-
const searchTool = api.runtime.tools.createMemorySearchTool({
|
|
48
|
-
config: ctx.config || config,
|
|
49
|
-
agentSessionKey: ctx.sessionKey,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (searchTool) {
|
|
53
|
-
const rawResults = await searchTool.execute("prompt-build-search", { query });
|
|
54
|
-
const details = rawResults?.details;
|
|
55
|
-
const results = Array.isArray(details?.results) ? details.results : [];
|
|
56
|
-
const snippets = results
|
|
57
|
-
.map((r: any) => r.snippet || "")
|
|
58
|
-
.filter((s: string) => s.length > 0);
|
|
59
|
-
|
|
60
|
-
if (snippets.length > 0) {
|
|
61
|
-
memoryBlock = `\n\n${snippets.join("\n\n")}`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// owner 搜索失败不回退
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
// 非 owner(peer / group):直接全量加载已过滤的记忆,不依赖 query
|
|
70
|
-
const fallback = loadAllowedMemoryContent(wsDir, identity);
|
|
71
|
-
if (fallback) memoryBlock = `\n\n${fallback}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
switch (identity.type) {
|
|
75
|
-
case "owner":
|
|
76
|
-
return {
|
|
77
|
-
prependContext:
|
|
78
|
-
`` +
|
|
79
|
-
memoryBlock,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
case "peer":
|
|
83
|
-
return {
|
|
84
|
-
prependContext:
|
|
85
|
-
`` +
|
|
86
|
-
memoryBlock,
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
case "group":
|
|
90
|
-
return {
|
|
91
|
-
prependContext:
|
|
92
|
-
`` +
|
|
93
|
-
memoryBlock,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ====================================================================
|
|
99
|
-
// 钩子 2: registerTool — 包装 memory_search / memory_get
|
|
100
|
-
// ====================================================================
|
|
101
|
-
api.registerTool(
|
|
102
|
-
(ctx: any) => {
|
|
103
|
-
const identity = resolveSessionIdentity(ctx.sessionKey);
|
|
104
|
-
const wsDir = ctx.workspaceDir || workspaceDir;
|
|
105
|
-
|
|
106
|
-
const originalSearchTool = api.runtime.tools.createMemorySearchTool({
|
|
107
|
-
config: ctx.config || config,
|
|
108
|
-
agentSessionKey: ctx.sessionKey,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const originalGetTool = api.runtime.tools.createMemoryGetTool({
|
|
112
|
-
config: ctx.config || config,
|
|
113
|
-
agentSessionKey: ctx.sessionKey,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const tools: any[] = [];
|
|
117
|
-
|
|
118
|
-
if (originalSearchTool) {
|
|
119
|
-
const wrappedSearchTool = {
|
|
120
|
-
...originalSearchTool,
|
|
121
|
-
execute: async (
|
|
122
|
-
toolCallId: string,
|
|
123
|
-
params: Record<string, unknown>,
|
|
124
|
-
signal?: AbortSignal,
|
|
125
|
-
onUpdate?: any
|
|
126
|
-
) => {
|
|
127
|
-
const results = await originalSearchTool.execute(toolCallId, params, signal, onUpdate);
|
|
128
|
-
return filterResultsByIdentity(results, identity, wsDir);
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
tools.push(wrappedSearchTool);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (originalGetTool) {
|
|
135
|
-
const wrappedGetTool = {
|
|
136
|
-
...originalGetTool,
|
|
137
|
-
execute: async (
|
|
138
|
-
toolCallId: string,
|
|
139
|
-
params: Record<string, unknown>,
|
|
140
|
-
signal?: AbortSignal,
|
|
141
|
-
onUpdate?: any
|
|
142
|
-
) => {
|
|
143
|
-
const filePath = (params.path || params.filePath || "") as string;
|
|
144
|
-
if (filePath && !isPathAllowed(filePath, identity, wsDir)) {
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: "text" as const, text: "No results found." }],
|
|
147
|
-
details: { error: "access_denied" },
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return originalGetTool.execute(toolCallId, params, signal, onUpdate);
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
tools.push(wrappedGetTool);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return tools.length > 0 ? tools : null;
|
|
157
|
-
},
|
|
158
|
-
{ names: ["memory_search", "memory_get"] }
|
|
159
|
-
);
|
|
20
|
+
log.info(`[memory-privacy] workspaceDir: ${workspaceDir}`);
|
|
160
21
|
|
|
161
22
|
// ====================================================================
|
|
162
23
|
// 钩子 3: before_tool_call — 写入拦截 + 重定向
|
|
@@ -223,7 +84,7 @@ export default {
|
|
|
223
84
|
const targetDir = getTargetDir(identity);
|
|
224
85
|
const correctedPath = path.join(wsDir, targetDir, filename);
|
|
225
86
|
|
|
226
|
-
|
|
87
|
+
log.info(`[memory-privacy] Redirecting write: ${filePath} → ${correctedPath}`);
|
|
227
88
|
|
|
228
89
|
// 同时设置所有可能的路径参数名,确保不同工具都能接收
|
|
229
90
|
const correctedParams = { ...event.params };
|
|
@@ -377,57 +238,11 @@ export default {
|
|
|
377
238
|
}
|
|
378
239
|
}
|
|
379
240
|
|
|
380
|
-
|
|
241
|
+
log.info(`[memory-privacy] Rewriting exec: ${command} → ${rewritten}`);
|
|
381
242
|
|
|
382
243
|
return { params: { ...event.params, command: rewritten, cmd: rewritten } };
|
|
383
244
|
}, { priority: 10 });
|
|
384
245
|
|
|
385
|
-
// ====================================================================
|
|
386
|
-
// 钩子 6: before_tool_call — memory_get 读取拦截
|
|
387
|
-
// registerTool 包装未生效(添加新工具而非替换),用 before_tool_call 可靠拦截
|
|
388
|
-
// ====================================================================
|
|
389
|
-
api.on("before_tool_call", (event: any, ctx: any) => {
|
|
390
|
-
if (event.toolName !== "memory_get") return;
|
|
391
|
-
|
|
392
|
-
const filePath = (event.params?.path || event.params?.filePath || event.params?.file_path || "") as string;
|
|
393
|
-
if (!filePath) return;
|
|
394
|
-
|
|
395
|
-
const identity = resolveSessionIdentity(ctx.sessionKey);
|
|
396
|
-
if (identity.type === "owner") return;
|
|
397
|
-
|
|
398
|
-
// 拦截 MEMORY.md(非 owner 不可读)
|
|
399
|
-
const lowerFilePath = filePath.toLowerCase();
|
|
400
|
-
if (lowerFilePath.endsWith("/memory.md") || lowerFilePath.endsWith("\\memory.md") || lowerFilePath === "memory.md") {
|
|
401
|
-
return { block: true, blockReason: "No results found." };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// 拦截 USER.md(group session 不可读)
|
|
405
|
-
if (lowerFilePath.endsWith("/user.md") || lowerFilePath.endsWith("\\user.md") || lowerFilePath === "user.md") {
|
|
406
|
-
if (identity.type === "group") {
|
|
407
|
-
return { block: true, blockReason: "No results found." };
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const wsDir = ctx.workspaceDir || workspaceDir;
|
|
412
|
-
if (!isPathAllowed(filePath, identity, wsDir)) {
|
|
413
|
-
return { block: true, blockReason: "No results found." };
|
|
414
|
-
}
|
|
415
|
-
}, { priority: 10 });
|
|
416
|
-
|
|
417
|
-
// ====================================================================
|
|
418
|
-
// 钩子 6.5: before_tool_call — 非 owner 禁用 memory_search
|
|
419
|
-
// 框架不 await async before_tool_call,无法做结果过滤
|
|
420
|
-
// 直接 block,依赖 hook1 (before_prompt_build) 注入已过滤的记忆
|
|
421
|
-
// ====================================================================
|
|
422
|
-
api.on("before_tool_call", (event: any, ctx: any) => {
|
|
423
|
-
if (event.toolName !== "memory_search") return;
|
|
424
|
-
|
|
425
|
-
const identity = resolveSessionIdentity(ctx.sessionKey);
|
|
426
|
-
if (identity.type === "owner") return;
|
|
427
|
-
|
|
428
|
-
return { block: true, blockReason: "No results found." };
|
|
429
|
-
}, { priority: 10 });
|
|
430
|
-
|
|
431
246
|
// ====================================================================
|
|
432
247
|
// 钩子 7: agent_end — 将 agent 对话同步到 memory 文件
|
|
433
248
|
// ====================================================================
|
|
@@ -474,7 +289,7 @@ export default {
|
|
|
474
289
|
if (assistantText) fs.appendFileSync(filePath!, `[${time}] AI助理: ${assistantText}\n`);
|
|
475
290
|
});
|
|
476
291
|
|
|
477
|
-
|
|
292
|
+
log.info("[memory-privacy] Plugin registered successfully.");
|
|
478
293
|
},
|
|
479
294
|
};
|
|
480
295
|
|
package/package.json
CHANGED