@zshuangmu/agenthub 0.4.14 → 0.4.16
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/LICENSE +21 -21
- package/README.md +268 -268
- package/package.json +41 -41
- package/src/api-server.js +518 -244
- package/src/cli.js +714 -671
- package/src/commands/api.js +9 -9
- package/src/commands/doctor.js +335 -335
- package/src/commands/info.js +15 -15
- package/src/commands/install.js +56 -56
- package/src/commands/list.js +78 -78
- package/src/commands/pack.js +249 -156
- package/src/commands/publish-remote.js +9 -9
- package/src/commands/publish.js +7 -7
- package/src/commands/rollback.js +59 -59
- package/src/commands/search.js +14 -14
- package/src/commands/serve.js +9 -9
- package/src/commands/stats.js +105 -105
- package/src/commands/uninstall.js +76 -76
- package/src/commands/update.js +54 -54
- package/src/commands/verify.js +133 -133
- package/src/commands/versions.js +75 -75
- package/src/commands/web.js +9 -9
- package/src/index.js +18 -18
- package/src/lib/auth.js +301 -0
- package/src/lib/bundle-transfer.js +58 -58
- package/src/lib/colors.js +60 -60
- package/src/lib/database.js +450 -244
- package/src/lib/debug.js +135 -135
- package/src/lib/fs-utils.js +107 -50
- package/src/lib/html.js +2163 -1824
- package/src/lib/http.js +168 -168
- package/src/lib/install.js +60 -60
- package/src/lib/manifest.js +124 -124
- package/src/lib/openclaw-config.js +40 -40
- package/src/lib/permissions.js +105 -0
- package/src/lib/privacy-engine.js +220 -0
- package/src/lib/registry.js +130 -130
- package/src/lib/remote.js +11 -11
- package/src/lib/security-scanner.js +233 -233
- package/src/lib/signing.js +158 -0
- package/src/lib/version-manager.js +77 -77
- package/src/server.js +176 -176
- package/src/web-server.js +135 -135
package/src/lib/manifest.js
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MANIFEST Generator
|
|
3
|
-
* 根据 PRD v1.1 规范生成完整的 MANIFEST.json
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const WORKSPACE_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
|
|
7
|
-
|
|
8
|
-
export function createManifest({ slug, name, description, author, memoryCounts, openclawTemplate, skills = [], prompts = [], tags = [], category, language = "zh-CN", version = "1.0.0", featured = false }) {
|
|
9
|
-
const hasModel = openclawTemplate?.agents?.defaults?.model?.primary;
|
|
10
|
-
const totalMemory = memoryCounts.public + memoryCounts.portable + memoryCounts.private;
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
// 基本信息
|
|
14
|
-
name: name || slug,
|
|
15
|
-
slug,
|
|
16
|
-
version,
|
|
17
|
-
description: description || `OpenClaw agent bundle for ${name || slug}`,
|
|
18
|
-
author: author || "anonymous",
|
|
19
|
-
icon: undefined,
|
|
20
|
-
bundleVersion: "1.0",
|
|
21
|
-
|
|
22
|
-
// 运行时
|
|
23
|
-
runtime: {
|
|
24
|
-
type: "openclaw",
|
|
25
|
-
version: ">=0.5.0",
|
|
26
|
-
compatibility: "openclaw-only",
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
// 性格
|
|
30
|
-
persona: {
|
|
31
|
-
summary: `Imported from OpenClaw workspace: ${name || slug}`,
|
|
32
|
-
traits: [],
|
|
33
|
-
expertise: [],
|
|
34
|
-
communication_style: undefined,
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
// 包含内容
|
|
38
|
-
includes: {
|
|
39
|
-
memory: {
|
|
40
|
-
count: totalMemory,
|
|
41
|
-
public: memoryCounts.public,
|
|
42
|
-
portable: memoryCounts.portable,
|
|
43
|
-
private: memoryCounts.private,
|
|
44
|
-
},
|
|
45
|
-
workspaceFiles: WORKSPACE_FILES,
|
|
46
|
-
skills: skills,
|
|
47
|
-
prompts: prompts.length,
|
|
48
|
-
configTemplates: ["OPENCLAW.template.json"],
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// 依赖
|
|
52
|
-
requirements: {
|
|
53
|
-
env: [],
|
|
54
|
-
model: hasModel ?? undefined,
|
|
55
|
-
openclaw: ">=0.5.0",
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
// 分享策略
|
|
59
|
-
sharingPolicy: {
|
|
60
|
-
memoryMode: "layered",
|
|
61
|
-
allowedMemoryLayers: ["public", "portable"],
|
|
62
|
-
blockedMemoryLayers: ["private"],
|
|
63
|
-
openclawConfigMode: "template-only",
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
// 元数据
|
|
67
|
-
metadata: {
|
|
68
|
-
tags: tags.length > 0 ? tags : ["openclaw"],
|
|
69
|
-
category: category || "General",
|
|
70
|
-
language: language,
|
|
71
|
-
license: "MIT",
|
|
72
|
-
featured,
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 验证 MANIFEST 完整性
|
|
79
|
-
*/
|
|
80
|
-
export function validateManifest(manifest) {
|
|
81
|
-
const errors = [];
|
|
82
|
-
const warnings = [];
|
|
83
|
-
|
|
84
|
-
// 必需字段检查
|
|
85
|
-
if (!manifest.name) errors.push("name is required");
|
|
86
|
-
if (!manifest.slug) errors.push("slug is required");
|
|
87
|
-
if (!manifest.version) errors.push("version is required");
|
|
88
|
-
if (!manifest.description) errors.push("description is required");
|
|
89
|
-
|
|
90
|
-
// 运行时检查
|
|
91
|
-
if (manifest.runtime?.type !== "openclaw") {
|
|
92
|
-
errors.push("runtime.type must be 'openclaw'");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 私有记忆检查
|
|
96
|
-
if (manifest.includes?.memory?.private > 0) {
|
|
97
|
-
warnings.push("Bundle contains private memory, cannot be published publicly");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
valid: errors.length === 0,
|
|
102
|
-
canPublish: errors.length === 0 && manifest.includes?.memory?.private === 0,
|
|
103
|
-
errors,
|
|
104
|
-
warnings,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 生成 Bundle ID
|
|
110
|
-
*/
|
|
111
|
-
export function generateBundleId(slug, version) {
|
|
112
|
-
return `agenthub://${slug}@${version}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 解析 Bundle ID
|
|
117
|
-
*/
|
|
118
|
-
export function parseBundleId(bundleId) {
|
|
119
|
-
const match = bundleId.match(/^agenthub:\/\/([^@]+)@(.+)$/);
|
|
120
|
-
if (!match) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
return { slug: match[1], version: match[2] };
|
|
124
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MANIFEST Generator
|
|
3
|
+
* 根据 PRD v1.1 规范生成完整的 MANIFEST.json
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
|
|
7
|
+
|
|
8
|
+
export function createManifest({ slug, name, description, author, memoryCounts, openclawTemplate, skills = [], prompts = [], tags = [], category, language = "zh-CN", version = "1.0.0", featured = false }) {
|
|
9
|
+
const hasModel = openclawTemplate?.agents?.defaults?.model?.primary;
|
|
10
|
+
const totalMemory = memoryCounts.public + memoryCounts.portable + memoryCounts.private;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
// 基本信息
|
|
14
|
+
name: name || slug,
|
|
15
|
+
slug,
|
|
16
|
+
version,
|
|
17
|
+
description: description || `OpenClaw agent bundle for ${name || slug}`,
|
|
18
|
+
author: author || "anonymous",
|
|
19
|
+
icon: undefined,
|
|
20
|
+
bundleVersion: "1.0",
|
|
21
|
+
|
|
22
|
+
// 运行时
|
|
23
|
+
runtime: {
|
|
24
|
+
type: "openclaw",
|
|
25
|
+
version: ">=0.5.0",
|
|
26
|
+
compatibility: "openclaw-only",
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// 性格
|
|
30
|
+
persona: {
|
|
31
|
+
summary: `Imported from OpenClaw workspace: ${name || slug}`,
|
|
32
|
+
traits: [],
|
|
33
|
+
expertise: [],
|
|
34
|
+
communication_style: undefined,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// 包含内容
|
|
38
|
+
includes: {
|
|
39
|
+
memory: {
|
|
40
|
+
count: totalMemory,
|
|
41
|
+
public: memoryCounts.public,
|
|
42
|
+
portable: memoryCounts.portable,
|
|
43
|
+
private: memoryCounts.private,
|
|
44
|
+
},
|
|
45
|
+
workspaceFiles: WORKSPACE_FILES,
|
|
46
|
+
skills: skills,
|
|
47
|
+
prompts: prompts.length,
|
|
48
|
+
configTemplates: ["OPENCLAW.template.json"],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// 依赖
|
|
52
|
+
requirements: {
|
|
53
|
+
env: [],
|
|
54
|
+
model: hasModel ?? undefined,
|
|
55
|
+
openclaw: ">=0.5.0",
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// 分享策略
|
|
59
|
+
sharingPolicy: {
|
|
60
|
+
memoryMode: "layered",
|
|
61
|
+
allowedMemoryLayers: ["public", "portable"],
|
|
62
|
+
blockedMemoryLayers: ["private"],
|
|
63
|
+
openclawConfigMode: "template-only",
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 元数据
|
|
67
|
+
metadata: {
|
|
68
|
+
tags: tags.length > 0 ? tags : ["openclaw"],
|
|
69
|
+
category: category || "General",
|
|
70
|
+
language: language,
|
|
71
|
+
license: "MIT",
|
|
72
|
+
featured,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 验证 MANIFEST 完整性
|
|
79
|
+
*/
|
|
80
|
+
export function validateManifest(manifest) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
const warnings = [];
|
|
83
|
+
|
|
84
|
+
// 必需字段检查
|
|
85
|
+
if (!manifest.name) errors.push("name is required");
|
|
86
|
+
if (!manifest.slug) errors.push("slug is required");
|
|
87
|
+
if (!manifest.version) errors.push("version is required");
|
|
88
|
+
if (!manifest.description) errors.push("description is required");
|
|
89
|
+
|
|
90
|
+
// 运行时检查
|
|
91
|
+
if (manifest.runtime?.type !== "openclaw") {
|
|
92
|
+
errors.push("runtime.type must be 'openclaw'");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 私有记忆检查
|
|
96
|
+
if (manifest.includes?.memory?.private > 0) {
|
|
97
|
+
warnings.push("Bundle contains private memory, cannot be published publicly");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
valid: errors.length === 0,
|
|
102
|
+
canPublish: errors.length === 0 && manifest.includes?.memory?.private === 0,
|
|
103
|
+
errors,
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 生成 Bundle ID
|
|
110
|
+
*/
|
|
111
|
+
export function generateBundleId(slug, version) {
|
|
112
|
+
return `agenthub://${slug}@${version}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 解析 Bundle ID
|
|
117
|
+
*/
|
|
118
|
+
export function parseBundleId(bundleId) {
|
|
119
|
+
const match = bundleId.match(/^agenthub:\/\/([^@]+)@(.+)$/);
|
|
120
|
+
if (!match) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return { slug: match[1], version: match[2] };
|
|
124
|
+
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
const ALLOWED_PATHS = [
|
|
2
|
-
["agents", "defaults", "model"],
|
|
3
|
-
["agents", "defaults", "compaction"],
|
|
4
|
-
["agents", "defaults", "memorySearch"],
|
|
5
|
-
["agents", "defaults", "sandbox"],
|
|
6
|
-
["plugins", "slots"],
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
function cloneJson(value) {
|
|
10
|
-
return JSON.parse(JSON.stringify(value));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function setDeep(target, keys, value) {
|
|
14
|
-
let cursor = target;
|
|
15
|
-
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
16
|
-
const key = keys[index];
|
|
17
|
-
cursor[key] ??= {};
|
|
18
|
-
cursor = cursor[key];
|
|
19
|
-
}
|
|
20
|
-
cursor[keys[keys.length - 1]] = cloneJson(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function extractOpenClawTemplate(config) {
|
|
24
|
-
const template = {};
|
|
25
|
-
for (const keys of ALLOWED_PATHS) {
|
|
26
|
-
let cursor = config;
|
|
27
|
-
let found = true;
|
|
28
|
-
for (const key of keys) {
|
|
29
|
-
if (!cursor || !(key in cursor)) {
|
|
30
|
-
found = false;
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
cursor = cursor[key];
|
|
34
|
-
}
|
|
35
|
-
if (found) {
|
|
36
|
-
setDeep(template, keys, cursor);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return template;
|
|
40
|
-
}
|
|
1
|
+
const ALLOWED_PATHS = [
|
|
2
|
+
["agents", "defaults", "model"],
|
|
3
|
+
["agents", "defaults", "compaction"],
|
|
4
|
+
["agents", "defaults", "memorySearch"],
|
|
5
|
+
["agents", "defaults", "sandbox"],
|
|
6
|
+
["plugins", "slots"],
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function cloneJson(value) {
|
|
10
|
+
return JSON.parse(JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setDeep(target, keys, value) {
|
|
14
|
+
let cursor = target;
|
|
15
|
+
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
16
|
+
const key = keys[index];
|
|
17
|
+
cursor[key] ??= {};
|
|
18
|
+
cursor = cursor[key];
|
|
19
|
+
}
|
|
20
|
+
cursor[keys[keys.length - 1]] = cloneJson(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function extractOpenClawTemplate(config) {
|
|
24
|
+
const template = {};
|
|
25
|
+
for (const keys of ALLOWED_PATHS) {
|
|
26
|
+
let cursor = config;
|
|
27
|
+
let found = true;
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (!cursor || !(key in cursor)) {
|
|
30
|
+
found = false;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
cursor = cursor[key];
|
|
34
|
+
}
|
|
35
|
+
if (found) {
|
|
36
|
+
setDeep(template, keys, cursor);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return template;
|
|
40
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions Module
|
|
3
|
+
* Agent 可见性与访问控制
|
|
4
|
+
*
|
|
5
|
+
* 职责:
|
|
6
|
+
* 1. Agent 可见性管理 (public/private/team)
|
|
7
|
+
* 2. 操作权限检查
|
|
8
|
+
* 3. 团队成员授权
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// === Agent 可见性级别 ===
|
|
12
|
+
export const VISIBILITY = {
|
|
13
|
+
PUBLIC: "public",
|
|
14
|
+
PRIVATE: "private",
|
|
15
|
+
TEAM: "team",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// === 操作类型 ===
|
|
19
|
+
export const ACTIONS = {
|
|
20
|
+
VIEW: "view",
|
|
21
|
+
INSTALL: "install",
|
|
22
|
+
PUBLISH: "publish",
|
|
23
|
+
UPDATE: "update",
|
|
24
|
+
DELETE: "delete",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检查用户是否有权访问 Agent
|
|
29
|
+
*
|
|
30
|
+
* @param {object} agent - Agent manifest (含 permissions 字段)
|
|
31
|
+
* @param {object|null} user - 当前用户 (null 表示匿名)
|
|
32
|
+
* @param {string} action - 操作类型
|
|
33
|
+
* @returns {{ allowed: boolean, reason?: string }}
|
|
34
|
+
*/
|
|
35
|
+
export function checkAccess(agent, user, action) {
|
|
36
|
+
const permissions = agent?.permissions || {};
|
|
37
|
+
const visibility = permissions.visibility || VISIBILITY.PUBLIC;
|
|
38
|
+
const owner = permissions.owner || "anonymous";
|
|
39
|
+
|
|
40
|
+
// 公开 Agent: 所有人可查看/安装
|
|
41
|
+
if (visibility === VISIBILITY.PUBLIC) {
|
|
42
|
+
if (action === ACTIONS.VIEW || action === ACTIONS.INSTALL) {
|
|
43
|
+
return { allowed: true };
|
|
44
|
+
}
|
|
45
|
+
// 发布/更新/删除需要是 owner
|
|
46
|
+
if (user && (user.username === owner || user.role === "admin")) {
|
|
47
|
+
return { allowed: true };
|
|
48
|
+
}
|
|
49
|
+
return { allowed: false, reason: `Only owner (${owner}) or admin can ${action}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 匿名用户对非公开 Agent 无权限
|
|
53
|
+
if (!user) {
|
|
54
|
+
return { allowed: false, reason: "Authentication required" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 私有 Agent: 仅 owner 可访问
|
|
58
|
+
if (visibility === VISIBILITY.PRIVATE) {
|
|
59
|
+
if (user.username === owner || user.role === "admin") {
|
|
60
|
+
return { allowed: true };
|
|
61
|
+
}
|
|
62
|
+
return { allowed: false, reason: "This agent is private" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 团队 Agent: owner + 授权列表中的用户
|
|
66
|
+
if (visibility === VISIBILITY.TEAM) {
|
|
67
|
+
const allowedUsers = permissions.allowedUsers || [];
|
|
68
|
+
if (user.username === owner || user.role === "admin" || allowedUsers.includes(user.username)) {
|
|
69
|
+
return { allowed: true };
|
|
70
|
+
}
|
|
71
|
+
return { allowed: false, reason: "You are not a member of this team" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { allowed: false, reason: "Unknown visibility level" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 在 Agent 列表中过滤用户有权查看的 Agent
|
|
79
|
+
*
|
|
80
|
+
* @param {Array} agents - Agent 列表
|
|
81
|
+
* @param {object|null} user - 当前用户
|
|
82
|
+
* @returns {Array} 过滤后的 Agent 列表
|
|
83
|
+
*/
|
|
84
|
+
export function filterVisibleAgents(agents, user) {
|
|
85
|
+
return agents.filter((agent) => {
|
|
86
|
+
const result = checkAccess(agent, user, ACTIONS.VIEW);
|
|
87
|
+
return result.allowed;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 创建默认权限配置
|
|
93
|
+
*
|
|
94
|
+
* @param {string} owner - 发布者用户名
|
|
95
|
+
* @param {string} [visibility] - 可见性级别
|
|
96
|
+
* @returns {object} 权限配置
|
|
97
|
+
*/
|
|
98
|
+
export function createDefaultPermissions(owner = "anonymous", visibility = VISIBILITY.PUBLIC) {
|
|
99
|
+
return {
|
|
100
|
+
visibility,
|
|
101
|
+
owner,
|
|
102
|
+
allowedUsers: [],
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Engine
|
|
3
|
+
* 隐私处理引擎 — AgentHub 核心竞争力模块
|
|
4
|
+
*
|
|
5
|
+
* 职责:
|
|
6
|
+
* 1. 打包前自动剥离私有数据
|
|
7
|
+
* 2. 对文本内容进行脱敏替换
|
|
8
|
+
* 3. 生成隐私合规报告
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
// === 默认排除目录 ===
|
|
15
|
+
const DEFAULT_EXCLUDE_DIRS = [
|
|
16
|
+
"memory/private",
|
|
17
|
+
".git",
|
|
18
|
+
".env",
|
|
19
|
+
"node_modules",
|
|
20
|
+
".vscode",
|
|
21
|
+
".idea",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// === 默认排除文件 ===
|
|
25
|
+
const DEFAULT_EXCLUDE_FILES = [
|
|
26
|
+
".env",
|
|
27
|
+
".env.local",
|
|
28
|
+
".env.production",
|
|
29
|
+
".env.development",
|
|
30
|
+
".npmrc",
|
|
31
|
+
".netrc",
|
|
32
|
+
"id_rsa",
|
|
33
|
+
"id_ed25519",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// === 敏感信息模式(与 security-scanner.js 保持一致,增强版) ===
|
|
37
|
+
const REDACT_PATTERNS = [
|
|
38
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/g, name: "OpenAI API Key", replacement: "sk-***REDACTED***" },
|
|
39
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: "GitHub Token", replacement: "ghp_***REDACTED***" },
|
|
40
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/g, name: "GitHub OAuth", replacement: "gho_***REDACTED***" },
|
|
41
|
+
{ pattern: /ghu_[a-zA-Z0-9]{36}/g, name: "GitHub User Token", replacement: "ghu_***REDACTED***" },
|
|
42
|
+
{ pattern: /ghs_[a-zA-Z0-9]{36}/g, name: "GitHub Server Token", replacement: "ghs_***REDACTED***" },
|
|
43
|
+
{ pattern: /ghr_[a-zA-Z0-9]{36}/g, name: "GitHub Refresh Token", replacement: "ghr_***REDACTED***" },
|
|
44
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9-]+/g, name: "Slack Token", replacement: "xox_-***REDACTED***" },
|
|
45
|
+
{ pattern: /-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END \1?PRIVATE KEY-----/g, name: "Private Key", replacement: "***PRIVATE_KEY_REDACTED***" },
|
|
46
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, name: "JWT Token", replacement: "***JWT_REDACTED***" },
|
|
47
|
+
{ pattern: /(password|passwd|pwd)\s*[=:]\s*['"][^'"]+['"]/gi, name: "Password", replacement: "$1=***REDACTED***" },
|
|
48
|
+
{ pattern: /(secret|api_key|apikey|access_key)\s*[=:]\s*['"][^'"]+['"]/gi, name: "API Secret", replacement: "$1=***REDACTED***" },
|
|
49
|
+
{ pattern: /(token|bearer)\s*[=:]\s*['"][^'"]+['"]/gi, name: "Token", replacement: "$1=***REDACTED***" },
|
|
50
|
+
// 邮箱地址脱敏
|
|
51
|
+
{ pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, name: "Email Address", replacement: "***@***.***" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// 文本文件扩展名
|
|
55
|
+
const TEXT_EXTENSIONS = new Set([
|
|
56
|
+
".md", ".txt", ".json", ".yaml", ".yml", ".xml", ".html", ".css", ".js", ".ts",
|
|
57
|
+
".py", ".sh", ".bash", ".zsh", ".env", ".conf", ".config", ".ini", ".toml",
|
|
58
|
+
".jsx", ".tsx", ".vue", ".svelte", ".astro",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function isTextFile(filename) {
|
|
62
|
+
const ext = path.extname(filename).toLowerCase();
|
|
63
|
+
return TEXT_EXTENSIONS.has(ext) || !ext;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 对文本内容进行脱敏
|
|
68
|
+
* @param {string} content - 原始内容
|
|
69
|
+
* @param {Array} [patterns] - 脱敏模式列表
|
|
70
|
+
* @returns {{ content: string, redactions: Array }} 脱敏后的内容和脱敏记录
|
|
71
|
+
*/
|
|
72
|
+
export function redactContent(content, patterns = REDACT_PATTERNS) {
|
|
73
|
+
const redactions = [];
|
|
74
|
+
let result = content;
|
|
75
|
+
|
|
76
|
+
for (const { pattern, name, replacement } of patterns) {
|
|
77
|
+
// 重建 RegExp 以重置 lastIndex
|
|
78
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
79
|
+
const matches = result.match(regex);
|
|
80
|
+
if (matches) {
|
|
81
|
+
redactions.push({ type: name, count: matches.length });
|
|
82
|
+
result = result.replace(regex, replacement);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { content: result, redactions };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 扫描目录,统计隐私风险
|
|
91
|
+
* @param {string} dirPath - 目录路径
|
|
92
|
+
* @returns {Promise<object>} 扫描报告
|
|
93
|
+
*/
|
|
94
|
+
export async function scanPrivacyRisks(dirPath) {
|
|
95
|
+
const report = {
|
|
96
|
+
hasPrivateMemory: false,
|
|
97
|
+
privateMemoryCount: 0,
|
|
98
|
+
sensitiveFindings: [],
|
|
99
|
+
riskyFiles: [],
|
|
100
|
+
totalFilesScanned: 0,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 检查 memory/private/
|
|
104
|
+
const privatePath = path.join(dirPath, "memory", "private");
|
|
105
|
+
try {
|
|
106
|
+
const privateStat = await stat(privatePath);
|
|
107
|
+
if (privateStat.isDirectory()) {
|
|
108
|
+
const entries = await readdir(privatePath);
|
|
109
|
+
report.hasPrivateMemory = entries.length > 0;
|
|
110
|
+
report.privateMemoryCount = entries.length;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// 不存在,正常
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 递归扫描文本文件
|
|
117
|
+
await scanDirForSensitive(dirPath, report, "");
|
|
118
|
+
|
|
119
|
+
return report;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function scanDirForSensitive(dirPath, report, relativePath) {
|
|
123
|
+
let entries;
|
|
124
|
+
try {
|
|
125
|
+
entries = await readdir(dirPath, { withFileTypes: true });
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
132
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
133
|
+
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
// 跳过不需要扫描的目录
|
|
136
|
+
if (["node_modules", ".git", ".vscode", ".idea"].includes(entry.name)) continue;
|
|
137
|
+
await scanDirForSensitive(fullPath, report, relPath);
|
|
138
|
+
} else if (entry.isFile() && isTextFile(entry.name)) {
|
|
139
|
+
try {
|
|
140
|
+
const content = await readFile(fullPath, "utf8");
|
|
141
|
+
report.totalFilesScanned++;
|
|
142
|
+
|
|
143
|
+
const { redactions } = redactContent(content);
|
|
144
|
+
if (redactions.length > 0) {
|
|
145
|
+
report.sensitiveFindings.push(...redactions.map((r) => ({
|
|
146
|
+
file: relPath,
|
|
147
|
+
type: r.type,
|
|
148
|
+
count: r.count,
|
|
149
|
+
})));
|
|
150
|
+
report.riskyFiles.push(relPath);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// 无法读取,跳过
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 获取打包过滤配置
|
|
161
|
+
* 根据 sharingPolicy 和扫描结果,返回 copyDirFiltered 所需的 options
|
|
162
|
+
*
|
|
163
|
+
* @param {object} manifest - MANIFEST 对象
|
|
164
|
+
* @param {object} [overrides] - 自定义覆盖
|
|
165
|
+
* @returns {object} copyDirFiltered 的 options
|
|
166
|
+
*/
|
|
167
|
+
export function getPackFilterOptions(manifest, overrides = {}) {
|
|
168
|
+
const policy = manifest?.sharingPolicy || {};
|
|
169
|
+
const blockedLayers = policy.blockedMemoryLayers || ["private"];
|
|
170
|
+
|
|
171
|
+
const excludeDirs = [
|
|
172
|
+
...DEFAULT_EXCLUDE_DIRS,
|
|
173
|
+
...blockedLayers.map((layer) => `memory/${layer}`),
|
|
174
|
+
...(overrides.excludeDirs || []),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// 去重
|
|
178
|
+
const uniqueDirs = [...new Set(excludeDirs)];
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
excludeDirs: uniqueDirs,
|
|
182
|
+
excludeFiles: [...DEFAULT_EXCLUDE_FILES, ...(overrides.excludeFiles || [])],
|
|
183
|
+
maxFileSize: overrides.maxFileSize || 50 * 1024 * 1024,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 生成隐私合规报告
|
|
189
|
+
* @param {object} scanResult - scanPrivacyRisks 的结果
|
|
190
|
+
* @param {object} copyReport - copyDirFiltered 的结果
|
|
191
|
+
* @returns {object} 隐私合规报告
|
|
192
|
+
*/
|
|
193
|
+
export function generatePrivacyReport(scanResult, copyReport) {
|
|
194
|
+
const severity = scanResult.hasPrivateMemory ? "high" :
|
|
195
|
+
scanResult.sensitiveFindings.length > 0 ? "medium" : "clean";
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
version: "1.0",
|
|
199
|
+
generatedAt: new Date().toISOString(),
|
|
200
|
+
severity,
|
|
201
|
+
summary: {
|
|
202
|
+
totalFilesScanned: scanResult.totalFilesScanned,
|
|
203
|
+
sensitiveItemsFound: scanResult.sensitiveFindings.length,
|
|
204
|
+
privateMemoryDetected: scanResult.hasPrivateMemory,
|
|
205
|
+
privateMemoryCount: scanResult.privateMemoryCount,
|
|
206
|
+
filesSkipped: copyReport?.skipped?.length || 0,
|
|
207
|
+
filesCopied: copyReport?.copied?.length || 0,
|
|
208
|
+
},
|
|
209
|
+
details: {
|
|
210
|
+
sensitiveFindings: scanResult.sensitiveFindings,
|
|
211
|
+
riskyFiles: scanResult.riskyFiles,
|
|
212
|
+
skippedPaths: copyReport?.skipped || [],
|
|
213
|
+
},
|
|
214
|
+
compliance: {
|
|
215
|
+
privateDataStripped: !scanResult.hasPrivateMemory || severity !== "clean",
|
|
216
|
+
sensitiveInfoRedacted: scanResult.sensitiveFindings.length === 0,
|
|
217
|
+
policyEnforced: true,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|