_sandbox.js 1.88 KB
/**
 * 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,
};