// Package hooks 与 Engine 的集成点. // // 提供构建 hook 环境变量的辅助函数. // 为每种 hook 类型提供环境变量注入逻辑, // Go 版本抽取为独立的 builder 函数,更清晰也更容易测试. package hooks import ( "encoding/json" "fmt" "os" "runtime" ) // BuildPreToolEnv 构建 pre_tool_use hook 的环境变量. // // 注入的变量: // - HOOK_TYPE: "pre_tool_use" // - TOOL_NAME: 工具名称 // - TOOL_INPUT: 工具输入参数的 JSON 字符串 // - PROJECT_ROOT: 项目根目录 // - PLATFORM: 操作系统 func BuildPreToolEnv(toolName string, input any, projectRoot string) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(HookPreToolUse) env["TOOL_NAME"] = toolName env["TOOL_INPUT"] = marshalJSON(input) return env } // BuildPostToolEnv 构建 post_tool_use / post_tool_use_failure hook 的环境变量. // // 注入的变量(在 pre 的基础上增加): // - TOOL_OUTPUT: 工具输出内容 // - TOOL_IS_ERROR: "true" 或 "false" // - HOOK_TYPE: "post_tool_use" 或 "post_tool_use_failure" func BuildPostToolEnv(toolName string, input any, output string, isError bool, projectRoot string) map[string]string { hookType := HookPostToolUse if isError { hookType = HookPostToolUseFailure } env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(hookType) env["TOOL_NAME"] = toolName env["TOOL_INPUT"] = marshalJSON(input) env["TOOL_OUTPUT"] = output env["TOOL_IS_ERROR"] = fmt.Sprintf("%t", isError) return env } // BuildSessionEnv 构建 session_start / session_end hook 的环境变量. // // 注入的变量: // - SESSION_ID: 会话 ID // - PROJECT_ROOT: 项目根目录 // - HOOK_TYPE: "session_start" 或 "session_end" func BuildSessionEnv(sessionID string, projectRoot string, hookType HookType) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(hookType) env["SESSION_ID"] = sessionID return env } // BuildPermissionEnv 构建 permission_request / permission_denied hook 的环境变量. // // 注入的变量: // - TOOL_NAME: 请求权限的工具名称 // - PERMISSION_REASON: 请求权限的原因描述 // - HOOK_TYPE: "permission_request" 或 "permission_denied" func BuildPermissionEnv(toolName string, reason string, projectRoot string, hookType HookType) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(hookType) env["TOOL_NAME"] = toolName env["PERMISSION_REASON"] = reason return env } // BuildNotificationEnv 构建 notification hook 的环境变量. func BuildNotificationEnv(message string, level string, projectRoot string) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(HookNotification) env["NOTIFICATION_MESSAGE"] = message env["NOTIFICATION_LEVEL"] = level return env } // BuildTaskEnv 构建 task_created / task_completed hook 的环境变量. func BuildTaskEnv(taskID string, taskDescription string, projectRoot string, hookType HookType) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(hookType) env["TASK_ID"] = taskID env["TASK_DESCRIPTION"] = taskDescription return env } // BuildPreSamplingEnv 构建 pre_sampling hook 的环境变量. // // pre_sampling hook 在 API 调用前触发(同步),可通过 exit 2 阻止本轮 API 调用. // // 注入的变量: // - HOOK_TYPE: "pre_sampling" // - MODEL: 当前使用的模型 ID // - TURN: 当前对话轮次(从 1 开始) // - MESSAGE_COUNT: 消息历史条数(不含当前轮新增) // - PROJECT_ROOT: 项目根目录 // - PLATFORM / ARCH / CWD: 基础环境(由 baseEnv 注入) // // 升华改进(ELEVATED): 只传轻量元数据,不传完整消息列表-- // 消息列表可能数百 KB,放进环境变量会触发 "argument list too long" 错误(Linux 默认 ARG_MAX=2MB). // 需要完整内容的场景应使用 SDK 模式的 CallbackHandler(直接接收 Go 结构体,无此限制). // 替代方案:<将 MESSAGES_JSON 注入 env> - 否决原因:单条上下文 > 128KB 即可能触发 execve 限制. func BuildPreSamplingEnv(model string, turnCount int, messageCount int, projectRoot string) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(HookPreSampling) env["MODEL"] = model env["TURN"] = fmt.Sprintf("%d", turnCount) env["MESSAGE_COUNT"] = fmt.Sprintf("%d", messageCount) return env } // BuildPostSamplingEnv 构建 post_sampling hook 的环境变量. // // post_sampling hook 在 API 响应后,工具执行前触发(异步 fire-and-forget),不影响控制流. // // 注入的变量: // - HOOK_TYPE: "post_sampling" // - MODEL: 使用的模型 ID // - TURN: 当前对话轮次 // - INPUT_TOKENS: 本轮输入 token 数(API 返回的精确值) // - OUTPUT_TOKENS: 本轮输出 token 数 // - STOP_REASON: 模型停止原因("end_turn"/"tool_use"/"max_tokens"/"stop_sequence") // - RESPONSE_PREVIEW: 助手回复文本的前 500 字节(截断,防止 env 过大) // - PROJECT_ROOT / PLATFORM / ARCH / CWD: 基础环境 // // 精妙之处(CLEVER): RESPONSE_PREVIEW 截断 500 字节而非字符-- // 多字节 UTF-8 字符截断在字节边界可能生成无效字符串, // 但 hook 脚本通常是文本处理工具,短暂的末尾乱码无实质影响. // 若需要完整响应,使用 SDK 模式的 CallbackHandler. func BuildPostSamplingEnv(model string, turnCount int, inputTokens, outputTokens int, stopReason string, responsePreview string, projectRoot string) map[string]string { env := baseEnv(projectRoot) env["HOOK_TYPE"] = string(HookPostSampling) env["MODEL"] = model env["TURN"] = fmt.Sprintf("%d", turnCount) env["INPUT_TOKENS"] = fmt.Sprintf("%d", inputTokens) env["OUTPUT_TOKENS"] = fmt.Sprintf("%d", outputTokens) env["STOP_REASON"] = stopReason // 截断到前 500 字节,防止超大响应撑爆环境变量 const previewMaxBytes = 500 if len(responsePreview) > previewMaxBytes { responsePreview = responsePreview[:previewMaxBytes] } env["RESPONSE_PREVIEW"] = responsePreview return env } // ParsePermissionResponse 从 hook 执行结果中解析权限决策. // // 原项目的 permission_request hook 可以返回 JSON 来自动批准/拒绝: // // {"decision": "allow"} → 自动批准 // {"decision": "deny"} → 自动拒绝 // {"decision": "deny", "reason": "..."} → 带原因拒绝 // // 返回值: // - decision: "allow", "deny", 或 ""(未决定,交给用户) // - reason: 拒绝原因(仅 deny 时有效) func ParsePermissionResponse(results *ExecuteResults) (decision string, reason string) { if results == nil || len(results.Results) == 0 { return "", "" } // 遍历所有结果,查找包含 decision 的 JSON 输出 for _, r := range results.Results { if r.JSONOutput == nil { continue } if d, ok := r.JSONOutput["decision"]; ok { if ds, ok := d.(string); ok { decision = ds // 提取可选的 reason 字段 if reasonVal, ok := r.JSONOutput["reason"]; ok { if rs, ok := reasonVal.(string); ok { reason = rs } } return decision, reason } } } return "", "" } // ============================================================ // Hook 输出解析 -- 从结果中提取控制指令 // ============================================================ // ExitCodeBlock 是 "有意阻止" 的退出码. // 精妙之处(CLEVER): exit 1 = "hook 自己出错了"(fail-open,不阻止). // exit 2 = "hook 有意阻止此操作"(用户明确的安全决策). // 这个区分很重要--写错的 hook 脚本(exit 1)不应该让 Agent 瘫痪. // 替代方案:<所有非零退出码都 block--一个写错的 hook 就瘫痪整个系统> const ExitCodeBlock = 2 // ParseToolHookResponse 解析 pre/post_tool_use hook 的控制指令. // // 两种阻止方式: // 1. Exit code 2 → blocked(shell 模式自然表达) // 2. JSON {"decision":"block"} → blocked(结构化表达) // // 返回: // - blocked: 是否阻止工具执行 // - reason: 阻止原因(来自 JSON 的 reason 字段,或 stderr) func ParseToolHookResponse(results *ExecuteResults) (blocked bool, reason string) { if results == nil || len(results.Results) == 0 { return false, "" } for _, r := range results.Results { // Exit code 2 = 有意阻止 if r.ExitCode == ExitCodeBlock { reason := r.Stderr if reason == "" { reason = r.Stdout } return true, reason } // JSON {"decision":"block"} if r.JSONOutput != nil { if d, ok := r.JSONOutput["decision"]; ok { if ds, ok := d.(string); ok && ds == "block" { blockReason := "" if rv, ok := r.JSONOutput["reason"]; ok { if rs, ok := rv.(string); ok { blockReason = rs } } return true, blockReason } } } } return false, "" } // ParseStopHookResponse 解析 stop hook 的控制指令. // 任何 hook 返回非零退出码 → 应该停止 Agent 循环. // 精妙之处(CLEVER): stop hook 的语义与 tool hook 不同-- // stop hook 的"失败"就是"应该停"(反直觉但合理: // hook 脚本检查某个条件,条件不满足就 exit 1 表示"该停了"). func ParseStopHookResponse(results *ExecuteResults) (shouldStop bool, reason string) { if results == nil || len(results.Results) == 0 { return false, "" } for _, r := range results.Results { if r.ExitCode != 0 { reason := r.Stderr if reason == "" { reason = r.Stdout } return true, reason } // JSON {"decision":"stop"} if r.JSONOutput != nil { if d, ok := r.JSONOutput["decision"]; ok { if ds, ok := d.(string); ok && ds == "stop" { stopReason := "" if rv, ok := r.JSONOutput["reason"]; ok { if rs, ok := rv.(string); ok { stopReason = rs } } return true, stopReason } } } } return false, "" } // ParsePostCompactHookResponse 提取 post_compact hook 的补充摘要. // 精妙之处(CLEVER): hook stdout 作为补充摘要追加到压缩结果. // 注入文本带来源标记 [hook: post_compact],让模型和日志能区分来源. // 空 stdout 或 hook 失败时返回空字符串(不注入). func ParsePostCompactHookResponse(results *ExecuteResults) string { if results == nil || len(results.Results) == 0 { return "" } var summaries []string for _, r := range results.Results { if r.Success() && r.Stdout != "" { summaries = append(summaries, r.Stdout) } } if len(summaries) == 0 { return "" } // 带来源标记,让模型和日志能区分 combined := "" for _, s := range summaries { combined += s + "\n" } return "[hook: post_compact]\n" + combined } // baseEnv 构建所有 hook 共用的基础环境变量. func baseEnv(projectRoot string) map[string]string { env := map[string]string{ "PROJECT_ROOT": projectRoot, "PLATFORM": runtime.GOOS, "ARCH": runtime.GOARCH, } // 注入当前工作目录 if cwd, err := os.Getwd(); err == nil { env["CWD"] = cwd } return env } // marshalJSON 将值序列化为 JSON 字符串. // 失败时返回空字符串(hook 环境变量不应导致 panic). func marshalJSON(v any) string { if v == nil { return "{}" } // 如果已经是字符串,直接返回 if s, ok := v.(string); ok { return s } data, err := json.Marshal(v) if err != nil { return "{}" } return string(data) }