_sandbox.js
1.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* Sandbox 边界校验 — Agent 写盘前的 path 检查。
*
* 用法:
* const { assertWritable } = require('./_sandbox');
* assertWritable(abs, allowList, ctx.agent);
*
* allowList 形式 (glob-like):
* "prd/**" → ${ctx.projectDir}/prd/** 任意子文件
* "units/{unitId}/code/**" → ${ctx.unitDir}/code/** (unitId 自动替换)
* "agent-state/**" → ${ctx.projectDir}/agent-state/** 任意
* "timeline.ndjson" → 精确文件
*/
const path = require('path');
const { ToolSandboxError } = require('./_errors');
/**
* @param {string} abs - 待写绝对路径
* @param {string[]} allowList - 允许写的相对 pattern (相对 ctx.projectDir 或 ctx.unitDir)
* @param {object} ctx - { agent, projectDir, unitDir? }
*/
function assertWritable(abs, allowList, ctx) {
const normalized = path.resolve(abs);
if (isWithinAllowList(normalized, allowList, ctx)) return;
throw new ToolSandboxError(normalized, ctx.agent || 'unknown');
}
function isWithinAllowList(abs, allowList, ctx) {
for (const pattern of allowList) {
if (matchesPattern(abs, pattern, ctx)) return true;
}
return false;
}
function matchesPattern(abs, pattern, ctx) {
// 替换 {unitId} 等占位符
const resolved = pattern
.replace('{unitId}', ctx.unit_id || '__no_unit__');
// 决定 base:含 units/{unitId}/ 的 pattern → 用 unitDir 的父目录(=projectDir/units/)
// 其他 → 用 projectDir
// 简化:统一用 projectDir 作为 base 拼接 pattern
const base = ctx.projectDir || '';
const expected = path.join(base, resolved);
// 处理 ** glob
if (expected.endsWith('/**')) {
const prefix = expected.slice(0, -3); // 去掉 "/**"
return abs === prefix || abs.startsWith(prefix + path.sep);
}
return abs === expected;
}
module.exports = {
assertWritable,
isWithinAllowList,
matchesPattern,
};