first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Package hooks implements pigo's user-extensible lifecycle hook system: a
|
||||
// config-driven way to run shell commands at agent lifecycle points (tool
|
||||
// calls, prompt submission, session start/end, etc.) without writing Go or
|
||||
// compiling a plugin. It is a leaf package depending only on the standard
|
||||
// library, so it can be composed into the runtime/cli layers without creating
|
||||
// an import cycle.
|
||||
//
|
||||
// This file defines the configuration types and their validation. A hook is a
|
||||
// single shell command; a matcher binds a group of hooks to an event (and,
|
||||
// for tool events, a tool-name pattern); a HookSet maps each event type to its
|
||||
// matchers. The types carry JSON tags so they can live directly in the layered
|
||||
// config.json.
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultTimeoutSeconds is the per-hook execution timeout when HookConfig.Timeout
|
||||
// is nil (FR-11). A slow or hung hook is killed after this many seconds and
|
||||
// treated as a failure (fail-open for blocking hooks).
|
||||
const DefaultTimeoutSeconds = 60
|
||||
|
||||
// CommandType is the only hook type supported in v1: a shell command. Other
|
||||
// types (embedded script engines, WASM) are explicitly out of scope.
|
||||
const CommandType = "command"
|
||||
|
||||
// HookConfig is a single hook command.
|
||||
type HookConfig struct {
|
||||
Type string `json:"type"` // v1: fixed "command"
|
||||
Command string `json:"command"` // handed to the system shell
|
||||
Timeout *int `json:"timeout,omitempty"` // seconds; nil = DefaultTimeoutSeconds
|
||||
}
|
||||
|
||||
// HookMatcherConfig binds a group of hooks to a matcher. An empty (or "*")
|
||||
// matcher applies to every trigger of the event; otherwise it is matched
|
||||
// against the tool name (see matcher.go).
|
||||
type HookMatcherConfig struct {
|
||||
Matcher string `json:"matcher,omitempty"`
|
||||
Hooks []HookConfig `json:"hooks"`
|
||||
}
|
||||
|
||||
// HookSet maps an event type (e.g. "PreToolUse") to its matcher list. It is
|
||||
// the shape stored in a ConfigLayer and in the resolved Config.
|
||||
type HookSet map[string][]HookMatcherConfig
|
||||
|
||||
// TimeoutSeconds returns the effective timeout for the hook: its own Timeout
|
||||
// when set to a positive value, otherwise DefaultTimeoutSeconds. A non-positive
|
||||
// override is ignored so a misconfigured 0/negative value cannot disable the
|
||||
// timeout guard.
|
||||
func (h HookConfig) TimeoutSeconds() int {
|
||||
if h.Timeout != nil && *h.Timeout > 0 {
|
||||
return *h.Timeout
|
||||
}
|
||||
return DefaultTimeoutSeconds
|
||||
}
|
||||
|
||||
// Validate reports whether the hook is well-formed: the type must be "command"
|
||||
// (empty is accepted and treated as "command" for convenience) and the command
|
||||
// must be non-empty. An invalid hook is rejected at load time and skipped with
|
||||
// a warning rather than executed.
|
||||
func (h HookConfig) Validate() error {
|
||||
if h.Type != "" && h.Type != CommandType {
|
||||
return errors.New("hook type must be \"command\"")
|
||||
}
|
||||
if strings.TrimSpace(h.Command) == "" {
|
||||
return errors.New("hook command must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// warnf writes a formatted warning to w when w is non-nil. Hook failures and
|
||||
// misconfigurations are surfaced this way (mirroring plugin.EventNotifier's
|
||||
// warnLog) so a bad hook never interrupts the agent.
|
||||
func warnf(w io.Writer, format string, args ...any) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, format, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user