Why: The N1 harness phase needs the kernel to resolve ask decisions through the host (paired HITL), emit the full hook lifecycle the plan promises, and carry mode/rules/session-allow in one named object. Evidence: go vet clean; go test ./... 24 packages ok, including new loop_hooks_lifecycle_test.go (lifecycle order, prompt text, ask allow/deny/legacy paths, stop continuation counts) and three new policy tests (struct-form parity, session-allow store, nil store). Impact: hooks adds PostToolBatch and SessionEnd events; loop emits SessionStart/UserPromptSubmit/PostToolBatch/SessionEnd and PreCompact lands in AgentSession.Compact; gateTool consults PermissionAsk on ask and keeps ask=reject when the port is unset; policy gains Policy, SessionAllows, and MemoryAllows. SubagentStart/Stop emissions are deferred to the adapters assembly increment (factory site has no bus access yet). Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP2/WP3
137 lines
2.9 KiB
Go
137 lines
2.9 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"
|
|
PostToolBatch Event = "PostToolBatch"
|
|
Stop Event = "Stop"
|
|
SubagentStart Event = "SubagentStart"
|
|
SubagentStop Event = "SubagentStop"
|
|
PreCompact Event = "PreCompact"
|
|
SessionEnd Event = "SessionEnd"
|
|
)
|
|
|
|
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{}
|
|
}
|
|
}
|