memory-privacy 1.1.0 → 1.2.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.
Files changed (3) hide show
  1. package/identity.ts +7 -9
  2. package/index.ts +61 -40
  3. package/package.json +1 -1
package/identity.ts CHANGED
@@ -10,7 +10,7 @@ export type SessionIdentity =
10
10
  * 单聊: agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_release_{releaseName}
11
11
  * 群聊: agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_group_{groupID}_release_{releaseName}
12
12
  */
13
- export function resolveSessionIdentity(sessionKey: string | undefined, config: any): SessionIdentity {
13
+ export function resolveSessionIdentity(sessionKey: string | undefined): SessionIdentity {
14
14
  const sk = sessionKey ?? "";
15
15
  // 取最后一段(会话ID)
16
16
  const sessionPart = sk.split(":").pop() || "";
@@ -25,7 +25,7 @@ export function resolveSessionIdentity(sessionKey: string | undefined, config: a
25
25
  const peerMatch = sessionPart.match(/user_(.+?)_lobster_(.+?)_release_(.+)$/);
26
26
  if (peerMatch) {
27
27
  const userID = peerMatch[1];
28
- if (isOwner(userID, config)) return { type: "owner", userId: userID };
28
+ if (isOwner(userID)) return { type: "owner", userId: userID };
29
29
  return { type: "peer", peerId: userID };
30
30
  }
31
31
 
@@ -35,12 +35,10 @@ export function resolveSessionIdentity(sessionKey: string | undefined, config: a
35
35
 
36
36
  /**
37
37
  * 判断 userId 是否是主人
38
- * 对比 openclaw.json 中的 commands.ownerAllowFrom 列表
38
+ * 从环境变量 owner_claw_user_id 获取主人 ID
39
39
  */
40
- export function isOwner(userId: string, config: any): boolean {
41
- const ownerList: (string | number)[] = config?.commands?.ownerAllowFrom ?? [];
42
- return ownerList.some(entry => {
43
- const entryStr = String(entry);
44
- return entryStr === userId || entryStr === "*";
45
- });
40
+ export function isOwner(userId: string): boolean {
41
+ const ownerUserId = process.env.owner_claw_user_id || "";
42
+ if (!ownerUserId) return false;
43
+ return ownerUserId === userId;
46
44
  }
package/index.ts CHANGED
@@ -23,7 +23,7 @@ export default {
23
23
  // 钩子 1: before_prompt_build — 用 memory_search 检索相关记忆注入上下文
24
24
  // ====================================================================
25
25
  api.on("before_prompt_build", async (event: any, ctx: any) => {
26
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
26
+ const identity = resolveSessionIdentity(ctx.sessionKey);
27
27
  const wsDir = ctx.workspaceDir || workspaceDir;
28
28
 
29
29
  // 从 event.messages 提取用户最新消息作为搜索 query
@@ -40,37 +40,35 @@ export default {
40
40
 
41
41
  let memoryBlock = "";
42
42
 
43
- if (query) {
44
- try {
45
- // 用内置 memory_search 搜索相关记忆
46
- const searchTool = api.runtime.tools.createMemorySearchTool({
47
- config: ctx.config || config,
48
- agentSessionKey: ctx.sessionKey,
49
- });
50
-
51
- if (searchTool) {
52
- const rawResults = await searchTool.execute("prompt-build-search", { query });
53
- // 按身份权限过滤结果
54
- const filtered = filterResultsByIdentity(rawResults, identity, wsDir);
55
- const details = filtered?.details;
56
- const results = Array.isArray(details?.results) ? details.results : [];
57
- const snippets = results
58
- .map((r: any) => r.snippet || "")
59
- .filter((s: string) => s.length > 0);
60
-
61
- if (snippets.length > 0) {
62
- memoryBlock = `\n\n${snippets.join("\n\n")}`;
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
63
  }
64
- } else {
65
- // memory_search 不可用,回退到全量加载
66
- const fallback = loadAllowedMemoryContent(wsDir, identity);
67
- if (fallback) memoryBlock = `\n\n${fallback}`;
64
+ } catch {
65
+ // owner 搜索失败不回退
68
66
  }
69
- } catch {
70
- // 搜索失败,回退到全量加载
71
- const fallback = loadAllowedMemoryContent(wsDir, identity);
72
- if (fallback) memoryBlock = `\n\n${fallback}`;
73
67
  }
68
+ } else {
69
+ // 非 owner(peer / group):直接全量加载已过滤的记忆,不依赖 query
70
+ const fallback = loadAllowedMemoryContent(wsDir, identity);
71
+ if (fallback) memoryBlock = `\n\n${fallback}`;
74
72
  }
75
73
 
76
74
  switch (identity.type) {
@@ -102,7 +100,7 @@ export default {
102
100
  // ====================================================================
103
101
  api.registerTool(
104
102
  (ctx: any) => {
105
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
103
+ const identity = resolveSessionIdentity(ctx.sessionKey);
106
104
  const wsDir = ctx.workspaceDir || workspaceDir;
107
105
 
108
106
  const originalSearchTool = api.runtime.tools.createMemorySearchTool({
@@ -176,7 +174,7 @@ export default {
176
174
  const filePath = (event.params?.path || event.params?.filePath || event.params?.file_path || "") as string;
177
175
  if (!filePath) return;
178
176
 
179
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
177
+ const identity = resolveSessionIdentity(ctx.sessionKey);
180
178
  const lowerFilePath = filePath.toLowerCase();
181
179
 
182
180
  // --- MEMORY.md 写入保护(非 owner 阻止)---
@@ -187,12 +185,15 @@ export default {
187
185
  return; // owner 可以正常写 MEMORY.md
188
186
  }
189
187
 
190
- // --- USER.md 写入保护(group session 阻止)---
191
- if (lowerFilePath.endsWith("/user.md") || lowerFilePath.endsWith("\\user.md") || lowerFilePath === "user.md") {
192
- if (identity.type === "group") {
188
+ // --- 系统配置文件写入保护(非 owner 一律阻止)---
189
+ // USER.md / SOUL.md / TOOL.md 等只有 owner 能写,peer/group 只能读
190
+ const systemFiles = ["/user.md", "/soul.md", "/tool.md"];
191
+ const isSystemFile = systemFiles.some(f => lowerFilePath.endsWith(f) || lowerFilePath === f.slice(1));
192
+ if (isSystemFile) {
193
+ if (identity.type !== "owner") {
193
194
  return { block: true, blockReason: "Access denied." };
194
195
  }
195
- return; // owner / peer 可以写 USER.md
196
+ return;
196
197
  }
197
198
 
198
199
  // --- 以下只处理 memory/ 目录下的写入 ---
@@ -247,7 +248,7 @@ export default {
247
248
  if (!filePath) return;
248
249
 
249
250
  const wsDir = ctx.workspaceDir || workspaceDir;
250
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
251
+ const identity = resolveSessionIdentity(ctx.sessionKey);
251
252
 
252
253
  // 拦截 memory/ 路径的读取
253
254
  if (filePath.includes("/memory/") || filePath.includes("memory/")) {
@@ -280,12 +281,18 @@ export default {
280
281
  api.on("before_tool_call", (event: any, ctx: any) => {
281
282
  if (event.toolName !== "exec" && event.toolName !== "process") return;
282
283
 
283
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
284
+ const identity = resolveSessionIdentity(ctx.sessionKey);
284
285
  if (identity.type === "owner") return;
285
286
 
286
- const command = (event.params?.command || event.params?.cmd || "") as string;
287
+ let command = (event.params?.command || event.params?.cmd || "") as string;
287
288
  if (!command) return;
288
289
 
290
+ // 展开 ~ 为绝对路径,防止用 ~ 绕过路径检测
291
+ const homeDir = process.env.HOME || require("os").homedir();
292
+ if (homeDir) {
293
+ command = command.replace(/(?<=^|[\s;|&"'(=])~/g, homeDir);
294
+ }
295
+
289
296
  // MEMORY.md / USER.md 始终拦截非 owner(大小写不敏感)
290
297
  const lowerCommand = command.toLowerCase();
291
298
  if (lowerCommand.includes("memory.md") || lowerCommand.includes("user.md")) {
@@ -385,7 +392,7 @@ export default {
385
392
  const filePath = (event.params?.path || event.params?.filePath || event.params?.file_path || "") as string;
386
393
  if (!filePath) return;
387
394
 
388
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
395
+ const identity = resolveSessionIdentity(ctx.sessionKey);
389
396
  if (identity.type === "owner") return;
390
397
 
391
398
  // 拦截 MEMORY.md(非 owner 不可读)
@@ -407,11 +414,25 @@ export default {
407
414
  }
408
415
  }, { priority: 10 });
409
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
+
410
431
  // ====================================================================
411
432
  // 钩子 7: agent_end — 将 agent 对话同步到 memory 文件
412
433
  // ====================================================================
413
434
  api.on("agent_end", (event: any, ctx: any) => {
414
- const identity = resolveSessionIdentity(ctx.sessionKey, config);
435
+ const identity = resolveSessionIdentity(ctx.sessionKey);
415
436
  const wsDir = ctx.workspaceDir || workspaceDir;
416
437
  const messages = event.messages || [];
417
438
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-privacy",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "files": [