hooks

package
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: None detected not legal advice Imports: 0 Imported by: 0

Documentation

Overview

Package hooks implements the hook system: 14 lifecycle event points (PreToolUse / PostToolUse / PreCompact / UserPromptSubmit etc.) the engine fires callbacks at, with three backend modes (shell command / Go callback / planned webhook) so the same HookDef works across CLI / SDK / API consumption models.

Consumer integration surfaces fall into three shapes. For the full taxonomy see `docs/api-reference.md` section "API 消费形态 / API Consumption Patterns".

Synchronous callback — form 3 (the only shape this package exposes):

  • HookHandler.ExecuteHook: Manager.Execute calls per registered hook for a given HookType, blocks for *HookResult (Decision allow/deny/ask, Message, etc.)
  • Three concrete backends:
  • ShellHandler: consumer writes a shell command in HookDef.Command; engine execs, reads stdout/stderr, parses exit code.
  • CallbackHandler: consumer provides a Go func (for SDK embedding).
  • WebhookHandler: POSTs to a URL (planned).

Package hooks 实现 hook 系统: 14 个生命周期事件点 (PreToolUse / PostToolUse / PreCompact / UserPromptSubmit 等) 引擎触发回调, 支持三种后端 (shell 命令 / Go 回调 / 规划中的 webhook), 让同一个 HookDef 在 CLI / SDK / API 三种消费模式下都工作.

消费者接入面分三种形态. 完整分类见 `docs/api-reference.md` "API 消费形态 / API Consumption Patterns" 章节.

同步回调 (callback) —— 形态三 (本包只暴露这一种):

  • HookHandler.ExecuteHook: Manager.Execute 对每个注册 hook 按 HookType 调用, 阻塞等 *HookResult (Decision allow/deny/ask, Message 等)
  • 三种具体后端:
  • ShellHandler: 消费者在 HookDef.Command 写 shell 命令; 引擎 exec, 读 stdout/stderr, 解析 exit code.
  • CallbackHandler: 消费者传 Go 函数 (用于 SDK 嵌入).
  • WebhookHandler: POST 到 URL (规划中).

Package hooks 的命令执行器.

走 execenv.Executor 统一门框 (M1 方案 β), 本地 CLI 映射到 DefaultExecutor (零开销包装 os/exec), 云端 SaaS 由 platform sandbox.Backend 接管, 本文件 对两种后端无差别.

历史

原本是 executor struct + newExecutor 的 OO 封装, 迁移到方案 β 后变成 纯函数 runShellHook: hooks 包不再管进程生命周期, 只负责把 HookDef 翻译 成 execenv.Spec, 剩下交给 Executor. Manager 和 ShellHandler 分别持有 execenv.Executor, 两条调用路径各自传入.

Package hooks 实现 Hook 系统.

Hook 系统的核心实现. Hook 系统允许用户在 Agent 的关键生命周期点注入自定义 shell 命令.

原项目的问题:

  • 12+ 种 hook 类型全塞一个 5000 行文件
  • 每种 hook 的执行逻辑重复但各有特殊分支
  • 同步/异步执行混在一起,错误处理不一致

Go 版本的设计:

  • Manager 统一管理所有 hook 的注册和触发
  • types.go 定义所有类型和结果结构
  • executor.go 封装命令执行(环境注入,超时,输出捕获)
  • integration.go 提供与 Engine 的集成辅助函数
  • 同步/异步执行有明确的 API 分离

Package hooks 与 Engine 的集成点.

提供构建 hook 环境变量的辅助函数. 为每种 hook 类型提供环境变量注入逻辑, Go 版本抽取为独立的 builder 函数,更清晰也更容易测试.

Package hooks 的类型定义.

定义所有 12 种 hook 类型常量,执行结果结构,环境变量构建器. Hook 系统的类型定义.

Index

Constants

View Source
const DefaultTimeout = 30

DefaultTimeout 是 hook 命令执行的默认超时时间(秒).

