hook

package
v0.34.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

pkg/hook

pkg/hook defines deterministic, typed, in-process interception for bounded Harness operations. Use guards for synchronous policy and around hooks for tracing, metrics, or other observation. Durable lifecycle facts remain on the pkg/event stream.

Install hooks on a rig

hooks := hook.Set{
    PolicyRevision: "tool-safety-v3",
    Guards: []hook.Guard{{
        Operation: hook.OperationToolCall,
        Check: func(ctx context.Context, call hook.Call) error {
            if unsafe(call.ToolCall.ToolName, call.ToolCall.ArgsJSON) {
                return hook.Deny("unsafe_tool", "tool call rejected by policy")
            }
            return nil
        },
    }},
    Around: []hook.Around{{
        Operation: hook.OperationInference,
        Begin: func(ctx context.Context, call hook.Call) (context.Context, hook.FinishFunc) {
            ctx, span := tracer.Start(ctx, "harness.inference")
            return ctx, func(result hook.Result) {
                recordOutcome(span, result)
                span.End()
            }
        },
    }},
}

r, err := rig.Define(
    rig.WithLoops(agent),
    rig.WithPrimers("agent"),
    rig.WithSessionStore(store),
    rig.WithHooks(hooks),
)

rig.Define validates, defensively copies, and compiles the set once. The compiled runner is immutable and is installed on every native loop and journal created or restored by that rig. Mutating the original slices later has no effect.

Operations and policy

There are eight operations; four are guardable:

Operation Guardable Meaning
Turn yes One selected user-input turn
Step no One inference/tool step
Inference yes One native streaming model request
Compaction yes One transcript-compaction attempt
ToolCall yes One semantic tool call, including policy
GateWait no Time actually waiting for a gate answer
ToolExecution no Approved invocation of tool code
JournalAppend no One checked durable append

Guards run sequentially in registration order and the first error stops the operation. Any error blocks: a validated *hook.Denial is an intentional policy decision, while every other error or panic is an internal guard failure. This fail-closed default prevents a broken policy dependency from silently granting access.

Use hook.Deny(code, reason) to construct a denial. Codes are bounded lowercase ASCII machine identifiers; reasons are bounded, valid UTF-8, nonblank text without control characters. hook.AsDenial is the only supported classifier: it follows wrapping, revalidates exported fields, and returns an independent copy. A malformed directly constructed Denial remains an internal failure.

PolicyRevision is required exactly when guards are present. It enters SessionStarted.Manifest.HookPolicyRev, so changing guard behavior requires a new revision and restore reports event.DriftHookPolicy at Warn. Around-only sets keep the field empty and do not affect configuration identity. Revisions are bounded to 128 bytes and must be valid UTF-8 without control characters.

Around-hook execution

Matching Begin callbacks run in registration order. Each returned context is passed to later callbacks, guards, the operation, nested operations, and durable appends made by that operation. Matching finish callbacks run in reverse order, once each. Different operations may dispatch concurrently, so callback code must be concurrency-safe.

Harness retains the prior context's cancellation and deadline even if a callback returns a detached context. A nil returned context is ignored. Begin and finish panics are isolated and logged without hook payloads; an observer cannot alter the operation result.

The operation owner must always call the returned finish function, including guard denial, failure, cancellation, and panic-normalization paths. Besides delivering the terminal snapshot, finish releases cancellation links that the runner may have installed. Runtime operation boundaries already enforce this rule; direct Runner.Start users inherit the same obligation.

Every callback receives an independent, read-only Call or Result snapshot. Mutable messages, requests, JSON arguments, compaction data, and tool results are cloned. Result.Err is deliberately the original trusted in-process error. Raw messages, arguments, results, and errors may contain sensitive data: redaction is mandatory before logging, telemetry export, or another trust boundary.

On terminal ToolCall results, ToolCall.Result contains the full normalized tool result, including pre-execution failures; ResultPreview remains the bounded display projection. On terminal ToolExecution results, ToolExecution.Result contains only the approved invocation's result.

Hooks versus events

Hooks surround attempts and can propagate context before an outcome is known. Events record committed facts. An operation finishing does not prove that a corresponding durable event exists; use the event stream for replay, audit, and state reconstruction.

JournalAppend observes new checked appends exactly once, including lifecycle opening fences. Restore does not replay historical operations or historical appends through hooks. A restored session uses the newly supplied immutable runner for its opening fence and all new work.

Durable appends initiated inside an operation inherit that operation's derived context. If a GateWait observer cancels only its derived wait context, Harness removes and closes the already-installed gate before the blocked tool call can continue.

