UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

403 lines (402 loc) 13.5 kB
/** * Policies — Agent 执行约束 (横切关注点) * * Policy 不改变 Agent 做什么,而是约束 Agent 如何做。 * 多个 Policy 可叠加,形成复合约束。 * * 三类 Policy: * 1. BudgetPolicy — 资源预算 (迭代次数 / Token / 时间) * 2. SafetyPolicy — 安全沙箱 (命令黑名单 / 文件范围 / 发送者鉴权) * 3. QualityGatePolicy — 质量门控 (证据数量 / 分析深度) * * 这就是为什么"飞书远程执行"不需要独立 Agent: * 它只是 Conversation + SystemInteraction + SafetyPolicy 的组合。 * SafetyPolicy 提供命令沙箱,而不是由 LarkBridgeAgent 硬编码。 * * @module policies */ import _path from 'node:path'; // ─── Base Policy ────────────────────────────── /** Policy 基类 — 所有约束的抽象接口 */ export class Policy { /** 策略名称 */ get name() { throw new Error('Subclass must implement name'); } /** 执行前校验 — 拒绝不满足条件的请求 */ validateBefore(_context) { return { ok: true }; } /** 执行中校验 — 每轮 ReAct 步骤后检查 */ validateDuring(_stepState) { return { ok: true, action: 'continue' }; } /** 执行后校验 — 对最终结果质量把关 */ validateAfter(_result) { return { ok: true }; } /** 修改配置 — 在执行前注入额外约束 */ applyToConfig(config) { return config; } } // ─── BudgetPolicy — 资源预算 ───────────────── /** * 控制 Agent 的资源消耗上限。 * * 适用于所有场景,不同 Preset 配置不同预算: * - 聊天: { maxIterations: 8, timeoutMs: 120_000 } * - 深度分析: { maxIterations: 24, timeoutMs: 300_000 } * - 冷启动: { maxIterations: 24, timeoutMs: 600_000 } * - 远程执行: { maxIterations: 6, timeoutMs: 60_000 } */ export class BudgetPolicy extends Policy { #maxIterations; #maxTokens; #timeoutMs; #temperature; constructor({ maxIterations = 20, maxTokens = 4096, timeoutMs = 300_000, temperature = 0.7, } = {}) { super(); this.#maxIterations = maxIterations; this.#maxTokens = maxTokens; this.#timeoutMs = timeoutMs; this.#temperature = temperature; } get name() { return 'budget'; } get maxIterations() { return this.#maxIterations; } get maxTokens() { return this.#maxTokens; } get timeoutMs() { return this.#timeoutMs; } get temperature() { return this.#temperature; } validateDuring(stepState) { if (stepState.iteration >= this.#maxIterations) { return { ok: false, action: 'stop', reason: `Budget: max iterations (${this.#maxIterations}) reached`, }; } if (Date.now() - stepState.startTime > this.#timeoutMs) { return { ok: false, action: 'stop', reason: `Budget: timeout (${this.#timeoutMs}ms) exceeded`, }; } return { ok: true, action: 'continue' }; } applyToConfig(config) { return { ...config, budget: { maxIterations: this.#maxIterations, maxTokens: this.#maxTokens, timeoutMs: this.#timeoutMs, temperature: this.#temperature, }, }; } } // ─── SafetyPolicy — 安全沙箱 ──────────────── /** * 安全约束: 命令过滤、文件范围限制、发送者鉴权。 * * 这取代了旧 LarkBridgeAgent 中硬编码的安全逻辑: * - SafetyPolicy 是可组合的、可配置的、可复用的 * - 任何需要安全约束的场景都可以叠加这个 Policy * - 不局限于飞书场景 — CLI 远程执行同样适用 */ export class SafetyPolicy extends Policy { /** 危险命令正则黑名单 */ static DANGEROUS_COMMANDS = Object.freeze([ /\brm\s+-rf\s+[/~]/, /\bsudo\b/, /\bmkfs\b/, /\bdd\s+if=/, /\b(shutdown|reboot|halt)\b/, />\s*\/dev\//, /\bcurl\b.*\|\s*(bash|sh)/, /\bchmod\s+777/, /\bpasswd\b/, /\bkillall\b/, ]); /** 安全命令前缀白名单 */ static SAFE_COMMANDS = Object.freeze([ 'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc', 'echo', 'pwd', 'date', 'which', 'file', 'stat', 'git log', 'git status', 'git diff', 'git branch', 'npm list', 'npm outdated', 'node -v', 'npm -v', ]); #fileScope; #allowedSenders; #commandBlacklist; #requireApprovalFor; /** * @param [opts.fileScope] 文件操作范围 (目录路径) * @param [opts.allowedSenders] 允许的发送者 ID (空=不限制) * @param [opts.commandBlacklist] 额外命令黑名单 * @param [opts.requireApprovalFor] 需要人工确认的工具名 */ constructor({ fileScope, allowedSenders = [], commandBlacklist = [], requireApprovalFor = [], } = {}) { super(); this.#fileScope = fileScope || null; this.#allowedSenders = allowedSenders; this.#commandBlacklist = [...SafetyPolicy.DANGEROUS_COMMANDS, ...commandBlacklist]; this.#requireApprovalFor = requireApprovalFor; } get name() { return 'safety'; } validateBefore(context) { // 发送者鉴权 if (this.#allowedSenders.length > 0) { const senderId = context.message?.sender?.id; if (!senderId || !this.#allowedSenders.includes(senderId)) { return { ok: false, reason: `Safety: sender "${senderId}" not in allowlist` }; } } return { ok: true }; } /** * 检查命令是否安全 * @returns } */ checkCommand(command) { for (const pattern of this.#commandBlacklist) { if (pattern.test(command)) { return { safe: false, reason: `Blocked: matches dangerous pattern ${pattern}` }; } } return { safe: true }; } /** * 检查文件路径是否在允许范围内 * @returns } */ checkFilePath(filePath) { if (!this.#fileScope) { return { safe: true }; } const resolved = _path.resolve(filePath); const scope = _path.resolve(this.#fileScope); if (!resolved.startsWith(scope)) { return { safe: false, reason: `File path "${filePath}" outside allowed scope "${this.#fileScope}"`, }; } return { safe: true }; } /** 是否需要人工确认 */ needsApproval(toolName) { return this.#requireApprovalFor.includes(toolName); } applyToConfig(config) { return { ...config, safetyPolicy: this, }; } } // ─── QualityGatePolicy — 质量门控 ──────────── /** * 评估 Agent 输出质量,决定是否接受结果。 * * 用于 Pipeline 的 gate 阶段,也可用于最终结果校验。 * 取代了旧 BootstrapOrchestrator 中硬编码的 qualityCheck。 */ export class QualityGatePolicy extends Policy { #minEvidenceLength; #minFileRefs; #minToolCalls; #customValidator; /** * @param [opts.minEvidenceLength=500] 分析文本最小长度 * @param [opts.minFileRefs=3] 最少文件引用数 * @param [opts.minToolCalls=2] 最少工具调用数 * @param [opts.customValidator] 自定义校验 (result) => { ok, reason } */ constructor({ minEvidenceLength = 500, minFileRefs = 3, minToolCalls = 2, customValidator, } = {}) { super(); this.#minEvidenceLength = minEvidenceLength; this.#minFileRefs = minFileRefs; this.#minToolCalls = minToolCalls; this.#customValidator = customValidator || null; } get name() { return 'quality_gate'; } validateAfter(result) { const reasons = []; if (result.reply && result.reply.length < this.#minEvidenceLength) { reasons.push(`分析长度不足: ${result.reply.length} < ${this.#minEvidenceLength}`); } if (result.reply) { const fileRefCount = (result.reply.match(/[\w/-]+\.\w{1,6}/g) || []).length; if (fileRefCount < this.#minFileRefs) { reasons.push(`文件引用不足: ${fileRefCount} < ${this.#minFileRefs}`); } } if ((result.toolCalls?.length || 0) < this.#minToolCalls) { reasons.push(`工具调用不足: ${result.toolCalls?.length || 0} < ${this.#minToolCalls}`); } if (this.#customValidator) { const custom = this.#customValidator(result); if (!custom.ok && custom.reason) { reasons.push(custom.reason); } } return reasons.length === 0 ? { ok: true } : { ok: false, reason: reasons.join('; ') }; } /** 导出为 PipelineStrategy gate 配置格式 */ toGateConfig() { return { minEvidenceLength: this.#minEvidenceLength, minFileRefs: this.#minFileRefs, minToolCalls: this.#minToolCalls, custom: this.#customValidator, }; } } // ─── PolicyEngine — 策略引擎 ───────────────── /** * 组合多个 Policy 并统一执行校验。 * * @example * const engine = new PolicyEngine([ * new BudgetPolicy({ maxIterations: 8 }), * new SafetyPolicy({ fileScope: '/project' }), * ]); * engine.validateBefore(context); // 所有 policy 依次检查 */ export class PolicyEngine { #policies; constructor(policies = []) { this.#policies = policies; } get policies() { return [...this.#policies]; } /** * 获取特定类型的 Policy * @template T */ get(PolicyClass) { return this.#policies.find((p) => p instanceof PolicyClass) ?? null; } validateBefore(context) { for (const policy of this.#policies) { const result = policy.validateBefore(context); if (!result.ok) { return result; } } return { ok: true }; } validateDuring(stepState) { for (const policy of this.#policies) { const result = policy.validateDuring(stepState); if (!result.ok) { return result; } } return { ok: true, action: 'continue' }; } validateAfter(result) { for (const policy of this.#policies) { const val = policy.validateAfter(result); if (!val.ok) { return val; } } return { ok: true }; } applyToConfig(config) { let result = config; for (const policy of this.#policies) { result = policy.applyToConfig(result); } return result; } /** 获取合并后的 Budget (从 BudgetPolicy) */ getBudget() { const bp = this.get(BudgetPolicy); return bp ? { maxIterations: bp.maxIterations, maxTokens: bp.maxTokens, timeoutMs: bp.timeoutMs, temperature: bp.temperature, } : null; } /** * 工具执行前的安全校验 — 在 reactLoop 中每次工具调用前自动触发 * * 对有副作用的工具 (run_safe_command, write_project_file) 执行安全检查。 * 委托给 SafetyPolicy,如果没有加载 SafetyPolicy 则放行。 * * @param toolName 工具名称 * @param args 工具参数 * @returns } */ validateToolCall(toolName, args) { const safety = this.get(SafetyPolicy); if (!safety) { return { ok: true }; } // 终端命令安全检查 if (toolName === 'run_safe_command' && args?.command) { const check = safety.checkCommand(args.command); if (!check.safe) { return { ok: false, reason: `[SafetyPolicy] 命令拦截: ${check.reason}` }; } } // 文件写入路径检查 if (toolName === 'write_project_file' && args?.filePath) { const check = safety.checkFilePath(args.filePath); if (!check.safe) { return { ok: false, reason: `[SafetyPolicy] 路径拦截: ${check.reason}` }; } } // 文件读取路径检查 if (toolName === 'read_project_file' && args?.filePath) { const check = safety.checkFilePath(args.filePath); if (!check.safe) { return { ok: false, reason: `[SafetyPolicy] 路径拦截: ${check.reason}` }; } } // 需要人工确认的工具 if (safety.needsApproval(toolName)) { return { ok: false, reason: `[SafetyPolicy] 工具 "${toolName}" 需要人工确认` }; } return { ok: true }; } } export default { Policy, BudgetPolicy, SafetyPolicy, QualityGatePolicy, PolicyEngine };