View Source
const ExitCodeBlock = 2

ExitCodeBlock 是 "有意阻止" 的退出码. 精妙之处(CLEVER): exit 1 = "hook 自己出错了"(fail-open,不阻止). exit 2 = "hook 有意阻止此操作"(用户明确的安全决策). 这个区分很重要--写错的 hook 脚本(exit 1)不应该让 Agent 瘫痪. 替代方案:<所有非零退出码都 block--一个写错的 hook 就瘫痪整个系统>

Variables

This section is empty.

Functions

func BuildNotificationEnv

func BuildNotificationEnv(message string, level string, projectRoot string) map[string]string

BuildNotificationEnv 构建 notification hook 的环境变量.

func BuildPermissionEnv

func BuildPermissionEnv(toolName string, reason string, projectRoot string, hookType HookType) map[string]string

BuildPermissionEnv 构建 permission_request / permission_denied hook 的环境变量.

注入的变量:

  • TOOL_NAME: 请求权限的工具名称
  • PERMISSION_REASON: 请求权限的原因描述
  • HOOK_TYPE: "permission_request" 或 "permission_denied"

func BuildPostSamplingEnv

func BuildPostSamplingEnv(model string, turnCount int, inputTokens, outputTokens int, stopReason string, responsePreview string, projectRoot string) map[string]string

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 BuildPostToolEnv

func BuildPostToolEnv(toolName string, input any, output string, isError bool, projectRoot string) map[string]string

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 BuildPreSamplingEnv

func BuildPreSamplingEnv(model string, turnCount int, messageCount int, projectRoot string) map[string]string

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 BuildPreToolEnv

func BuildPreToolEnv(toolName string, input any, projectRoot string) map[string]string

BuildPreToolEnv 构建 pre_tool_use hook 的环境变量.

注入的变量:

  • HOOK_TYPE: "pre_tool_use"
  • TOOL_NAME: 工具名称
  • TOOL_INPUT: 工具输入参数的 JSON 字符串
  • PROJECT_ROOT: 项目根目录
  • PLATFORM: 操作系统

func BuildSessionEnv

func BuildSessionEnv(sessionID string, projectRoot string, hookType HookType) map[string]string

BuildSessionEnv 构建 session_start / session_end hook 的环境变量.

注入的变量:

  • SESSION_ID: 会话 ID
  • PROJECT_ROOT: 项目根目录
  • HOOK_TYPE: "session_start" 或 "session_end"

func BuildTaskEnv

func BuildTaskEnv(taskID string, taskDescription string, projectRoot string, hookType HookType) map[string]string

BuildTaskEnv 构建 task_created / task_completed hook 的环境变量.

func ParsePermissionResponse

func ParsePermissionResponse(results *ExecuteResults) (decision string, reason string)

ParsePermissionResponse 从 hook 执行结果中解析权限决策.

原项目的 permission_request hook 可以返回 JSON 来自动批准/拒绝:

{"decision": "allow"}           → 自动批准
{"decision": "deny"}            → 自动拒绝
{"decision": "deny", "reason": "..."} → 带原因拒绝

返回值:

  • decision: "allow", "deny", 或 ""(未决定,交给用户)
  • reason: 拒绝原因(仅 deny 时有效)

func ParsePostCompactHookResponse

func ParsePostCompactHookResponse(results *ExecuteResults) string

ParsePostCompactHookResponse 提取 post_compact hook 的补充摘要. 精妙之处(CLEVER): hook stdout 作为补充摘要追加到压缩结果. 注入文本带来源标记 [hook: post_compact],让模型和日志能区分来源. 空 stdout 或 hook 失败时返回空字符串(不注入).

func ParseStopHookResponse

func ParseStopHookResponse(results *ExecuteResults) (shouldStop bool, reason string)