Operation hooks are native-loop semantics. Foreign loop builders do not receive the runner and therefore do not produce native Turn, Step, Inference, Compaction, or tool hooks. Harness-owned journal appends around those loops remain observable. Hustle inference is also a separate execution plane and does not produce native Inference hooks; a compaction driven by a hustle is observed at the enclosing native Compaction boundary.

Direct runner use

Most applications should use rig.WithHooks. Lower-level integration code may compile and dispatch directly:

runner, err := hook.Compile(set)
ctx, finish, err := runner.Start(ctx, call)
if finish != nil {
    defer finish(result)
}

Compile owns the registration slices. Start validates and clones the call, runs observers and guards, and returns an exactly-once aggregate finish function. Direct callers must supply a valid terminal Result for the same operation and must not omit finish when it is returned.

Documentation

Overview

Package hook defines the in-process interception contracts for bounded Harness runtime operations.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Deny

func Deny(code, reason string) error

Deny constructs an intentional denial or returns ConfigError when its diagnostic fields violate the bounded public contract.

func ValidateCall

func ValidateCall(call Call) error

ValidateCall validates the closed operation-payload union.

func ValidateSet

func ValidateSet(set Set) error

ValidateSet validates one declarative hook set.

Types

type Around

type Around struct {
	Operation Operation
	Begin     BeginFunc
}

Around registers one paired begin/finish observer for an operation.

type BeginFunc

type BeginFunc func(context.Context, Call) (context.Context, FinishFunc)

BeginFunc begins observation of an operation and returns its derived context and optional terminal callback. A returned context may add values or tighter cancellation, but Runner preserves cancellation and deadlines from the input context even if the returned context is detached.

type Call

type Call struct {
	// Operation selects the one matching operation-specific payload below.
	Operation Operation
	// StartedAt is the runtime-owned operation start time.
	StartedAt time.Time
	// Coordinates locate the operation in the session/loop/turn/step hierarchy.
	Coordinates identity.Coordinates
	// AgentName is the immutable attribution name of the executing loop.
	AgentName identity.AgentName
	// Cause is the direct causal edge that initiated the operation.
	Cause identity.Cause

	// Exactly one pointer below is non-nil and matches Operation.
	Turn          *TurnData
	Step          *StepData
	Inference     *InferenceData
	Compaction    *CompactionData
	ToolCall      *ToolCallData
	GateWait      *GateWaitData
	ToolExecution *ToolExecutionData
	JournalAppend *JournalAppendData
}

Call is the immutable typed snapshot supplied when an operation begins. Exactly one operation-specific payload must be non-nil and match Operation.

func CloneCall

func CloneCall(call Call) Call

CloneCall gives a hook independent ownership of every reference-backed payload while preserving nil-versus-empty distinctions. It panics with *CloneError if an upstream sealed content union gains an unsupported variant; it never silently drops unknown content.

type CallError

type CallError struct {
	Kind      CallErrorKind
	Operation Operation
}

CallError reports a malformed runtime operation snapshot.

func (*CallError) Error

func (e *CallError) Error() string

type CallErrorKind

type CallErrorKind string

CallErrorKind identifies a malformed operation call snapshot.

const (
	CallUnknownOperation CallErrorKind = "unknown_operation"
	CallInvalidPayload   CallErrorKind = "invalid_payload"
)

type CloneError

type CloneError struct {
	Kind      CloneErrorKind
	ValueType string
}

CloneError reports a sealed content variant the hook snapshot clone does not yet support. CloneCall panics with this error rather than silently losing data.

func (*CloneError) Error

func (e *CloneError) Error() string

type CloneErrorKind

type CloneErrorKind string

CloneErrorKind identifies the sealed union that gained an unsupported variant.

const (
	CloneUnknownConversation CloneErrorKind = "unknown_conversation"
	CloneUnknownBlock        CloneErrorKind = "unknown_block"
)

type CompactionData

type CompactionData struct {
	// AttemptID correlates the operation with compaction lifecycle events.
	AttemptID event.CompactAttemptID
	// Input is the exact transcript and context identity being compacted.
	Input *loop.CompactionInput
	// Output is the validated summary, when compaction succeeds.
	Output *loop.CompactionOutput
}

CompactionData carries one transcript-compaction attempt and its optional terminal summary.

type ConfigError

type ConfigError struct {
	Kind      ConfigErrorKind
	Operation Operation
	Index     int
	Field     string
}

ConfigError reports invalid hook-set or denial configuration.

func (*ConfigError) Error

func (e *ConfigError) Error() string

type ConfigErrorKind

type ConfigErrorKind string

