memory-privacy 1.1.0 → 1.4.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 +7 -9
- package/index.ts +61 -40
- package/package.json +1 -1
- package/utils.ts +45 -47
package/filters.ts
CHANGED
|
@@ -116,10 +116,10 @@ function isPathAllowedRelative(
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (identity.type === "peer") {
|
|
119
|
-
//
|
|
119
|
+
// 私聊中访问群记忆:通过 API 获取成员资格,按时间线过滤
|
|
120
120
|
const fileDate = extractDateFromPath(relPath);
|
|
121
121
|
if (!fileDate) return false;
|
|
122
|
-
return isWithinMembershipPeriod(
|
|
122
|
+
return isWithinMembershipPeriod(groupId, identity.peerId, fileDate);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
return false;
|
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
|
|
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
|
|
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
|
-
*
|
|
38
|
+
* 从环境变量 owner_claw_user_id 获取主人 ID
|
|
39
39
|
*/
|
|
40
|
-
export function isOwner(userId: string
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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 (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
}
|
|
65
|
-
//
|
|
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
|
|
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
|
|
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
|
-
// ---
|
|
191
|
-
|
|
192
|
-
|
|
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;
|
|
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
|
|
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
|
|
284
|
+
const identity = resolveSessionIdentity(ctx.sessionKey);
|
|
284
285
|
if (identity.type === "owner") return;
|
|
285
286
|
|
|
286
|
-
|
|
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
|
|
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
|
|
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
package/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
import type { SessionIdentity } from "./identity.js";
|
|
4
5
|
|
|
5
6
|
// ========== 类型定义 ==========
|
|
@@ -7,17 +8,16 @@ import type { SessionIdentity } from "./identity.js";
|
|
|
7
8
|
export interface MemberPeriod {
|
|
8
9
|
joined: number; // Unix 毫秒时间戳
|
|
9
10
|
left: number | null;
|
|
10
|
-
reason?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface
|
|
14
|
-
|
|
13
|
+
export interface MemberGroupInfo {
|
|
14
|
+
group_id: string;
|
|
15
15
|
periods: MemberPeriod[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export interface MemberApiResponse {
|
|
19
|
+
user_id: string;
|
|
20
|
+
groups: MemberGroupInfo[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface KnowledgeBaseRule {
|
|
@@ -67,22 +67,37 @@ export function extractKnowledgeBaseId(filePath: string): string {
|
|
|
67
67
|
return knowledgeIndex >= 0 && parts[knowledgeIndex + 1] ? parts[knowledgeIndex + 1] : "unknown-kb";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// ==========
|
|
70
|
+
// ========== API 调用 ==========
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* 根据 HELM_ENV 环境变量返回 API base URL
|
|
74
74
|
*/
|
|
75
|
-
export function
|
|
75
|
+
export function getApiBaseUrl(): string {
|
|
76
|
+
const env = (process.env.HELM_ENV || "dev").toLowerCase();
|
|
77
|
+
switch (env) {
|
|
78
|
+
case "prod": return "https://claw-server.csagentai.com";
|
|
79
|
+
case "staging": return "https://claw-server.csjkagent.com";
|
|
80
|
+
default: return "https://claw-server.csaiagent.com";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 同步调用 API 获取用户的群成员资格数据
|
|
86
|
+
* 使用 execSync + curl 实现同步 HTTP 请求
|
|
87
|
+
*/
|
|
88
|
+
export function fetchMemberGroupsSync(userId: string): MemberApiResponse | null {
|
|
76
89
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
90
|
+
const baseUrl = getApiBaseUrl();
|
|
91
|
+
const url = `${baseUrl}/api/members?user_id=${encodeURIComponent(userId)}`;
|
|
92
|
+
const result = execSync(`curl -s --max-time 5 "${url}"`, { encoding: "utf-8" });
|
|
93
|
+
return JSON.parse(result) as MemberApiResponse;
|
|
80
94
|
} catch {
|
|
81
|
-
|
|
95
|
+
return null;
|
|
82
96
|
}
|
|
83
|
-
return null;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
// ========== 文件加载 ==========
|
|
100
|
+
|
|
86
101
|
/**
|
|
87
102
|
* 加载知识库 ACL(带 mtime 缓存)
|
|
88
103
|
*/
|
|
@@ -111,29 +126,25 @@ export function loadKnowledgeACL(workspaceDir: string): KnowledgeACL {
|
|
|
111
126
|
/**
|
|
112
127
|
* 检查某个用户在某个日期是否处于群成员期内
|
|
113
128
|
*
|
|
129
|
+
* 通过外部 API 获取成员资格数据
|
|
114
130
|
* 规则:fileTimestamp >= joined && (left === null || fileTimestamp < left)
|
|
115
131
|
* fileTimestamp = 文件日期 00:00:00 UTC
|
|
116
132
|
*/
|
|
117
133
|
export function isWithinMembershipPeriod(
|
|
118
|
-
workspaceDir: string,
|
|
119
134
|
groupId: string,
|
|
120
135
|
peerId: string,
|
|
121
136
|
fileDate: string // "2026-03-17"
|
|
122
137
|
): boolean {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!registry) return false;
|
|
138
|
+
const response = fetchMemberGroupsSync(peerId);
|
|
139
|
+
if (!response || !response.groups) return false;
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const member = memberKey ? registry.members[memberKey] : undefined;
|
|
132
|
-
if (!member || !member.periods || member.periods.length === 0) return false;
|
|
141
|
+
const lowerGroupId = groupId.toLowerCase();
|
|
142
|
+
const group = response.groups.find(g => g.group_id.toLowerCase() === lowerGroupId);
|
|
143
|
+
if (!group || !group.periods || group.periods.length === 0) return false;
|
|
133
144
|
|
|
134
145
|
const fileTimestamp = new Date(`${fileDate}T00:00:00Z`).getTime();
|
|
135
146
|
|
|
136
|
-
return
|
|
147
|
+
return group.periods.some((period) => {
|
|
137
148
|
const afterJoin = fileTimestamp >= period.joined;
|
|
138
149
|
const beforeLeft = period.left === null || fileTimestamp < period.left;
|
|
139
150
|
return afterJoin && beforeLeft;
|
|
@@ -160,7 +171,7 @@ export function toRelativeMemoryPath(absolutePath: string, workspaceDir: string)
|
|
|
160
171
|
/**
|
|
161
172
|
* 获取某个身份有权限访问的所有 memory 子目录(绝对路径列表)
|
|
162
173
|
*
|
|
163
|
-
* peer: peers/{peerId}/ + _acl.json 中 userids 包含 peerId 的知识库 +
|
|
174
|
+
* peer: peers/{peerId}/ + _acl.json 中 userids 包含 peerId 的知识库 + API 返回当前在群的群组
|
|
164
175
|
* group: groups/{groupId}/ + _acl.json 中 allowGroups 包含 groupId 的知识库
|
|
165
176
|
* owner: memory/ 整个目录
|
|
166
177
|
*/
|
|
@@ -191,29 +202,16 @@ export function getAllowedDirs(wsDir: string, identity: SessionIdentity): string
|
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
// 3.
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const membersPath = path.join(groupPath, "_members.json");
|
|
202
|
-
const registry = loadMembersRegistry(membersPath);
|
|
203
|
-
if (!registry) continue;
|
|
204
|
-
|
|
205
|
-
const memberKey = Object.keys(registry.members)
|
|
206
|
-
.find(k => k.toLowerCase() === lowerPeerId);
|
|
207
|
-
if (!memberKey) continue;
|
|
208
|
-
|
|
209
|
-
const member = registry.members[memberKey];
|
|
210
|
-
const lastPeriod = member.periods?.[member.periods.length - 1];
|
|
211
|
-
if (lastPeriod && lastPeriod.left === null) {
|
|
212
|
-
dirs.push(groupPath);
|
|
213
|
-
}
|
|
205
|
+
// 3. 群组:通过 API 获取当前在群的群组(last period.left === null)
|
|
206
|
+
const response = fetchMemberGroupsSync(identity.peerId);
|
|
207
|
+
if (response && response.groups) {
|
|
208
|
+
for (const group of response.groups) {
|
|
209
|
+
const lastPeriod = group.periods?.[group.periods.length - 1];
|
|
210
|
+
if (lastPeriod && lastPeriod.left === null) {
|
|
211
|
+
dirs.push(path.join(memDir, "groups", group.group_id));
|
|
214
212
|
}
|
|
215
213
|
}
|
|
216
|
-
}
|
|
214
|
+
}
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
if (identity.type === "group") {
|