memory-privacy 1.7.0 → 1.8.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 (6) hide show
  1. package/filters.ts +108 -95
  2. package/identity.ts +69 -29
  3. package/index.ts +350 -265
  4. package/package.json +1 -1
  5. package/route.ts +182 -0
  6. package/utils.ts +94 -132
package/filters.ts CHANGED
@@ -1,57 +1,75 @@
1
1
  import type { SessionIdentity } from "./identity";
2
2
  import {
3
- extractGroupIdFromPath,
4
3
  extractDateFromPath,
4
+ extractGroupIdFromPath,
5
5
  extractKnowledgeBaseId,
6
6
  isWithinMembershipPeriod,
7
7
  loadKnowledgeACL,
8
8
  toRelativeMemoryPath,
9
9
  } from "./utils";
10
10
 
11
- /**
12
- * 过滤 memory_search 结果,按身份隔离
13
- */
11
+ type SearchResultEntry = Record<string, unknown> & {
12
+ path?: string;
13
+ filePath?: string;
14
+ };
15
+
16
+ type SearchContentEntry = Record<string, unknown> & {
17
+ type?: string;
18
+ text?: string;
19
+ };
20
+
21
+ type SearchDetails = Record<string, unknown> & {
22
+ results?: SearchResultEntry[];
23
+ };
24
+
25
+ type SearchToolResult = Record<string, unknown> & {
26
+ content?: SearchContentEntry[];
27
+ details?: SearchDetails | SearchResultEntry[];
28
+ };
29
+
30
+ function normalizeRelativePath(relPath: string): string {
31
+ return relPath.replace(/\\/g, "/");
32
+ }
33
+
14
34
  export function filterResultsByIdentity(
15
- results: any,
35
+ results: SearchToolResult | null | undefined,
16
36
  identity: SessionIdentity,
17
- workspaceDir: string
18
- ): any {
19
- // results 是 AgentToolResult: { content, details }
20
- // details 中通常包含 results 数组,每项有 path 字段
21
- if (!results || !results.details) return results;
37
+ workspaceDir: string,
38
+ ): SearchToolResult | null | undefined {
39
+ if (!results?.details) {
40
+ return results;
41
+ }
22
42
 
23
43
  const details = results.details;
24
-
25
- // memory_search 返回的 details 可能有 results 数组
26
- if (Array.isArray(details.results)) {
27
- const filtered = details.results.filter((r: any) => {
28
- const rawPath: string = r.path || r.filePath || "";
44
+ if (Array.isArray((details as SearchDetails).results)) {
45
+ const filtered = ((details as SearchDetails).results ?? []).filter((result) => {
46
+ const rawPath = result.path || result.filePath || "";
29
47
  const relPath = toRelativeMemoryPath(rawPath, workspaceDir);
30
48
  return isPathAllowedRelative(relPath, identity, workspaceDir);
31
49
  });
32
50
 
51
+ const content = Array.isArray(results.content)
52
+ ? results.content.map((entry) => {
53
+ if (entry.type === "text" && typeof entry.text === "string") {
54
+ return {
55
+ ...entry,
56
+ text: filtered.length > 0 ? `Found ${filtered.length} result(s).` : "No results found.",
57
+ };
58
+ }
59
+ return entry;
60
+ })
61
+ : results.content;
62
+
33
63
  return {
34
64
  ...results,
35
- content: results.content.map((c: any) => {
36
- if (c.type === "text" && typeof c.text === "string") {
37
- // Rewrite the text content to reflect filtered results
38
- return {
39
- ...c,
40
- text: filtered.length > 0
41
- ? `Found ${filtered.length} result(s).`
42
- : "No results found.",
43
- };
44
- }
45
- return c;
46
- }),
47
- details: { ...details, results: filtered },
65
+ content,
66
+ details: { ...(details as SearchDetails), results: filtered },
48
67
  };
49
68
  }
50
69
 
51
- // If results is an array directly
52
70
  if (Array.isArray(details)) {
53
- const filtered = details.filter((r: any) => {
54
- const rawPath: string = r.path || r.filePath || "";
71
+ const filtered = details.filter((result) => {
72
+ const rawPath = result.path || result.filePath || "";
55
73
  const relPath = toRelativeMemoryPath(rawPath, workspaceDir);
56
74
  return isPathAllowedRelative(relPath, identity, workspaceDir);
57
75
  });
@@ -65,140 +83,135 @@ export function filterResultsByIdentity(
65
83
  return results;
66
84
  }
67
85
 
68
- /**
69
- * 检查相对路径是否允许访问(用于 memory_search 过滤和 memory_get 检查)
70
- */
71
86
  function isPathAllowedRelative(
72
87
  relPath: string,
73
88
  identity: SessionIdentity,
74
- workspaceDir: string
89
+ workspaceDir: string,
75
90
  ): boolean {
76
- // 元数据文件永远不返回(包括 owner)
77
- if (relPath.endsWith("/_members.json") || relPath.endsWith("/_acl.json")) {
91
+ const normalizedPath = normalizeRelativePath(relPath);
92
+
93
+ if (normalizedPath.endsWith("/_members.json") || normalizedPath.endsWith("/_acl.json")) {
78
94
  return false;
79
95
  }
80
96
 
81
- // owner 可以访问 memory/ 下的所有文件(元数据除外,已在上面拦截)
82
97
  if (identity.type === "owner") {
83
- return relPath.startsWith("memory/");
98
+ return normalizedPath.startsWith("memory/");
84
99
  }
85
100
 
86
- // 以下为非 owner(peer / group)的权限判定
87
-
88
- // owner/ 目录 — 非 owner 一律拒绝
89
- if (relPath.startsWith("memory/owner/")) {
101
+ if (normalizedPath.startsWith("memory/owner/")) {
90
102
  return false;
91
103
  }
92
104
 
93
- // memory/ 根目录下的散落文件(如 memory/2026-03-19.md)— owner 拒绝
94
- // 合法文件应在 peers/groups/knowledge 子目录中
95
- const thirdSegment = relPath.split("/")[1] || "";
96
- if (thirdSegment && !["owner", "peers", "groups", "knowledge"].includes(thirdSegment)) {
105
+ const rootSection = normalizedPath.split("/")[1] || "";
106
+ if (rootSection && !["owner", "peers", "groups", "knowledge"].includes(rootSection)) {
97
107
  return false;
98
108
  }
99
109
 
100
- // peers/ 目录
101
- if (relPath.startsWith("memory/peers/")) {
102
- if (identity.type === "peer") {
103
- // OpenClaw 会把 sessionKey 转小写,但目录名保留原始大小写,需忽略大小写
104
- const lowerPath = relPath.toLowerCase();
105
- return lowerPath.startsWith(`memory/peers/${identity.peerId.toLowerCase()}/`);
110
+ if (normalizedPath.startsWith("memory/peers/")) {
111
+ if (identity.type !== "peer") {
112
+ return false;
106
113
  }
107
- return false; // group session 不能访问 peers/
114
+ return normalizedPath
115
+ .toLowerCase()
116
+ .startsWith(`memory/peers/${identity.peerId.toLowerCase()}/`);
108
117
  }
109
118
 
110
- // groups/ 目录
111
- if (relPath.startsWith("memory/groups/")) {
112
- const groupId = extractGroupIdFromPath(relPath);
119
+ if (normalizedPath.startsWith("memory/groups/")) {
120
+ const groupId = extractGroupIdFromPath(normalizedPath);
113
121
 
114
122
  if (identity.type === "group") {
115
123
  return groupId.toLowerCase() === identity.groupId.toLowerCase();
116
124
  }
117
125
 
118
126
  if (identity.type === "peer") {
119
- // 私聊中访问群记忆:通过 API 获取成员资格,按时间线过滤
120
- const fileDate = extractDateFromPath(relPath);
121
- if (!fileDate) return false;
127
+ const fileDate = extractDateFromPath(normalizedPath);
128
+ if (!fileDate) {
129
+ return false;
130
+ }
122
131
  return isWithinMembershipPeriod(groupId, identity.peerId, fileDate);
123
132
  }
124
133
 
125
134
  return false;
126
135
  }
127
136
 
128
- // knowledge/ 目录
129
- if (relPath.startsWith("memory/knowledge/")) {
130
- return isKnowledgeAccessAllowed(relPath, identity, workspaceDir);
137
+ if (normalizedPath.startsWith("memory/knowledge/")) {
138
+ return isKnowledgeAccessAllowed(normalizedPath, identity, workspaceDir);
131
139
  }
132
140
 
133
- // 未知路径,默认拒绝
134
141
  return false;
135
142
  }
136
143
 
137
- /**
138
- * 检查路径是否允许访问(支持绝对路径和相对路径)
139
- */
140
144
  export function isPathAllowed(
141
145
  filePath: string,
142
146
  identity: SessionIdentity,
143
- workspaceDir: string
147
+ workspaceDir: string,
144
148
  ): boolean {
145
149
  const relPath = toRelativeMemoryPath(filePath, workspaceDir);
146
150
  return isPathAllowedRelative(relPath, identity, workspaceDir);
147
151
  }
148
152
 
149
- /**
150
- * 检查知识库是否允许访问
151
- */
152
153
  function isKnowledgeAccessAllowed(
153
154
  relPath: string,
154
155
  identity: SessionIdentity,
155
- workspaceDir: string
156
+ workspaceDir: string,
156
157
  ): boolean {
157
158
  const acl = loadKnowledgeACL(workspaceDir);
158
159
  const kbId = extractKnowledgeBaseId(relPath);
159
160
  const kbAcl = acl[kbId];
160
161
 
161
- if (!kbAcl) return false;
162
- if (kbAcl.rules.public) return true;
162
+ if (!kbAcl) {
163
+ return false;
164
+ }
165
+ if (kbAcl.rules.public) {
166
+ return true;
167
+ }
163
168
 
164
169
  switch (identity.type) {
165
- case "owner": return true;
170
+ case "owner":
171
+ return true;
166
172
  case "peer": {
167
- const lp = identity.peerId.toLowerCase();
173
+ const lowerPeerId = identity.peerId.toLowerCase();
168
174
  return (
169
- kbAcl.rules.userids?.some((u: string) => u.toLowerCase() === lp) ||
170
- kbAcl.rules.allowUsers?.some((u: string) => u.toLowerCase() === lp)
175
+ kbAcl.rules.userids?.some((userId) => userId.toLowerCase() === lowerPeerId) ||
176
+ kbAcl.rules.allowUsers?.some((userId) => userId.toLowerCase() === lowerPeerId)
171
177
  ) ?? false;
172
178
  }
173
- case "group": return kbAcl.rules.allowGroups?.some((g: string) => g.toLowerCase() === identity.groupId.toLowerCase()) ?? false;
174
- default: return false;
179
+ case "group":
180
+ return (
181
+ kbAcl.rules.allowGroups?.some(
182
+ (groupId) => groupId.toLowerCase() === identity.groupId.toLowerCase(),
183
+ ) ?? false
184
+ );
185
+ default:
186
+ return false;
175
187
  }
176
188
  }
177
189
 
178
- /**
179
- * 根据身份获取写入目标目录(相对路径)
180
- */
181
190
  export function getTargetDir(identity: SessionIdentity): string {
182
191
  switch (identity.type) {
183
- case "owner": return "memory/owner/";
184
- case "peer": return `memory/peers/${identity.peerId}/`;
185
- case "group": return `memory/groups/${identity.groupId}/`;
192
+ case "owner":
193
+ return "memory/owner/";
194
+ case "peer":
195
+ return `memory/peers/${identity.peerId}/`;
196
+ case "group":
197
+ return `memory/groups/${identity.groupId}/`;
186
198
  }
187
199
  }
188
200
 
189
- /**
190
- * 检查写入路径是否已经在正确目录中(支持绝对路径和相对路径)
191
- */
192
201
  export function isWritePathCorrect(filePath: string, identity: SessionIdentity): boolean {
193
- const lowerPath = filePath.toLowerCase();
202
+ const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/");
194
203
  switch (identity.type) {
195
204
  case "owner":
196
- return lowerPath.includes("/memory/owner/") || lowerPath.startsWith("memory/owner/");
205
+ return normalizedPath.includes("/memory/owner/") || normalizedPath.startsWith("memory/owner/");
197
206
  case "peer":
198
- return lowerPath.includes(`/memory/peers/${identity.peerId.toLowerCase()}/`) ||
199
- lowerPath.startsWith(`memory/peers/${identity.peerId.toLowerCase()}/`);
207
+ return (
208
+ normalizedPath.includes(`/memory/peers/${identity.peerId.toLowerCase()}/`) ||
209
+ normalizedPath.startsWith(`memory/peers/${identity.peerId.toLowerCase()}/`)
210
+ );
200
211
  case "group":
201
- return lowerPath.includes(`/memory/groups/${identity.groupId.toLowerCase()}/`) ||
202
- lowerPath.startsWith(`memory/groups/${identity.groupId.toLowerCase()}/`);
212
+ return (
213
+ normalizedPath.includes(`/memory/groups/${identity.groupId.toLowerCase()}/`) ||
214
+ normalizedPath.startsWith(`memory/groups/${identity.groupId.toLowerCase()}/`)
215
+ );
203
216
  }
204
217
  }
package/identity.ts CHANGED
@@ -1,47 +1,87 @@
1
+ import { normalizeAgentId, parseAgentSessionKey, parsePalzSessionTail } from "./route";
2
+
1
3
  export type SessionIdentity =
2
- | { type: "owner"; userId: string }
3
- | { type: "peer"; peerId: string }
4
- | { type: "group"; groupId: string };
4
+ | { type: "owner"; agentId: string; userId: string; lobsterId?: string; releaseName?: string }
5
+ | { type: "peer"; agentId: string; peerId: string; lobsterId?: string; releaseName?: string }
6
+ | {
7
+ type: "group";
8
+ agentId: string;
9
+ groupId: string;
10
+ userId?: string;
11
+ lobsterId?: string;
12
+ releaseName?: string;
13
+ };
14
+
15
+ export interface SessionIdentityParams {
16
+ sessionKey?: string;
17
+ agentId?: string;
18
+ }
5
19
 
6
20
  /**
7
- * sessionKey 解析会话身份
21
+ * Resolve identity for palz session keys.
8
22
  *
9
- * sessionKey 格式(最后一段用 : 分隔的是会话ID):
10
- * 单聊: agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_release_{releaseName}
11
- * 群聊: agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_group_{groupID}_release_{releaseName}
23
+ * Session key examples:
24
+ * - direct:
25
+ * agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_release_{releaseName}
26
+ * - group:
27
+ * agent:{agentId}:palz:direct:{userId}:user_{userID}_lobster_{lobsterID}_group_{groupID}_release_{releaseName}
12
28
  */
13
- export function resolveSessionIdentity(sessionKey: string | undefined): SessionIdentity {
14
- const sk = sessionKey ?? "";
15
- // 取最后一段(会话ID)
16
- const sessionPart = sk.split(":").pop() || "";
17
-
18
- // 1. 群聊:包含 _group_
19
- const groupMatch = sessionPart.match(/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/);
20
- if (groupMatch) {
21
- return { type: "group", groupId: groupMatch[3] };
29
+ export function resolveSessionIdentity(params: SessionIdentityParams): SessionIdentity {
30
+ const parsedSession = parseAgentSessionKey(params.sessionKey);
31
+ const parsedTail = parsePalzSessionTail(params.sessionKey);
32
+ const agentId = normalizeAgentId(params.agentId) ?? parsedSession?.agentId ?? "main";
33
+
34
+ if (parsedTail.groupId) {
35
+ return {
36
+ type: "group",
37
+ agentId,
38
+ groupId: parsedTail.groupId,
39
+ userId: parsedTail.userId,
40
+ lobsterId: parsedTail.lobsterId,
41
+ releaseName: parsedTail.releaseName,
42
+ };
22
43
  }
23
44
 
24
- // 2. 私聊
25
- const peerMatch = sessionPart.match(/user_(.+?)_lobster_(.+?)_release_(.+)$/);
26
- if (peerMatch) {
27
- const userID = peerMatch[1];
28
- if (isOwner(userID)) return { type: "owner", userId: userID };
29
- return { type: "peer", peerId: userID };
45
+ if (parsedTail.userId) {
46
+ if (isOwner(parsedTail.userId)) {
47
+ return {
48
+ type: "owner",
49
+ agentId,
50
+ userId: parsedTail.userId,
51
+ lobsterId: parsedTail.lobsterId,
52
+ releaseName: parsedTail.releaseName,
53
+ };
54
+ }
55
+
56
+ return {
57
+ type: "peer",
58
+ agentId,
59
+ peerId: parsedTail.userId,
60
+ lobsterId: parsedTail.lobsterId,
61
+ releaseName: parsedTail.releaseName,
62
+ };
30
63
  }
31
64
 
32
- // 3. 兜底:未知 session peer 处理(安全优先)
33
- return { type: "peer", peerId: "unknown" };
65
+ return { type: "peer", agentId, peerId: "unknown" };
34
66
  }
35
67
 
36
68
  /**
37
- * 判断 userId 是否是主人
38
- * 从环境变量 OPENCLAW_GATEWAY_TOKEN 获取并解析主人 ID
39
- * token 格式如 "oc-user-f76e8c98afb148539963c1b726f2afac",用 "-" 切分取最后一段作为 owner ID
69
+ * Resolve whether a user id belongs to the owner.
70
+ *
71
+ * The current deployment stores the owner user id inside OPENCLAW_GATEWAY_TOKEN.
72
+ * Example token format:
73
+ * oc-user-f76e8c98afb148539963c1b726f2afac
40
74
  */
41
75
  export function isOwner(userId: string): boolean {
42
76
  const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
43
- if (!token) return false;
77
+ if (!token) {
78
+ return false;
79
+ }
80
+
44
81
  const ownerUserId = token.split("-").pop() || "";
45
- if (!ownerUserId) return false;
82
+ if (!ownerUserId) {
83
+ return false;
84
+ }
85
+
46
86
  return ownerUserId === userId;
47
87
  }