ParseStopHookResponse 解析 stop hook 的控制指令. 任何 hook 返回非零退出码 → 应该停止 Agent 循环. 精妙之处(CLEVER): stop hook 的语义与 tool hook 不同-- stop hook 的"失败"就是"应该停"(反直觉但合理: hook 脚本检查某个条件,条件不满足就 exit 1 表示"该停了").

func ParseToolHookResponse

func ParseToolHookResponse(results *ExecuteResults) (blocked bool, reason string)

ParseToolHookResponse 解析 pre/post_tool_use hook 的控制指令.

两种阻止方式:

  1. Exit code 2 → blocked(shell 模式自然表达)
  2. JSON {"decision":"block"} → blocked(结构化表达)

返回:

  • blocked: 是否阻止工具执行
  • reason: 阻止原因(来自 JSON 的 reason 字段,或 stderr)

func WithSessionHooksCtx

func WithSessionHooksCtx(ctx context.Context, m *Manager) context.Context

WithSessionHooksCtx 将 session hooks 注入 context(SubAgent 继承用).

升华改进(ELEVATED): SubAgent 的 context 链路中注入 session hooks-- 父 Agent 通过 WithSessionHooks RunOption 注册的 hooks, 自动传播到 SubAgent,无需调用方手动传递. 替代方案:<SubAgent 不继承 session hooks> - 否决:审计 hook 漏掉子任务,合规不完整.

Types

type CallbackHandler

type CallbackHandler struct {
	// contains filtered or unexported fields
}

CallbackHandler 通过 Go 函数回调执行 hook.

升华改进(ELEVATED): SDK 模式的核心--嵌入 Go 程序中的 Agent 不应该 为了一个 hook 而 fork shell 进程.直接调 Go 函数:

  • 零进程开销(shell fork+exec 约 1-5ms,Go 调用 <1μs)
  • 类型安全(Go 编译器检查参数类型,shell 是纯字符串)
  • 可调试(Go debugger 直接跟进 hook 函数)

用法:

hooksMgr.Register(hooks.HookPreToolUse, hooks.HookDef{
    Handler: hooks.NewCallbackHandler(func(ctx context.Context, hookType hooks.HookType, env map[string]string) *hooks.HookResult {
        if env["TOOL_NAME"] == "Bash" && strings.Contains(env["TOOL_INPUT"], "rm -rf") {
            return &hooks.HookResult{ExitCode: 2} // block
        }
        return &hooks.HookResult{ExitCode: 0}
    }),
})

替代方案:<原方案 SDK 用户也得写 shell 脚本,然后设 Command: "my-hook.sh">

func NewCallbackHandler

func NewCallbackHandler(fn func(ctx context.Context, hookType HookType, env map[string]string) *HookResult) *CallbackHandler

NewCallbackHandler 创建 Go 回调后端.

func NewCallbackHandlerWithTimeout

func NewCallbackHandlerWithTimeout(fn func(ctx context.Context, hookType HookType, env map[string]string) *HookResult, timeout time.Duration) *CallbackHandler

NewCallbackHandlerWithTimeout 创建带超时的 Go 回调后端.

func (*CallbackHandler) ExecuteHook

func (h *CallbackHandler) ExecuteHook(ctx context.Context, hookType HookType, env map[string]string) *HookResult

ExecuteHook 执行 Go 回调函数.

type CompositeManager

type CompositeManager struct {
	// contains filtered or unexported fields
}

CompositeManager 将 session-level hooks 与 engine-level hooks 组合执行.

查找规则:

  1. HasHooks:session || engine(任意一方有即返回 true)
  2. Execute:先执行 session hooks,再执行 engine hooks
  3. ExecuteAsync:同上,但异步(各自独立 goroutine)

nil session 时,行为与 engine-level Manager 完全相同(向后兼容).

type Config

type Config struct {
	Hooks map[HookType][]HookDef `json:"hooks"`
}

Config 是 hook 系统的配置. 对应原项目中 settings.json 的 hooks 字段.

type ExecuteResults

