import type { BashToolCallEvent, CustomToolCallEvent, EditToolCallEvent, FindToolCallEvent, GrepToolCallEvent, LsToolCallEvent, ReadToolCallEvent, ToolCallEvent, WriteToolCallEvent, } from "@earendil-works/pi-coding-agent"; /** * 本地实现,避免运行时导入 pi-coding-agent(宿主导入需加载整个包,~3.7s)。 * 与上游 isToolCallEventType 等价的类型守卫重载(type-only 导入编译后擦除)。 */ function isToolCallEventType( toolName: "bash", event: ToolCallEvent ): event is BashToolCallEvent; function isToolCallEventType( toolName: "read", event: ToolCallEvent ): event is ReadToolCallEvent; function isToolCallEventType( toolName: "edit", event: ToolCallEvent ): event is EditToolCallEvent; function isToolCallEventType( toolName: "write", event: ToolCallEvent ): event is WriteToolCallEvent; function isToolCallEventType( toolName: "grep", event: ToolCallEvent ): event is GrepToolCallEvent; function isToolCallEventType( toolName: "find", event: ToolCallEvent ): event is FindToolCallEvent; function isToolCallEventType( toolName: "ls", event: ToolCallEvent ): event is LsToolCallEvent; function isToolCallEventType< TName extends string, TInput extends Record, >( toolName: TName, event: ToolCallEvent ): event is CustomToolCallEvent & { toolName: TName; input: TInput; }; function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean { return event.toolName === toolName; } import type { CompiledPathPolicy, DamageControlEngine, DamageControlPolicy, DamageControlRuntimeState, FileAccess, FileCheckResult, FileIntent, } from "ast-guard-core"; import { evaluateFileAccess, syntheticFileIntent } from "ast-guard-core"; const PATCH_TARGET_LINE_SPECS = [ { access: "write", prefix: "*** Update File: " }, { access: "write", prefix: "*** Add File: " }, { access: "delete", prefix: "*** Delete File: " }, ] as const; const UNPARSED_PATCH_REASON = "apply_patch file targets could not be parsed"; const UNPARSED_PATCH_TARGET = ""; export interface ToolAdapterContext { compiledPathPolicy: CompiledPathPolicy; cwd: string; engine: DamageControlEngine; policy: DamageControlPolicy; runtime: DamageControlRuntimeState; /** 会话决策层:命中时跳过违规(本会话允许覆盖的路径)。 */ sessionAllow: (ruleId: string, matchedPath?: string) => boolean; } interface ToolAdapterResultBase { input: string; toolName: string; } export type ToolAdapterResult = | (ToolAdapterResultBase & { result: ReturnType; }) | (ToolAdapterResultBase & { result: FileCheckResult }) | undefined; function combineFileResults(results: FileCheckResult[]): FileCheckResult { let decision: FileCheckResult["decision"] = "allow"; if (results.some((result) => result.decision === "block")) { decision = "block"; } else if (results.some((result) => result.decision === "ask")) { decision = "ask"; } return { decision, fileIntents: results.flatMap((result) => result.fileIntents), pathViolations: results.flatMap((result) => result.pathViolations), reasons: [...new Set(results.flatMap((result) => result.reasons))], }; } function pathInput(path: string): string { return path; } interface PatchTarget { access: FileAccess; path: string; } function patchTargets(patch: string): PatchTarget[] { const targets: PatchTarget[] = []; for (const line of patch.split("\n")) { for (const spec of PATCH_TARGET_LINE_SPECS) { if (line.startsWith(spec.prefix)) { targets.push({ access: spec.access, path: line.slice(spec.prefix.length).trim(), }); } } } return targets; } function unparsedPatchResult(): FileCheckResult { const intent: FileIntent = syntheticFileIntent({ access: "write", dynamic: true, path: UNPARSED_PATCH_TARGET, toolName: "apply_patch", }); return { decision: "ask", fileIntents: [intent], pathViolations: [ { action: "ask", intent, matchedPath: UNPARSED_PATCH_TARGET, pattern: UNPARSED_PATCH_TARGET, priority: 0, reason: UNPARSED_PATCH_REASON, ruleId: "apply-patch-unparsed", ruleType: "zeroAccess", }, ], reasons: [UNPARSED_PATCH_REASON], }; } export function evaluateToolCall( event: ToolCallEvent, context: ToolAdapterContext ): ToolAdapterResult { if (isToolCallEventType("bash", event)) { return { input: event.input.command, toolName: "bash", result: context.engine.checkBash( event.input.command, context.runtime, context.sessionAllow ), }; } if (isToolCallEventType("read", event)) { return { input: pathInput(event.input.path), toolName: "read", result: evaluateFileAccess({ access: "read", compiledPathPolicy: context.compiledPathPolicy, cwd: context.cwd, path: event.input.path, policy: context.policy, sessionAllow: context.sessionAllow, toolName: "read", }), }; } if ( isToolCallEventType("write", event) || isToolCallEventType("edit", event) ) { return { input: pathInput(event.input.path), toolName: event.toolName, result: evaluateFileAccess({ access: "write", compiledPathPolicy: context.compiledPathPolicy, cwd: context.cwd, path: event.input.path, policy: context.policy, sessionAllow: context.sessionAllow, toolName: event.toolName, }), }; } if ( isToolCallEventType("grep", event) || isToolCallEventType("find", event) || isToolCallEventType("ls", event) ) { const path = event.input.path ?? "."; return { input: pathInput(path), toolName: event.toolName, result: evaluateFileAccess({ access: "read", compiledPathPolicy: context.compiledPathPolicy, cwd: context.cwd, path, policy: context.policy, sessionAllow: context.sessionAllow, toolName: event.toolName, }), }; } if ( event.toolName === "apply_patch" && typeof event.input.patch === "string" ) { const targets = patchTargets(event.input.patch); if (targets.length > 0) { const results = targets.map((target) => evaluateFileAccess({ access: target.access, compiledPathPolicy: context.compiledPathPolicy, cwd: context.cwd, path: target.path, policy: context.policy, sessionAllow: context.sessionAllow, toolName: event.toolName, }) ); return { input: targets.map((target) => target.path).join("\n"), toolName: "apply_patch", result: combineFileResults(results), }; } return { input: "", toolName: "apply_patch", result: unparsedPatchResult(), }; } return; }