Why: Squad members never saw the direct-run stop, reminder, and before-tool gates, and there was no shared place to decide deny, ask, or allow. Evidence: GOWORK=off go test ./runtime/hooks/ ./runtime/policy/ ./runtime/ ./adapters/native/ passed, including an A/B run where hook adapters match the old callbacks. Impact: An empty bus leaves stop reason, reminder count, and deny count unchanged. Callers opt in by setting GateOptions. Refs: feature/permission-hooks Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
2.8 KiB
Go
135 lines
2.8 KiB
Go
// Package hooks 是进程内钩子总线。决策按 block/deny、ask、modify、allow 取更强的一条。
|
|
// 没有表态的处理函数不改变结果。
|
|
package hooks
|
|
|
|
import "sync"
|
|
|
|
type Event string
|
|
|
|
const (
|
|
SessionStart Event = "SessionStart"
|
|
UserPromptSubmit Event = "UserPromptSubmit"
|
|
PreToolUse Event = "PreToolUse"
|
|
PermissionRequest Event = "PermissionRequest"
|
|
PostToolUse Event = "PostToolUse"
|
|
PostToolUseFailure Event = "PostToolUseFailure"
|
|
Stop Event = "Stop"
|
|
SubagentStart Event = "SubagentStart"
|
|
SubagentStop Event = "SubagentStop"
|
|
PreCompact Event = "PreCompact"
|
|
)
|
|
|
|
type Decision string
|
|
|
|
const (
|
|
Allow Decision = "allow"
|
|
Deny Decision = "deny"
|
|
Ask Decision = "ask"
|
|
Modify Decision = "modify"
|
|
Block Decision = "block"
|
|
)
|
|
|
|
// Call 是一次事件。工具字段只在工具事件上有意义。
|
|
type Call struct {
|
|
Event Event
|
|
Tool string
|
|
Args string
|
|
Result string
|
|
IsError bool
|
|
Turn int
|
|
LastText string
|
|
Continuations int
|
|
ToolCalls int
|
|
}
|
|
|
|
// Outcome 是一个处理函数的表态。空 Decision 表示不插手。
|
|
type Outcome struct {
|
|
Decision Decision
|
|
Reason string
|
|
Args string
|
|
Notice string
|
|
Replacement string
|
|
}
|
|
|
|
type Handler func(Call) Outcome
|
|
|
|
type Bus struct {
|
|
mu sync.Mutex
|
|
hs []Handler
|
|
}
|
|
|
|
func New() *Bus { return &Bus{} }
|
|
|
|
func (b *Bus) On(h Handler) {
|
|
if b == nil || h == nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
b.hs = append(b.hs, h)
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
// Emit 按优先级合并表态。deny 与 block 同级,先登记的保留。
|
|
func (b *Bus) Emit(c Call) Outcome {
|
|
if b == nil {
|
|
return Outcome{}
|
|
}
|
|
b.mu.Lock()
|
|
hs := append([]Handler(nil), b.hs...)
|
|
b.mu.Unlock()
|
|
var merged Outcome
|
|
for _, h := range hs {
|
|
merged = prefer(merged, h(c))
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func prefer(cur, next Outcome) Outcome {
|
|
if rank(next.Decision) > rank(cur.Decision) {
|
|
return next
|
|
}
|
|
return cur
|
|
}
|
|
|
|
func rank(d Decision) int {
|
|
switch d {
|
|
case Block, Deny:
|
|
return 4
|
|
case Ask:
|
|
return 3
|
|
case Modify:
|
|
return 2
|
|
case Allow:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// AdaptBefore 把「返回错误即拒绝」收成 PreToolUse 处理函数。
|
|
func AdaptBefore(fn func(name, args string) error) Handler {
|
|
return func(c Call) Outcome {
|
|
if c.Event != PreToolUse || fn == nil {
|
|
return Outcome{}
|
|
}
|
|
if err := fn(c.Tool, c.Args); err != nil {
|
|
return Outcome{Decision: Deny, Reason: err.Error()}
|
|
}
|
|
return Outcome{Decision: Allow}
|
|
}
|
|
}
|
|
|
|
// AdaptAfter 把工具结果观察收成 PostToolUse 或失败事件。它不改写结果。
|
|
func AdaptAfter(fn func(name, args, result string, isError bool)) Handler {
|
|
return func(c Call) Outcome {
|
|
if fn == nil {
|
|
return Outcome{}
|
|
}
|
|
switch c.Event {
|
|
case PostToolUse, PostToolUseFailure:
|
|
fn(c.Tool, c.Args, c.Result, c.IsError)
|
|
}
|
|
return Outcome{}
|
|
}
|
|
}
|