/** * DelegationPolicy — gate policy for pre-delegation and post-delegation checkpoints. * * Pre-delegation checks: * - Role validation (must be a known DelegationRole) * - Phase check: code delegation only allowed in execute phase * - Parallel role check: no concurrent code delegations * * Post-delegation checks: * - Prevent subagent output from becoming main evidence/facts * * Checkpoints: pre-delegation, post-delegation */ import type { GateContext, GateFinding } from "../findings.ts"; import type { ActiveRun } from "../../types.ts"; export const DELEGATION_POLICY_NAME = "delegation-policy"; // ── Valid delegation roles ──────────────────────────────────────────────────── const VALID_DELEGATION_ROLES = new Set(["research", "doc", "code", "review", "verify"]); /** Roles that require execute phase. */ const EXECUTE_ONLY_ROLES = new Set(["code"]); /** Roles that can write evidence/facts. */ const EVIDENCE_WRITING_ROLES = new Set(["verify"]); // ── Active delegation tracking (in-memory) ─────────────────────────────────── /** Active code delegations tracked per run. */ const activeCodeDelegations = new Map>(); /** * Register an active code delegation for a run. * * @param runId - The run id. * @param planId - The delegation plan id. */ export function registerActiveDelegation(runId: string, planId: string): void { let delegations = activeCodeDelegations.get(runId); if (!delegations) { delegations = new Set(); activeCodeDelegations.set(runId, delegations); } delegations.add(planId); } /** * Unregister an active code delegation for a run. * * @param runId - The run id. * @param planId - The delegation plan id. */ export function unregisterActiveDelegation(runId: string, planId: string): void { const delegations = activeCodeDelegations.get(runId); if (delegations) { delegations.delete(planId); if (delegations.size === 0) { activeCodeDelegations.delete(runId); } } } /** * Check if a run has active code delegations. * * @param runId - The run id. * @returns true if there are active code delegations. */ export function hasActiveCodeDelegation(runId: string): boolean { const delegations = activeCodeDelegations.get(runId); return delegations !== undefined && delegations.size > 0; } // ── Policy evaluation ───────────────────────────────────────────────────────── /** * Evaluate delegation policy for pre-delegation and post-delegation checkpoints. * * @param ctx - The gate context. * @returns Array of findings (empty = pass). */ export function evaluateDelegationPolicy(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; if (ctx.checkpoint === "pre-delegation") { findings.push(...evaluatePreDelegation(ctx)); } else if (ctx.checkpoint === "post-delegation") { findings.push(...evaluatePostDelegation(ctx)); } return findings; } // ── Pre-delegation checks ───────────────────────────────────────────────────── /** * Pre-delegation checks: * 1. Role must be a known DelegationRole. * 2. Code delegation only in execute phase. * 3. No concurrent code delegations (parallel role check). * 4. Active run required for code delegation. */ function evaluatePreDelegation(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; const { delegationRole, run, payload } = ctx; // 1. Role validation. if (!delegationRole || !VALID_DELEGATION_ROLES.has(delegationRole)) { findings.push({ id: "DLP-001", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: `未知的委派角色: "${delegationRole ?? "(空)"}"。有效角色: ${Array.from(VALID_DELEGATION_ROLES).join(", ")}`, nextAction: "使用有效的委派角色: research, doc, code, review, verify。", }); return findings; // No point checking further with invalid role. } // 2. Code delegation requires execute phase. if (EXECUTE_ONLY_ROLES.has(delegationRole)) { if (!run) { findings.push({ id: "DLP-002", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: `"${delegationRole}" 委派需要活跃的需求运行(ActiveRun)。`, nextAction: "先用 /kd <需求> 开始一个需求,再进行 code 委派。", }); } else if (run.phase !== "execute") { findings.push({ id: "DLP-003", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: `"${delegationRole}" 委派仅在 execute 阶段允许,当前阶段为 "${run.phase}"。`, nextAction: "先完成当前阶段文档和检查,用 /kd phase execute 进入 execute 后再委派 code 任务。", }); } } // 3. Parallel role check: no concurrent code delegations. if (run && EXECUTE_ONLY_ROLES.has(delegationRole)) { if (hasActiveCodeDelegation(run.id)) { findings.push({ id: "DLP-004", severity: "soft-deny", policy: DELEGATION_POLICY_NAME, message: "已存在活跃的 code 委派,不允许并行 code 委派。请等待当前委派完成。", nextAction: "等待当前 code 委派完成后再发起新的 code 委派。", }); } } // 4. Payload validation: planId should be present for tracking. const planId = (payload as { planId?: string })?.planId; if (!planId) { findings.push({ id: "DLP-005", severity: "warning", policy: DELEGATION_POLICY_NAME, message: "委派缺少 planId,无法追踪活跃委派状态。", nextAction: "确保 DelegationPlan 包含 planId。", }); } return findings; } // ── Post-delegation checks ──────────────────────────────────────────────────── /** * Post-delegation checks: * 1. Subagent output must NOT become main evidence/facts. * 2. Only verify-role delegations can write evidence. * 3. Subagent output must be tagged as delegation-sourced. */ function evaluatePostDelegation(ctx: GateContext): GateFinding[] { const findings: GateFinding[] = []; const { delegationRole, payload } = ctx; const output = payload as { writesToFacts?: boolean; writesToEvidence?: boolean; outputTag?: string; targetPaths?: string[]; } | undefined; // 1. Subagent must not write to main facts. if (output?.writesToFacts) { findings.push({ id: "DLP-101", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: "子 agent 输出禁止写入主事实(facts)。子 agent 结论必须经过主 agent 审核后才能成为事实。", nextAction: "子 agent 输出应作为参考信息返回主 agent,由主 agent 决定是否纳入 facts。", }); } // 2. Only verify-role can write evidence directly. if (output?.writesToEvidence && delegationRole !== "verify") { findings.push({ id: "DLP-102", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: `角色 "${delegationRole ?? "(未知)"}" 的子 agent 禁止直接写入证据(evidence)。仅 verify 角色允许写入证据。`, nextAction: "子 agent 输出应返回主 agent,由主 agent 或 verify 委派写入证据。", }); } // 3. Subagent output must be tagged as delegation-sourced. if (output?.outputTag && output.outputTag !== "delegation-sourced") { findings.push({ id: "DLP-103", severity: "warning", policy: DELEGATION_POLICY_NAME, message: `子 agent 输出标签 "${output.outputTag}" 不是 "delegation-sourced",可能导致来源混淆。`, nextAction: "确保子 agent 输出标记为 delegation-sourced 以便追溯。", }); } // 4. Verify no forbidden target paths. if (output?.targetPaths) { const forbiddenPaths = output.targetPaths.filter((p) => isMainFactOrEvidencePath(p), ); if (forbiddenPaths.length > 0) { findings.push({ id: "DLP-104", severity: "hard-deny", policy: DELEGATION_POLICY_NAME, message: `子 agent 禁止写入以下路径: ${forbiddenPaths.join(", ")}`, nextAction: "子 agent 输出应写入委派专属的临时路径,由主 agent 审核后移入正式路径。", sourceRefs: forbiddenPaths, }); } } return findings; } // ── Path utilities ──────────────────────────────────────────────────────────── /** Paths that are considered main facts/evidence and off-limits to subagents. */ const FORBIDDEN_PATH_PATTERNS = [ /evidence\/index\.json$/i, /facts\.json$/i, /\/facts\//i, /\/evidence\/[^/]+$/i, ]; /** * Check if a path targets main facts or evidence files. */ function isMainFactOrEvidencePath(path: string): boolean { const normalized = path.replace(/\\/g, "/"); return FORBIDDEN_PATH_PATTERNS.some((pattern) => pattern.test(normalized)); }