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.
- package/filters.ts +108 -95
- package/identity.ts +69 -29
- package/index.ts +350 -265
- package/package.json +1 -1
- package/route.ts +182 -0
- 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
|
-
|
|
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:
|
|
35
|
+
results: SearchToolResult | null | undefined,
|
|
16
36
|
identity: SessionIdentity,
|
|
17
|
-
workspaceDir: string
|
|
18
|
-
):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
36
|
-
|
|
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((
|
|
54
|
-
const rawPath
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
98
|
+
return normalizedPath.startsWith("memory/");
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
114
|
+
return normalizedPath
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.startsWith(`memory/peers/${identity.peerId.toLowerCase()}/`);
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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)
|
|
162
|
-
|
|
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":
|
|
170
|
+
case "owner":
|
|
171
|
+
return true;
|
|
166
172
|
case "peer": {
|
|
167
|
-
const
|
|
173
|
+
const lowerPeerId = identity.peerId.toLowerCase();
|
|
168
174
|
return (
|
|
169
|
-
kbAcl.rules.userids?.some((
|
|
170
|
-
kbAcl.rules.allowUsers?.some((
|
|
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":
|
|
174
|
-
|
|
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":
|
|
184
|
-
|
|
185
|
-
case "
|
|
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
|
|
202
|
+
const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/");
|
|
194
203
|
switch (identity.type) {
|
|
195
204
|
case "owner":
|
|
196
|
-
return
|
|
205
|
+
return normalizedPath.includes("/memory/owner/") || normalizedPath.startsWith("memory/owner/");
|
|
197
206
|
case "peer":
|
|
198
|
-
return
|
|
199
|
-
|
|
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
|
|
202
|
-
|
|
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
|
-
| {
|
|
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
|
-
*
|
|
21
|
+
* Resolve identity for palz session keys.
|
|
8
22
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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(
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
return { type: "peer", peerId: "unknown" };
|
|
65
|
+
return { type: "peer", agentId, peerId: "unknown" };
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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)
|
|
77
|
+
if (!token) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
const ownerUserId = token.split("-").pop() || "";
|
|
45
|
-
if (!ownerUserId)
|
|
82
|
+
if (!ownerUserId) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
46
86
|
return ownerUserId === userId;
|
|
47
87
|
}
|