ConfigErrorKind identifies an invalid hook declaration or typed payload.

const (
	ConfigUnknownOperation         ConfigErrorKind = "unknown_operation"
	ConfigOperationNotGuardable    ConfigErrorKind = "operation_not_guardable"
	ConfigNilGuard                 ConfigErrorKind = "nil_guard"
	ConfigNilAround                ConfigErrorKind = "nil_around"
	ConfigMissingPolicyRevision    ConfigErrorKind = "missing_policy_revision"
	ConfigUnexpectedPolicyRevision ConfigErrorKind = "unexpected_policy_revision"
	ConfigInvalidPolicyRevision    ConfigErrorKind = "invalid_policy_revision"
	ConfigInvalidDenial            ConfigErrorKind = "invalid_denial"
)

type Denial

type Denial struct {
	Code   string
	Reason string
}

Denial is an intentional, bounded guard refusal.

func AsDenial

func AsDenial(err error) (*Denial, bool)

AsDenial classifies an intentional guard denial. It revalidates exported Denial fields so direct construction cannot bypass the bounded contract and returns an independent copy owned by the caller.

func (*Denial) Error

func (e *Denial) Error() string

type FinishFunc

type FinishFunc func(Result)

FinishFunc observes the terminal result of an operation.

type GateWaitData

type GateWaitData struct {
	// GateID identifies the gate being awaited.
	GateID gate.ID
	// Kind identifies the user-facing gate scenario.
	Kind gate.Kind
	// Resolver identifies the component responsible for resolving the gate.
	Resolver gate.ResolverKind
	// Blocks identifies the execution scope held by the gate.
	Blocks gate.Blocks
	// Effect identifies what resolution does to execution.
	Effect gate.Effect
	// Answer is the validated live answer, when one was delivered.
	Answer *gate.Answer
}

GateWaitData describes the time spent waiting for one gate resolution.

type Guard

type Guard struct {
	Operation Operation
	Check     GuardFunc
}

Guard registers one synchronous check for a guardable operation.

type GuardError

type GuardError struct {
	Operation Operation
	Index     int
	Cause     error
}

GuardError reports an internal guard callback failure. Intentional denials are returned as validated *Denial values instead.

func (*GuardError) Error

func (e *GuardError) Error() string

func (*GuardError) Unwrap

func (e *GuardError) Unwrap() error

Unwrap exposes the trusted in-process cause for classification.

type GuardFunc

type GuardFunc func(context.Context, Call) error

GuardFunc checks whether a guardable operation may proceed.

type InferenceData

type InferenceData struct {
	// Request is the provider-neutral request submitted to inference.
	Request *inference.Request
	// AIMessage is the completed assistant message, when produced.
	AIMessage *content.AIMessage
	// StreamResult is authoritative terminal provider metadata, when produced.
	StreamResult *stream.StreamResult
}

InferenceData carries the provider-neutral request and terminal model output. Terminal fields are nil until their corresponding values exist.

type JournalAppendData

type JournalAppendData struct {
	// Family is the closed record family.
	Family RecordFamily
	// RecordID is the record's bounded textual identity.
	RecordID string
}

JournalAppendData describes one bounded durable append without exposing serialized record bytes.

type Operation

type Operation uint8

Operation identifies one bounded runtime operation.

const (
	OperationTurn Operation = iota + 1
	OperationStep
	OperationInference
	OperationCompaction
	OperationToolCall
	OperationGateWait
	OperationToolExecution
	OperationJournalAppend
)

func (Operation) Guardable

func (o Operation) Guardable() bool

Guardable reports whether a guard may synchronously deny operation.

func (Operation) Valid

func (o Operation) Valid() bool

Valid reports whether operation is recognized by this package.

type Outcome

type Outcome uint8

Outcome identifies how an operation ended.

const (
	OutcomeCompleted Outcome = iota + 1
	OutcomeDenied
	OutcomeFailed
	OutcomeCanceled
)

func (Outcome) Valid

func (o Outcome) Valid() bool

Valid reports whether outcome is recognized by this package.

type RecordFamily

type RecordFamily string

RecordFamily identifies the bounded journal record family being appended.

const (
	RecordEvent        RecordFamily = "event"
	RecordCommand      RecordFamily = "command"
	RecordGatePrepared RecordFamily = "gate_prepared"
	RecordFence        RecordFamily = "fence"
	// RecordCommandApplication is the private prefix correlating a public CommandID
	// with the RuntimeCommandID and lease epoch of its application.
	RecordCommandApplication RecordFamily = "command_application"
)

type Result

