Files
Agent-Native/runtime/hooks/hooks.go
weichen 9758a8d8d0
ci / vet-and-test (pull_request) Successful in 48s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add permission ask loop, lifecycle hook emissions, and Policy type
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
2026-10-07 03:01:17 +00:00

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