/** * 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, };