type Result struct {
	Call
	// EndedAt is the runtime-owned operation completion time.
	EndedAt time.Time
	// Outcome is the bounded terminal classification.
	Outcome Outcome
	// Err is the original trusted in-process terminal error and is not
	// deep-cloned. Consumers must redact or classify it before exporting it to
	// logs, telemetry, or another trust boundary.
	Err error
}

Result is the terminal snapshot supplied to an around hook.

func CloneResult

func CloneResult(result Result) Result

CloneResult clones the embedded Call while intentionally retaining Err.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner is an immutable, compiled hook set safe for concurrent dispatch.

func Compile

func Compile(set Set) (*Runner, error)

Compile validates a hook set and takes independent ownership of its registration slices.

func (*Runner) Handles

func (r *Runner) Handles(operation Operation) bool

Handles reports whether the compiled runner has a guard or observer for operation. It is nil-safe and lets operation boundaries skip snapshot and clock work when no callback can run.

func (*Runner) Start

func (r *Runner) Start(
	ctx context.Context,
	call Call,
) (context.Context, FinishFunc, error)

Start begins observation and evaluates policy for one valid operation call. Matching begin callbacks run in registration order with chained contexts, followed by matching guards in registration order. Every callback receives an independent snapshot.

Observer panics are logged without callback-owned details and fail open. A guard or denial-classification panic fails closed as *GuardError. A validated intentional denial is returned as *Denial; every other guard failure is returned as *GuardError.

The returned FinishFunc runs completed observers in reverse registration order exactly once, including when a guard blocks. Every non-nil context returned by Begin keeps its values and tighter cancellation while also preserving cancellation and deadlines from the previous context. Calling Finish releases the resources used to bridge detached contexts, even when no observer returned its own finish callback. The caller must therefore always invoke Finish and supply a valid Result for the same operation with a valid terminal Outcome.

type Set

type Set struct {
	// PolicyRevision identifies behavior implemented by Guards. It is required
	// when Guards is non-empty and forbidden otherwise; Around observers are
	// operational configuration and do not contribute to policy identity.
	PolicyRevision string
	// Guards run in registration order at guardable operation boundaries.
	Guards []Guard
	// Around observers begin in registration order and finish in reverse order.
	Around []Around
}

Set is an ordered collection of guards and around observers. A Set and its backing slices are immutable after installation. Callbacks may run concurrently for different operations and must be concurrency-safe; Call and Result arguments are read-only snapshots.

type StepData

type StepData struct {
	// Index is the step's zero-based turn-local index.
	Index StepIndex
}

StepData describes one bounded inference/tool step within a turn.

type StepIndex

type StepIndex uint64

StepIndex is the zero-based index of a step within a turn.

type ToolCallData

type ToolCallData struct {
	// ToolExecutionID is the runtime-minted identity for this attempted call.
	ToolExecutionID uuid.UUID
	// ToolUseID is the model-supplied call identity.
	ToolUseID string
	// ToolName is the normalized invoked tool name.
	ToolName string
	// Summary is the bounded, redacted call summary.
	Summary string
	// ArgsJSON is the raw model-supplied argument object.
	ArgsJSON json.RawMessage
	// PermissionEffect is the terminal approve/deny decision, when known.
	PermissionEffect event.PermissionDecisionEffect
	// PermissionReason is the bounded decision reason.
	PermissionReason string
	// Result is the normalized terminal tool result, including pre-execution
	// failures, when the semantic call has completed.
	Result *tool.ToolResult
	// ResultPreview is the bounded terminal tool-output preview.
	ResultPreview string
	// IsError reports whether the semantic call ended in an error.
	IsError bool
}

ToolCallData describes the semantic tool-call operation, including permission resolution and its normalized terminal result.

type ToolExecutionData

type ToolExecutionData struct {
	// ToolExecutionID is the runtime-minted identity for this execution.
	ToolExecutionID uuid.UUID
	// ToolUseID is the model-supplied call identity.
	ToolUseID string
	// ToolName is the normalized invoked tool name.
	ToolName string
	// ArgsJSON is the raw model-supplied argument object.
	ArgsJSON json.RawMessage
	// Result is the tool's terminal content, when produced.
	Result *tool.ToolResult
	// ResultPreview is the bounded terminal output preview.
	ResultPreview string
	// IsError reports whether execution ended in an error.
	IsError bool
}

ToolExecutionData describes only the approved tool execution boundary.

type TurnData

type TurnData struct {
	// Index is the turn's loop-local index.
	Index event.TurnIndex
	// Input is the user message that initiated the turn, when available.
	Input *content.UserMessage
}

TurnData describes one bounded turn.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL