autosnippet 2.7.0 → 2.7.1
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/README.md +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +5 -0
- package/dashboard/dist/assets/{icons-Cq4-iQhP.js → icons-B_Xg4B-s.js} +61 -61
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +17 -0
- package/lib/cli/SetupService.js +53 -0
- package/lib/external/ai/providers/ClaudeProvider.js +12 -1
- package/lib/external/ai/providers/GoogleGeminiProvider.js +13 -1
- package/lib/external/ai/providers/OpenAiProvider.js +13 -3
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +194 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +8 -10
- package/lib/external/mcp/handlers/bootstrap.js +8 -0
- package/lib/external/mcp/handlers/skill.js +4 -0
- package/lib/http/routes/ai.js +155 -1
- package/lib/infrastructure/config/Paths.js +3 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -0
- package/lib/service/candidate/CandidateFileWriter.js +4 -0
- package/lib/service/chat/AnalystAgent.js +37 -8
- package/lib/service/chat/CandidateGuardrail.js +3 -3
- package/lib/service/chat/ChatAgent.js +20 -1
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +53 -0
- package/lib/service/chat/tools.js +13 -6
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +4 -0
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +2 -0
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/dashboard/dist/assets/index-DBxH7pVn.css +0 -1
- package/dashboard/dist/assets/index-Dw2F6qAS.js +0 -197
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PathGuard — 文件写入路径安全守卫(双层防护)
|
|
3
|
+
*
|
|
4
|
+
* 防止 AutoSnippet 在项目目录之外 或 项目内非法位置 创建文件。
|
|
5
|
+
* BiliDemo/data 事件的根因:process.cwd() 解析到非预期目录,DB/日志等写操作
|
|
6
|
+
* 逃逸到用户项目外,创建了脏数据。
|
|
7
|
+
*
|
|
8
|
+
* 双层防护:
|
|
9
|
+
* Layer 1 — assertSafe(path):
|
|
10
|
+
* 边界检查,拦截写到 projectRoot 外的操作
|
|
11
|
+
* Layer 2 — assertProjectWriteSafe(path):
|
|
12
|
+
* 项目内作用域检查,仅允许写入以下前缀:
|
|
13
|
+
* .autosnippet/ — 运行时 DB、记忆、对话、信号快照
|
|
14
|
+
* {kbDir}/ — 知识库(recipes、candidates、skills、guard 文件)
|
|
15
|
+
* .cursor/ — Cursor IDE 集成
|
|
16
|
+
* .vscode/ — VSCode 集成
|
|
17
|
+
* .github/ — Copilot instructions
|
|
18
|
+
* .gitignore — 追加忽略规则
|
|
19
|
+
* 项目内其他位置(如 data/、src/ 等)一律拦截
|
|
20
|
+
*
|
|
21
|
+
* 设计:
|
|
22
|
+
* - 单例模式,通过 configure() 绑定 projectRoot
|
|
23
|
+
* - 新建文件/目录前调用 assertProjectWriteSafe() 校验
|
|
24
|
+
* - 修改已有文件前调用 assertSafe() 校验(不限制项目内位置)
|
|
25
|
+
* - 允许白名单目录(Xcode snippets、全局缓存等)
|
|
26
|
+
* - 错误不静默:越界写操作抛出 PathGuardError
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
|
|
32
|
+
export class PathGuardError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} targetPath - 被拦截的目标路径
|
|
35
|
+
* @param {string} projectRoot - 当前项目根目录
|
|
36
|
+
* @param {string} [reason] - 拦截原因
|
|
37
|
+
*/
|
|
38
|
+
constructor(targetPath, projectRoot, reason) {
|
|
39
|
+
const msg = reason
|
|
40
|
+
? `[PathGuard] ${reason}: "${targetPath}"`
|
|
41
|
+
: `[PathGuard] 写入路径越界: "${targetPath}" 不在允许范围内。`;
|
|
42
|
+
super(
|
|
43
|
+
msg +
|
|
44
|
+
`\n projectRoot: ${projectRoot}` +
|
|
45
|
+
`\n 提示: 检查 process.cwd() 或 projectRoot 配置是否正确`
|
|
46
|
+
);
|
|
47
|
+
this.name = 'PathGuardError';
|
|
48
|
+
this.targetPath = targetPath;
|
|
49
|
+
this.projectRoot = projectRoot;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 项目内允许 AutoSnippet 创建新文件/目录的前缀
|
|
55
|
+
* 注意:这是相对于 projectRoot 的前缀列表
|
|
56
|
+
*/
|
|
57
|
+
const PROJECT_WRITE_SCOPE_PREFIXES = [
|
|
58
|
+
'.autosnippet', // 运行时 DB、记忆、对话、信号快照
|
|
59
|
+
'.cursor', // Cursor IDE 集成
|
|
60
|
+
'.vscode', // VSCode 集成
|
|
61
|
+
'.github', // Copilot instructions
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 项目根目录下允许直接写入的文件(非目录前缀匹配)
|
|
66
|
+
*/
|
|
67
|
+
const PROJECT_ROOT_WRITABLE_FILES = [
|
|
68
|
+
'.gitignore',
|
|
69
|
+
'.env',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
class PathGuard {
|
|
73
|
+
/** @type {string|null} 项目根目录(绝对路径) */
|
|
74
|
+
#projectRoot = null;
|
|
75
|
+
|
|
76
|
+
/** @type {string|null} AutoSnippet 包自身根目录 */
|
|
77
|
+
#packageRoot = null;
|
|
78
|
+
|
|
79
|
+
/** @type {Set<string>} 额外允许的绝对路径前缀 */
|
|
80
|
+
#allowList = new Set();
|
|
81
|
+
|
|
82
|
+
/** @type {string|null} 知识库目录名(如 'AutoSnippet') */
|
|
83
|
+
#knowledgeBaseDir = null;
|
|
84
|
+
|
|
85
|
+
/** @type {boolean} 是否已配置 */
|
|
86
|
+
#configured = false;
|
|
87
|
+
|
|
88
|
+
constructor() {}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 配置 PathGuard(每个进程执行一次)
|
|
92
|
+
* @param {object} opts
|
|
93
|
+
* @param {string} opts.projectRoot - 用户项目根目录(绝对路径)
|
|
94
|
+
* @param {string} [opts.packageRoot] - AutoSnippet 包自身根目录
|
|
95
|
+
* @param {string} [opts.knowledgeBaseDir='AutoSnippet'] - 知识库目录名
|
|
96
|
+
* @param {string[]} [opts.extraAllowPaths] - 额外允许的路径前缀
|
|
97
|
+
*/
|
|
98
|
+
configure({ projectRoot, packageRoot, knowledgeBaseDir, extraAllowPaths = [] }) {
|
|
99
|
+
if (!projectRoot || !path.isAbsolute(projectRoot)) {
|
|
100
|
+
throw new Error(`[PathGuard] projectRoot 必须是绝对路径,收到: "${projectRoot}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.#projectRoot = path.resolve(projectRoot);
|
|
104
|
+
this.#packageRoot = packageRoot ? path.resolve(packageRoot) : null;
|
|
105
|
+
this.#knowledgeBaseDir = knowledgeBaseDir || null; // 延迟解析
|
|
106
|
+
|
|
107
|
+
// 默认白名单:Xcode snippets 目录、全局缓存
|
|
108
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
109
|
+
if (HOME) {
|
|
110
|
+
this.#allowList.add(path.join(HOME, 'Library/Developer/Xcode/UserData/CodeSnippets'));
|
|
111
|
+
this.#allowList.add(path.join(HOME, '.autosnippet'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 用户自定义白名单
|
|
115
|
+
for (const p of extraAllowPaths) {
|
|
116
|
+
if (path.isAbsolute(p)) {
|
|
117
|
+
this.#allowList.add(path.resolve(p));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.#configured = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 是否已配置 */
|
|
125
|
+
get configured() {
|
|
126
|
+
return this.#configured;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 当前 projectRoot */
|
|
130
|
+
get projectRoot() {
|
|
131
|
+
return this.#projectRoot;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 设置知识库目录名(可在 configure 之后延迟设置)
|
|
136
|
+
* @param {string} dirName - 如 'AutoSnippet'、'Knowledge' 等
|
|
137
|
+
*/
|
|
138
|
+
setKnowledgeBaseDir(dirName) {
|
|
139
|
+
if (dirName && typeof dirName === 'string') {
|
|
140
|
+
this.#knowledgeBaseDir = dirName;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Layer 1: 断言路径在允许的边界范围内
|
|
146
|
+
* 用于修改已有文件的场景(如 XcodeIntegration 插入 header、SpmService 修改 Package.swift)
|
|
147
|
+
* @param {string} targetPath - 要写入的绝对路径
|
|
148
|
+
* @throws {PathGuardError}
|
|
149
|
+
*/
|
|
150
|
+
assertSafe(targetPath) {
|
|
151
|
+
if (!this.#configured) return;
|
|
152
|
+
|
|
153
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
154
|
+
throw new PathGuardError(String(targetPath), this.#projectRoot);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const resolved = path.resolve(targetPath);
|
|
158
|
+
|
|
159
|
+
// 1. 项目目录内 — 允许
|
|
160
|
+
if (this.#isUnder(resolved, this.#projectRoot)) return;
|
|
161
|
+
|
|
162
|
+
// 2. AutoSnippet 包自身目录内(logs/ 等)— 允许
|
|
163
|
+
if (this.#packageRoot && this.#isUnder(resolved, this.#packageRoot)) return;
|
|
164
|
+
|
|
165
|
+
// 3. 白名单目录 — 允许
|
|
166
|
+
for (const allowed of this.#allowList) {
|
|
167
|
+
if (this.#isUnder(resolved, allowed)) return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 越界
|
|
171
|
+
throw new PathGuardError(resolved, this.#projectRoot);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Layer 2: 断言路径在项目内允许的写入作用域中
|
|
176
|
+
* 用于创建新目录/新文件的场景(如 mkdirSync、writeFileSync 创建新文件)
|
|
177
|
+
* 比 assertSafe() 更严格:即使在 projectRoot 内,也只允许写入特定前缀
|
|
178
|
+
* @param {string} targetPath - 要创建的绝对路径
|
|
179
|
+
* @throws {PathGuardError}
|
|
180
|
+
*/
|
|
181
|
+
assertProjectWriteSafe(targetPath) {
|
|
182
|
+
if (!this.#configured) return;
|
|
183
|
+
|
|
184
|
+
// 先做边界检查
|
|
185
|
+
this.assertSafe(targetPath);
|
|
186
|
+
|
|
187
|
+
const resolved = path.resolve(targetPath);
|
|
188
|
+
|
|
189
|
+
// 如果不在 projectRoot 内(在白名单/packageRoot 中),跳过项目内检查
|
|
190
|
+
if (!this.#isUnder(resolved, this.#projectRoot)) return;
|
|
191
|
+
|
|
192
|
+
// 计算相对于 projectRoot 的路径
|
|
193
|
+
const relative = path.relative(this.#projectRoot, resolved);
|
|
194
|
+
const firstSegment = relative.split(path.sep)[0];
|
|
195
|
+
|
|
196
|
+
// 检查是否在允许的前缀中
|
|
197
|
+
for (const prefix of PROJECT_WRITE_SCOPE_PREFIXES) {
|
|
198
|
+
if (firstSegment === prefix) return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 检查知识库目录(动态解析)
|
|
202
|
+
const kbDir = this.#resolveKnowledgeBaseDir();
|
|
203
|
+
if (kbDir && firstSegment === kbDir) return;
|
|
204
|
+
|
|
205
|
+
// 检查根目录可写文件(如 .gitignore)
|
|
206
|
+
if (PROJECT_ROOT_WRITABLE_FILES.includes(relative)) return;
|
|
207
|
+
|
|
208
|
+
// 不在允许的写入范围内
|
|
209
|
+
throw new PathGuardError(
|
|
210
|
+
resolved,
|
|
211
|
+
this.#projectRoot,
|
|
212
|
+
`项目内写入范围受限: "${relative}" 不在允许的目录中(允许: ${[...PROJECT_WRITE_SCOPE_PREFIXES, kbDir || 'AutoSnippet'].join(', ')})`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 安全检查(不抛错,返回 boolean)
|
|
218
|
+
* @param {string} targetPath
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
isSafe(targetPath) {
|
|
222
|
+
try {
|
|
223
|
+
this.assertSafe(targetPath);
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 项目内写入范围检查(不抛错,返回 boolean)
|
|
232
|
+
* @param {string} targetPath
|
|
233
|
+
* @returns {boolean}
|
|
234
|
+
*/
|
|
235
|
+
isProjectWriteSafe(targetPath) {
|
|
236
|
+
try {
|
|
237
|
+
this.assertProjectWriteSafe(targetPath);
|
|
238
|
+
return true;
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 将相对路径安全地解析到 projectRoot 下
|
|
246
|
+
* 替代 path.resolve(relativePath)(后者基于 cwd,不安全)
|
|
247
|
+
* @param {string} relativePath
|
|
248
|
+
* @returns {string} 绝对路径
|
|
249
|
+
*/
|
|
250
|
+
resolveProjectPath(relativePath) {
|
|
251
|
+
if (!this.#configured || !this.#projectRoot) {
|
|
252
|
+
// 未配置时 fallback 到 cwd(向后兼容)
|
|
253
|
+
return path.resolve(relativePath);
|
|
254
|
+
}
|
|
255
|
+
const resolved = path.resolve(this.#projectRoot, relativePath);
|
|
256
|
+
this.assertSafe(resolved);
|
|
257
|
+
return resolved;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 重置状态(仅用于测试)
|
|
262
|
+
*/
|
|
263
|
+
_reset() {
|
|
264
|
+
this.#projectRoot = null;
|
|
265
|
+
this.#packageRoot = null;
|
|
266
|
+
this.#allowList.clear();
|
|
267
|
+
this.#knowledgeBaseDir = null;
|
|
268
|
+
this.#configured = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* resolved 是否在 base 目录下
|
|
273
|
+
*/
|
|
274
|
+
#isUnder(resolved, base) {
|
|
275
|
+
return resolved === base || resolved.startsWith(base + path.sep);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 解析知识库目录名
|
|
280
|
+
* 优先使用 configure 阶段传入的值,否则尝试运行时探测
|
|
281
|
+
*/
|
|
282
|
+
#resolveKnowledgeBaseDir() {
|
|
283
|
+
if (this.#knowledgeBaseDir) return this.#knowledgeBaseDir;
|
|
284
|
+
|
|
285
|
+
// 运行时探测: 查找包含 AutoSnippet.boxspec.json 的子目录
|
|
286
|
+
try {
|
|
287
|
+
const entries = fs.readdirSync(this.#projectRoot, { withFileTypes: true });
|
|
288
|
+
for (const e of entries) {
|
|
289
|
+
if (e.isDirectory() && !e.name.startsWith('.')) {
|
|
290
|
+
if (fs.existsSync(path.join(this.#projectRoot, e.name, 'AutoSnippet.boxspec.json'))) {
|
|
291
|
+
this.#knowledgeBaseDir = e.name;
|
|
292
|
+
return e.name;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch { /* ignore */ }
|
|
297
|
+
|
|
298
|
+
// 默认
|
|
299
|
+
return 'AutoSnippet';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 延迟加载 fs(避免循环依赖)
|
|
305
|
+
*/
|
|
306
|
+
function await_fs() {
|
|
307
|
+
// eslint-disable-next-line no-eval
|
|
308
|
+
return eval("require('fs')");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 单例 — 整个进程共享
|
|
312
|
+
const pathGuard = new PathGuard();
|
|
313
|
+
|
|
314
|
+
export default pathGuard;
|