import type { ActiveRun } from "./types.ts"; import { inspectGate } from "./gates.ts"; export type KdRiskPolicyLevel = "hard-deny" | "soft-deny" | "evidence-required" | "warning"; export interface KdRiskPolicyRule { id: string; level: KdRiskPolicyLevel; title: string; description: string; } export const RISK_POLICY_RULES: KdRiskPolicyRule[] = [ { id: "workspace-external-source-write", level: "hard-deny", title: "拒绝工作区外源码写入", description: "生产源码写入必须位于当前工作区,并使用当前项目相对路径或工作区内路径。", }, { id: "write-transaction-bypass", level: "hard-deny", title: "拒绝绕过写入事务", description: "禁止用 PowerShell 重定向、Set-Content、Out-File、Remove/Move/Copy 等文件变更命令绕过 write/edit 事务。", }, { id: "open-question-or-resume-write", level: "hard-deny", title: "阻断问题或恢复断点未处理时拒绝源码写入", description: "存在 open blocking question 或 open resumeSnapshot 时,唯一动作是记录答案、修订事实或处理恢复断点。", }, { id: "non-execute-source-write", level: "hard-deny", title: "非 execute 阶段拒绝源码写入", description: "生产源码只能在 execute 阶段写入,并且必须满足阶段门禁、PLAN、source anchor 和后置条件。", }, { id: "subagent-main-state-mutation", level: "hard-deny", title: "子 agent 不得推进主状态机", description: "子 agent 不能创建子 agent、推进主 run 阶段、记录主验证结果或修改非授权文件。", }, { id: "credential-persistence", level: "hard-deny", title: "拒绝持久化凭证", description: "禁止把数据库密码、连接串、Token 或密钥写入仓库、阶段文档、evidence、prompt 或提交说明。", }, { id: "plan-scope-change", level: "soft-deny", title: "PLAN 范围外新增文件需先修订计划", description: "新增 PLAN 未列出的文件时,先更新计划并重新刷新门禁。", }, { id: "repair-limit", level: "soft-deny", title: "验证修复达到上限后需用户决策", description: "连续验证失败达到修复上限后,继续修复、回到 plan 或停止必须有明确决策。", }, { id: "external-verification", level: "soft-deny", title: "外部验证缺失需用户提供证据", description: "本机无法执行的 BOS 注册、外部系统和生产验证必须由用户提供可核验证据。", }, { id: "sdk-signature-evidence", level: "evidence-required", title: "SDK 签名证据", description: "SDK 类、方法、构造器、枚举和属性必须来自当前项目 jar/dll、构建输出或官方元数据证据。", }, { id: "metadata-data-source-evidence", level: "evidence-required", title: "元数据和数据源证据", description: "FormId、字段/实体、表名、插件事件和读写方式必须来自结构化 facts 或 evidence,PLAN 自由文本不能替代。", }, { id: "verification-evidence", level: "evidence-required", title: "真实验证证据", description: "TDD red/green、VERIFY 和 ship 需要真实命令、Exit 和关键输出;待验证不能作为绿灯。", }, { id: "risk-disclosure", level: "evidence-required", title: "交付风险说明", description: "ship 前必须记录风险等级、风险原因和无法本地复现的残余风险。", }, { id: "context-budget-truncated", level: "warning", title: "上下文预算裁剪", description: "Context budget 发生裁剪时,被裁剪内容必须读取本地文件、ledger 或 evidence 后才能作为完整事实使用。", }, { id: "tool-result-summary-incomplete", level: "warning", title: "工具结果摘要不完整", description: "Tool Result Contract 摘要不完整时只能作为线索,不能替代 facts、metadata evidence 或验证证据。", }, { id: "deep-context-stale", level: "warning", title: "Deep Context 过期", description: "Deep Context 过期或缺失时先刷新项目上下文,禁止凭旧目录约定编码。", }, { id: "multiple-run-confusion", level: "warning", title: "多个 run 可能混淆", description: "多个相关 run 并存时必须确认 active run,禁止把其他 run 的事实当作当前事实。", }, ]; export function formatRiskPolicyForPrompt(cwd: string, run?: ActiveRun): string { return [ "## KCode Risk Policy(固定安全段,压缩/交接时不可裁剪)", "", ...formatPolicyGroups(), "", "## 当前 Run 风险信号", formatCurrentRiskSignals(cwd, run), ].join("\n"); } export function formatCompactRiskPolicyForPrompt(cwd: string, run?: ActiveRun, options: { detail?: boolean } = {}): string { const activeRules = activeRiskRules(cwd, run); if (run?.mode === "quick" && !options.detail) { return [ "## KCode Risk Policy(轻量摘要)", "- 简单插件只保留少数硬红线:工作区内写入、execute 阶段、PLAN 批准文件、凭证不得持久化、禁止猜路径。", `- active rules:${activeRules.map((rule) => `${rule.id}[${rule.level}]`).join(", ")}`, "- 元数据/SDK/字段事实优先自动查;查不到不编造,进入 UAT/验证闭环。", "## 当前 Run 风险信号", formatCurrentRiskSignals(cwd, run), ].join("\n"); } if (!options.detail) { return [ "## KCode Risk Policy(轻量摘要)", "- 质量门禁仍由 run.facts、evidence、gate、write transaction、source anchor 和 path policy 强制执行;摘要不会降低任何 hard-deny。", `- active rules:${activeRules.map((rule) => `${rule.id}[${rule.level}]`).join(", ")}`, "- 关键红线:生产源码只在 execute 阶段写入;凭证不得持久化;禁止猜测路径;Windows 不假设 bash。", "## 当前 Run 风险信号", formatCurrentRiskSignals(cwd, run), ].join("\n"); } return [ "## KCode Risk Policy(轻量摘要)", "- 质量门禁仍由 run.facts、evidence、gate、write transaction、source anchor 和 path policy 强制执行;摘要不会降低任何 hard-deny。", ...activeRules.map((rule) => `- ${rule.id} [${rule.level}]:${rule.title}。${rule.description}`), "## 当前 Run 风险信号", formatCurrentRiskSignals(cwd, run), ].join("\n"); } function activeRiskRules(cwd: string, run?: ActiveRun): KdRiskPolicyRule[] { if (!run) return rulesById(["multiple-run-confusion", "workspace-external-source-write", "credential-persistence"]); const gate = inspectGate(cwd, run); const openQuestions = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking); const ids = new Set(["workspace-external-source-write", "credential-persistence"]); if (openQuestions.length > 0 || run.resumeSnapshot?.status === "open") ids.add("open-question-or-resume-write"); if (run.phase !== "execute") ids.add("non-execute-source-write"); if (!gate.passed) { ids.add("metadata-data-source-evidence"); ids.add("verification-evidence"); } if (run.mode === "normal" || run.riskAssessment?.level === "high") { ids.add("external-verification"); ids.add("risk-disclosure"); } if ((run.writeTransactions?.length ?? 0) > 0) ids.add("write-transaction-bypass"); if ((run.sourceAnchors?.length ?? 0) > 0 || run.phase === "execute") ids.add("sdk-signature-evidence"); return rulesById([...ids]); } function rulesById(ids: string[]): KdRiskPolicyRule[] { const byId = new Map(RISK_POLICY_RULES.map((rule) => [rule.id, rule])); return ids.flatMap((id) => { const rule = byId.get(id); return rule ? [rule] : []; }); } function formatPolicyGroups(): string[] { const labels: Record = { "hard-deny": "Hard Deny(不可由普通确认放行)", "soft-deny": "Soft Deny(需明确授权或修订计划)", "evidence-required": "Evidence Required(阻断阶段推进或交付)", warning: "Warning(提示但不直接阻断)", }; const levels: KdRiskPolicyLevel[] = ["hard-deny", "soft-deny", "evidence-required", "warning"]; return levels.flatMap((level) => [ `### ${labels[level]}`, ...RISK_POLICY_RULES.filter((rule) => rule.level === level).map((rule) => `- ${rule.id}:${rule.title}。${rule.description}`), "", ]); } function formatCurrentRiskSignals(cwd: string, run?: ActiveRun): string { if (!run) return "- 当前没有 active run;禁止根据旧会话记忆直接编码。"; const gate = inspectGate(cwd, run); const openQuestions = (run.questions ?? []).filter((question) => question.status === "open" && question.blocking); return [ `- phase/mode:${run.phase}/${run.mode}`, `- gate:${gate.passed ? "passed" : `blocked:${gate.reason ?? "未说明原因"}`}`, `- open blocking question:${openQuestions.length > 0 ? openQuestions.map((question) => question.id).join(", ") : "无"}`, `- resumeSnapshot:${run.resumeSnapshot?.status === "open" ? `${run.resumeSnapshot.pendingQuestionId ?? "open"}:${run.resumeSnapshot.nextAction}` : "无 open 断点"}`, `- writeTransactions:${run.writeTransactions?.length ?? 0}`, `- sourceAnchors:${run.sourceAnchors?.length ?? 0}`, ].join("\n"); }