type ExecuteResults struct {
	HookType HookType      `json:"hook_type"`
	Results  []*HookResult `json:"results"`
}

ExecuteResults 是一批 hook 的执行结果.

func (*ExecuteResults) FindJSONOutput

func (er *ExecuteResults) FindJSONOutput(key string) (any, bool)

FindJSONOutput 在所有结果中查找包含指定 key 的 JSON 输出. 返回第一个匹配的值.用于提取 hook 的控制指令.

func (*ExecuteResults) FirstError

func (er *ExecuteResults) FirstError() *HookResult

FirstError 返回第一个失败的 hook 结果(如果有).

func (*ExecuteResults) HasErrors

func (er *ExecuteResults) HasErrors() bool

HasErrors 检查是否有任何 hook 执行失败.

type HookDef

type HookDef struct {
	Command    string      `json:"command,omitempty"`     // Shell 命令(CLI 模式)
	Handler    HookHandler `json:"-"`                     // Go 回调(SDK 模式,不序列化)
	WebhookURL string      `json:"webhook_url,omitempty"` // HTTP 回调 URL(API 模式,待实现)
	Timeout    int         `json:"timeout,omitempty"`     // 超时秒数,默认 30
	Async      bool        `json:"async,omitempty"`       // 是否异步执行(不阻塞主流程)

	// Source 标识 hook 的来源(模块 9.3).
	//
	// 升华改进(ELEVATED): 早期设计在 PluginHookMatcher 里用 pluginRoot/pluginId 区分来源,
	// 导致"全局 hook 注册表"和"插件 hook 注册表"必须分开维护,执行时三路合并(gh-29767).
	// 我们把来源信息内嵌进 HookDef,单一注册表即可精准移除特定来源的 hooks,
	// 不需要维护多个注册表或全量重建.
	//
	// 约定:
	//   - ""(空字符串)= 全局 hook(来自 HooksConfig 或 Manager.Register 直接调用)
	//   - 非空字符串 = 插件名(来自 plugin.Host,值为 plugin.Plugin.Name)
	//
	// 精妙之处(CLEVER): 空字符串 = 全局,向后兼容--
	// 所有现有 HookDef{Command: "..."} 字面量 Source 自动为 ""(全局),
	// 零迁移成本.UnregisterBySource("") 等价于原来的 Unregister(移除所有全局 hooks).
	//
	// 扩展点(预留,未实现):
	//   HTTP API / SaaS 多租户场景可用 "tenantID:pluginName" 前缀,
	//   UnregisterAllBySource 按前缀匹配实现租户级隔离.
	//   C# 移植对应字段:public string Source { get; init; } = ""
	Source string `json:"source,omitempty"` // 来源标识(空=全局,非空=插件名)

	// PluginDir 是插件的根目录(绝对路径).
	// 非空时,executor 将 FLYTO_PLUGIN_ROOT 注入到 hook 子进程环境变量,
	// 让 hook 脚本通过 $FLYTO_PLUGIN_ROOT/scripts/foo.py 引用插件内文件.
	//
	// 升华改进(ELEVATED): 早期设计通过 pluginRoot 参数在调用链中传递,
	// 需要修改所有 Build*Env 函数签名(破坏性变更).
	// 我们内嵌在 HookDef 里--注册时知道 pluginDir,执行时自然可用,
	// 无需修改 Build*Env 或 executor 的公共接口.
	// 替代方案:os.Setenv 全局设置(无法区分不同插件的路径).
	PluginDir string `json:"plugin_dir,omitempty"` // 插件根目录(注入 FLYTO_PLUGIN_ROOT)
}

HookDef 是单个 hook 的定义. 对应原项目中 settings.json 里每个 hook 条目.

升华改进(ELEVATED): 支持三种执行后端--

  • Command(shell):CLI 模式,JSON 配置文件使用
  • Handler(Go 回调):SDK 模式,Go 代码注册使用(json:"-" 不序列化)
  • WebhookURL(HTTP):API 模式(待实现)

优先级:Handler > Command.如果两者都设置,Handler 胜出. 替代方案:<早期方案只有 Command 一种,SDK 用户也得写 shell 脚本>

func (HookDef) EffectiveTimeout

func (d HookDef) EffectiveTimeout() time.Duration

EffectiveTimeout 返回 hook 定义的有效超时时间. 如果未设置(0),使用默认值 30 秒.

type HookHandler

type HookHandler interface {
	// ExecuteHook 执行 hook 并返回结果.
	// hookType: hook 类型(pre_tool_use 等)
	// env: 环境变量(TOOL_NAME,TOOL_INPUT 等)
	ExecuteHook(ctx context.Context, hookType HookType, env map[string]string) *HookResult
}

HookHandler 是 hook 的执行后端. 精妙之处(CLEVER): 单方法接口--Go 惯例,最小抽象. 任何 func(ctx, HookType, env) → HookResult 都可以适配.

Shape: synchronous callback. Manager.Execute calls ExecuteHook per registered hook for a given HookType and blocks for *HookResult. Three concrete backends (ShellHandler / CallbackHandler / WebhookHandler-planned) cover CLI / SDK / API consumption modes.

形态: 同步回调. Manager.Execute 对每个注册 hook 按 HookType 调 ExecuteHook 并阻塞等 *HookResult. 三种具体后端 (ShellHandler / CallbackHandler / WebhookHandler 规划中) 覆盖 CLI / SDK / API 三种消费模式.

type HookHandlerFunc

type HookHandlerFunc func(ctx context.Context, hookType HookType, env map[string]string) *HookResult

HookHandlerFunc 是函数类型的 HookHandler 适配器. 精妙之处(CLEVER): http.HandlerFunc 模式-- 任何签名匹配的函数自动满足接口,不需要包装 struct.

func (HookHandlerFunc) ExecuteHook

func (f HookHandlerFunc) ExecuteHook(ctx context.Context, hookType HookType, env map[string]string) *HookResult

ExecuteHook 实现 HookHandler 接口.

type HookResult

type HookResult struct {
	// Command 是执行的原始命令
	Command string `json:"command"`

	// Stdout 是标准输出内容
	Stdout string `json:"stdout"`

	// Stderr 是标准错误输出内容
	Stderr string `json:"stderr"`

	// ExitCode 是进程退出码(0 表示成功)
	ExitCode int `json:"exit_code"`

	// JSONOutput 是从 stdout 解析出的 JSON 对象(如果 stdout 是有效 JSON).
	// 原项目用这个机制让 hook 影响 Agent 行为,
	// 例如 permission_request hook 返回 {"decision": "allow"} 可以自动批准.
	JSONOutput map[string]any `json:"json_output,omitempty"`

	// Duration 是命令执行耗时
	Duration time.Duration `json:"duration"`

	// Error 是执行过程中的错误(超时,找不到命令等)
	Error error `json:"error,omitempty"`
}

HookResult 是单个 hook 命令的执行结果.

func (*HookResult) Success

func (r *HookResult) Success() bool

Success 返回 hook 是否执行成功(退出码为 0 且没有错误).

type HookType

type HookType string

HookType 是 hook 类型枚举. 原项目有 12+ 种 hook 类型,覆盖了工具调用,会话生命周期,权限等场景.

const (
	// 工具相关的 hook
	HookPreToolUse         HookType = "pre_tool_use"          // 工具执行前触发
	HookPostToolUse        HookType = "post_tool_use"         // 工具执行成功后触发
	HookPostToolUseFailure HookType = "post_tool_use_failure" // 工具执行失败后触发

	// 会话生命周期 hook
	HookSessionStart HookType = "session_start" // 会话开始时触发
	HookSessionEnd   HookType = "session_end"   // 会话结束时触发

	// 权限相关 hook
	HookPermission       HookType = "permission_request" // 请求权限时触发(可自动批准)
	HookPermissionDenied HookType = "permission_denied"  // 权限被拒绝时触发

	// 控制流 hook
	HookStop HookType = "stop" // Agent 停止时触发

	// 通知 hook
	HookNotification HookType = "notification" // 发送通知时触发

	// 配置变更 hook
	HookConfigChange HookType = "config_change" // 配置变更时触发

	// 任务相关 hook
	HookTaskCreated   HookType = "task_created"   // 子任务创建时触发
	HookTaskCompleted HookType = "task_completed" // 子任务完成时触发

	// 采样生命周期 hook(模块 9.2)
	//
	// 升华改进(ELEVATED): 早期设计只有 post-sampling,且仅为程序化 API(不暴露在 settings.json),
	// 用于 memory extraction / auto-dream 等内部副作用(post-sampling hook 返回 void).
	// 我们新增 pre-sampling,并将两者统一进 settings.json 可配置的 hook 体系.
	//
	// pre_sampling: API 调用前触发,同步执行,可阻止(exit 2 → 终止本轮,推送 WarningEvent).
	//   用途:请求级审计,配额控制,合规拦截,动态路由(SDK 模式 CallbackHandler 切换模型).
	//
	// post_sampling: API 响应后,工具执行前触发,异步 fire-and-forget,不影响控制流.
	//   用途:响应记录,内存提取,分析埋点.
	//
	// 精妙之处(CLEVER): pre 同步 / post 异步的非对称设计--
	//   pre 需要拦截能力(同步返回决策),post 只做副作用(不能阻塞用户等待 AI 响应).
	//   如果 post 也同步,一个慢 hook 脚本会让每轮 API 响应后都多等 N 秒.
	// 替代方案:两者都同步(统一简单)- 否决:post-sampling 主路径性能不可接受.
	HookPreSampling  HookType = "pre_sampling"  // API 调用前触发(同步,可阻止)
	HookPostSampling HookType = "post_sampling" // API 响应后触发(异步,fire-and-forget)
)

func AllHookTypes

func AllHookTypes() []HookType

AllHookTypes 返回所有支持的 hook 类型列表.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager 管理所有 hook 的注册和触发.

线程安全:内部用 RWMutex 保护 hook 注册表, 支持并发执行不同类型的 hook.

Executor 依赖 (M1 方案 β 严格 DI)

Manager 持有 execenv.Executor 用于启动 shell hook 子进程. 本地 CLI 场景 由 engine.New 把 cfg.Executor (默认 execenv.DefaultExecutor{}) 注入, 云端 SaaS 场景由 platform 层注入 sandbox.Backend. Manager 自己不知道也不关心 后端是哪个, 只负责把 Class/Spec 透传下去.

强制约定: executor 字段永远 non-nil. NewManager 会校验, nil 直接 panic -- 严格 DI 不做 fallback, 避免历史上的 "mock hooks 绕过 executor 路径" 漏网之鱼.

func NewCompositeManager

func NewCompositeManager(session, engine *Manager) *Manager

NewCompositeManager 创建 CompositeManager.

精妙之处(CLEVER): session 为 nil 时不创建包装对象,直接返回 engine Manager-- 避免零值 session 带来的双重 nil 检查开销,热路径性能不降级. 调用方始终收到非 nil 返回值,无需后续 nil 守卫. 替代方案:<总是创建 CompositeManager,session=nil 时内部判断> - 否决:每次 HasHooks/Execute 都多一层空指针判断,高频路径不必要开销.

func NewManager

func NewManager(cfg *Config, executor execenv.Executor) *Manager

NewManager 创建 hook 管理器.

如果 cfg 为 nil,创建一个空的管理器(没有预注册的 hook). 消费层后续可以用 Register 动态添加 hook.

executor 参数是**必填**, nil 会 panic. 严格 DI 契约 (M1 方案 β) 不做 fallback 到 execenv.DefaultExecutor{}: 本地 CLI 场景由 engine.New 注入, 云端 SaaS 场景由 platform 层注入 sandbox.Backend. 绕过 DI 让测试用 nil 会让 "shell hook 在沙盒里真的会走后端" 的合同静默失守.

测试代码直接传 execenv.DefaultExecutor{} 即可 (零开销包装 os/exec, 和 老路径 bit-identical, 详见 core/pkg/execenv bench).

func ResolveHooksMgr

func ResolveHooksMgr(session, engine *Manager) *Manager

ResolveHooksMgr 为本次 Run 解析有效的 Hook 管理器.

升华改进(ELEVATED): 单一函数封装"session/engine 合并"决策-- 所有触发点(session_start/end, pre/post_tool_use, pre/post_sampling, stop) 都调用此函数,无需在每个触发点重复判断 session hooks 是否存在.

参数:

  • session: session-level Manager(来自 runConfig.sessionHooks,可为 nil)
  • engine: engine-level Manager(来自 Engine.hooksMgr,可为 nil)

精妙之处(CLEVER): engine=nil 时也安全--返回 nil,调用方的 nil 守卫正常工作. session!=nil && engine=nil 时,直接用 session Manager(稀有但合法场景).

历史包袱(LEGACY): 早期设计中所有触发点直接引用 e.hooksMgr, 替换为 ResolveHooksMgr(runCfg.sessionHooks, e.hooksMgr) 即可升级.

func SessionHooksFromCtx

func SessionHooksFromCtx(ctx context.Context) *Manager

SessionHooksFromCtx 从 context 取 session hooks(可能为 nil).

func (*Manager) Count

func (m *Manager) Count(hookType HookType) int

Count 返回指定类型已注册的 hook 数量.

func (*Manager) Disable

func (m *Manager) Disable()

Disable 全局禁用 hook 执行(测试用).

func (*Manager) Enable

func (m *Manager) Enable()

Enable 重新启用 hook 执行.

func (*Manager) Execute

func (m *Manager) Execute(ctx context.Context, hookType HookType, env map[string]string) (*ExecuteResults, error)

Execute 同步执行指定类型的所有 hook.

按注册顺序依次执行,每个 hook 都会执行(即使前一个失败). 返回所有 hook 的执行结果,调用方可以检查是否有错误.

env 是注入到 shell 命令的环境变量(通过 integration.go 的 Build* 函数构建).

精妙之处(CLEVER): fail-open 策略--单个 hook 失败不阻断其余 hook 的执行. 如果 fail-close,一个写错的 hook 脚本就会让整个 Agent 瘫痪. 结果按执行顺序返回,调用方可以检查每个 hook 的状态. 如果没有注册的 hook,返回空结果(不是错误).

func (*Manager) ExecuteAll

func (m *Manager) ExecuteAll(ctx context.Context, hookType HookType, env map[string]string) (syncResults *ExecuteResults, asyncResults <-chan *ExecuteResults)

ExecuteAll 执行指定类型的所有 hook(包括同步和异步的).

同步 hook 按顺序执行并等待结果; 异步 hook 在后台执行,结果通过 asyncResults channel 返回.

返回值:

  • syncResults: 同步 hook 的执行结果
  • asyncResults: 异步 hook 的结果 channel(可以 select 或忽略)

func (*Manager) ExecuteAsync

func (m *Manager) ExecuteAsync(hookType HookType, env map[string]string) <-chan *ExecuteResults

ExecuteAsync 异步执行指定类型的所有 hook(不阻塞主流程).

适用于不需要等待结果的 hook,如 notification,session_end 等. 返回一个 channel,异步完成后会收到结果. 调用方可以选择等待结果或忽略.

设计决策:

  • 使用 goroutine 执行,不阻塞调用方
  • 使用 background context(不受调用方 context 取消影响) 因为异步 hook 通常在会话结束时触发,此时 context 可能已取消
  • 设置独立的超时(每个 hook 的 EffectiveTimeout)

