Files
Agent-Native/runtime/hooks/hooks.go
T
weichenandCursor 0a52d9f976
ci / vet-and-test (pull_request) Successful in 46s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add a hook bus and permission decisions for tool calls
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>
2026-10-03 17:02:28 +00:00

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{}
}
}