func (*Manager) HasHooks

func (m *Manager) HasHooks(hookType HookType) bool

HasHooks 检查指定类型是否有注册的 hook. 用于 Engine 在关键路径上快速判断是否需要触发 hook(避免不必要的环境变量构建).

func (*Manager) Register

func (m *Manager) Register(hookType HookType, def HookDef) error

Register 注册一个新的 hook 定义到指定类型.

同一个 hook 类型可以注册多个 handler,执行时按注册顺序依次执行. 返回错误如果 hookType 无效.

func (*Manager) SetObserver

func (m *Manager) SetObserver(obs flyto.EventObserver)

SetObserver 设置可观测性接口. 升华改进(ELEVATED): Setter 注入而非构造函数参数-- NewManager 已在多处调用,加参数会破坏所有调用点. Setter 注入让现有代码零改动即可工作. 替代方案:修改 NewManager 签名(破坏所有现有调用点).

func (*Manager) Unregister

func (m *Manager) Unregister(hookType HookType)

Unregister 移除指定类型的所有 hook.

func (*Manager) UnregisterAllBySource

func (m *Manager) UnregisterAllBySource(source string)

UnregisterAllBySource 移除所有 hook 类型中来自特定来源的 hook.

典型用途:禁用插件时一次性清理该插件在所有 hook 类型上的注册.

升华改进(ELEVATED): 早期实现 的 clearRegisteredPluginHooks() 清除所有插件 hooks 并保留 callback hooks--这是全量操作,无法精准到单个插件. 我们按 Source 精准过滤,支持 N 个插件各自独立 disable,互不干扰. 替代方案:<全量清除所有非全局 hooks 再重建> - 否决:插件 A disable 时不应影响插件 B 的 hooks.

func (*Manager) UnregisterBySource

func (m *Manager) UnregisterBySource(hookType HookType, source string)

UnregisterBySource 移除指定 hook 类型中来自特定来源的所有 hook.

source 对应 HookDef.Source 字段(空字符串 = 全局,插件名 = 该插件的 hooks). 若该类型下没有指定来源的 hook,静默返回(幂等操作).

用途:

  • 禁用插件时,按类型精准移除该插件的 hooks,不影响全局 hooks
  • 调试时临时移除某来源的特定类型 hooks

精妙之处(CLEVER): 过滤而非删除-- 保留所有 Source != source 的 hooks,生成新 slice 替换旧 slice. 若过滤后 slice 为空,用 delete 从 map 中彻底移除(节省内存,HasHooks 返回 false). 替代方案:<遍历删除 index,原地修改> - 否决:slice 原地删除需要 copy 填坑,代码复杂且有 off-by-one 风险.

type ShellHandler

type ShellHandler struct {
	// contains filtered or unexported fields
}

ShellHandler 通过 shell 执行 hook 命令. 走 runShellHook (M1 方案 β 之后), 让它满足 HookHandler 接口.

精妙之处(CLEVER): 共享 runShellHook 函数, 不自己重写 shell 执行逻辑. 唯一差别是 handler 自己持一个 execenv.Executor, 而不是从 Manager 借. 这让 ShellHandler 可以独立构造 (SDK 用户在 Manager 之外单独用), 同时 仍然遵守严格 DI 契约.

func NewShellHandler

func NewShellHandler(def HookDef, executor execenv.Executor) *ShellHandler

NewShellHandler 从 HookDef 创建 shell 后端.

executor 参数是**必填**, nil 会 panic. 严格 DI 契约 (M1 方案 β) 不做 fallback. SDK 用户直接传 execenv.DefaultExecutor{}.

func (*ShellHandler) ExecuteHook

func (h *ShellHandler) ExecuteHook(ctx context.Context, hookType HookType, env map[string]string) *HookResult

ExecuteHook 通过 shell 执行命令.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL