hitl

package
v0.1.7 Latest Latest
Warning

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

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

Documentation

Overview

Package hitl implements human-in-the-loop approval for non-readonly tool calls. When a mutating tool is invoked, execution pauses until a human approves or rejects the call. Without this package, the agent would execute all tool calls autonomously, which may be undesirable for destructive operations like file writes or shell commands.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CanResolve added in v0.1.7

func CanResolve(approval ApprovalRequest, resolverID, resolverRole string) bool

CanResolve checks whether the resolving user is authorized to approve or reject an approval. Authorization rules:

  1. The principal who created the approval can always resolve it.
  2. Users with the "admin" role can resolve any approval.
  3. Legacy rows without a CreatedBy value are resolvable by anyone to maintain backward compatibility after the schema migration.

Without this function, any authenticated user could approve or reject any other user's pending tool calls, breaking tenant isolation in multi-user deployments.

Types

type ApprovalRequest

type ApprovalRequest struct {
	ID            string         `json:"id"`
	ThreadID      string         `json:"thread_id"`
	RunID         string         `json:"run_id"`
	TenantID      string         `json:"tenant_id,omitempty"`
	ToolName      string         `json:"tool_name"`
	Args          string         `json:"args"`
	Status        ApprovalStatus `json:"status"`
	Feedback      string         `json:"feedback,omitempty"`
	CreatedAt     time.Time      `json:"created_at"`
	ExpiresAt     *time.Time     `json:"expires_at,omitempty"`
	ResolvedAt    *time.Time     `json:"resolved_at,omitempty"`
	ResolvedBy    string         `json:"resolved_by,omitempty"`
	CreatedBy     string         `json:"created_by,omitempty"`
	SenderContext string         `json:"sender_context,omitempty"`
	Question      string         `json:"question,omitempty"`
}

ApprovalRequest represents a pending or resolved tool approval. Each non-readonly tool call creates one ApprovalRequest before execution. Without this struct the system would have no way to track or persist individual approval decisions across requests.

func (ApprovalRequest) String

func (a ApprovalRequest) String() string

type ApprovalStatus

type ApprovalStatus string

ApprovalStatus represents the current state of an approval request.

const (
	// StatusPending means the request is waiting for a human decision.
	StatusPending ApprovalStatus = "pending"
	// StatusApproved means the human approved the tool call.
	StatusApproved ApprovalStatus = "approved"
	// StatusRejected means the human rejected the tool call.
	StatusRejected ApprovalStatus = "rejected"
	// StatusExpired means the request was orphaned by a server restart and
	// automatically expired during startup recovery.
	StatusExpired ApprovalStatus = "expired"
)

func (ApprovalStatus) String

func (a ApprovalStatus) String() string

type ApprovalStore

type ApprovalStore interface {
	// Create persists a new pending approval and returns it with a generated ID.
	Create(ctx context.Context, req CreateRequest) (ApprovalRequest, error)

	// Resolve updates the approval's status to approved or rejected.
	// It unblocks any goroutine waiting in WaitForResolution for the same ID.
	Resolve(ctx context.Context, req ResolveRequest) error

	// WaitForResolution blocks until the given approval is resolved or the
	// context is cancelled. Returns the resolved ApprovalRequest.
	WaitForResolution(ctx context.Context, approvalID string) (ApprovalRequest, error)

	// RecoverPending handles approvals left in "pending" state after a restart.
	// Approvals older than maxAge are marked as expired; recent ones get their
	// waiter channels re-registered so they can still be resolved via the API.
	RecoverPending(ctx context.Context, maxAge time.Duration) (RecoverResult, error)

	// ListPending returns all approval requests currently in "pending" state
	// whose deadline has not expired.
	// Without this, external systems (e.g. the GUILD API) would have no way
	// to discover which tool calls are waiting for human approval.
	ListPending(ctx context.Context) ([]ApprovalRequest, error)

	// Get returns the approval request by ID (pending or resolved).
	// Used by the approve endpoint to read tool name and args when adding to the approve list.
	Get(ctx context.Context, approvalID string) (ApprovalRequest, error)

	// ExpireStale marks all pending approvals past their expires_at as expired.
	// Returns the number of rows affected. Called periodically by a background
	// reaper goroutine.
	ExpireStale(ctx context.Context) (int64, error)

	// Close releases any resources held by the store.
	Close() error

	IsAllowed(toolName string) bool
}

ApprovalStore is the interface for persisting and coordinating tool approvals. Implementations must be safe for concurrent use.

type Config

type Config struct {
	AlwaysAllowed []string      `yaml:"always_allowed,omitempty" toml:"always_allowed,omitempty" json:"always_allowed"`
	DeniedTools   []string      `yaml:"denied_tools,omitempty" toml:"denied_tools,omitempty" json:"denied_tools"`
	ApprovalTTL   time.Duration `yaml:"approval_ttl,omitempty" toml:"approval_ttl,omitempty" json:"approval_ttl"`
	CacheTTL      time.Duration `yaml:"cache_ttl,omitempty" toml:"cache_ttl,omitempty" json:"cache_ttl"`
}

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns sensible defaults. ApprovalTTL defaults to 30 minutes — pending approvals older than this are automatically expired by the background reaper. CacheTTL defaults to 10 minutes — approved tool+args combinations are auto-approved for this duration before requiring fresh human approval.

func (Config) IsAllowed

func (c Config) IsAllowed(toolName string) bool

IsAllowed checks if a tool is exempt from HITL approval. Supports exact names (case-insensitive) and prefix wildcards:

  • "*" matches all tools (HITL fully disabled)
  • "browser_*" matches browser_navigate, browser_read_text, etc.
  • "read_file" matches only read_file (exact)

func (Config) IsDenied

func (c Config) IsDenied(toolName string) bool

IsDenied checks if a tool is explicitly denied via the denied_tools config. Supports exact names (case-insensitive) and prefix wildcards, same as IsAllowed.

func (Config) NewStore

func (c Config) NewStore(gormDB *gorm.DB) ApprovalStore

NewStore creates an ApprovalStore backed by the given GORM database. The caller is responsible for opening and migrating the database (via pkg/db). This constructor does NOT own the database lifecycle — Close() is a no-op.

type CreateRequest

type CreateRequest struct {
	ThreadID      string
	RunID         string
	TenantID      string
	ToolName      string
	Args          string
	CreatedBy     string // principal ID of the user who initiated the tool call
	SenderContext string // originating sender (e.g. "slack:U123:C456")
	Question      string // original user question — needed for replay-on-resume
}

CreateRequest contains the fields needed to create a new approval request.

type RecoverResult

type RecoverResult struct {
	Expired    int                  // approvals that were too old and marked expired
	Recovered  int                  // approvals that had waiter channels re-registered
	Replayable []ReplayableApproval // recent approvals with a saved question, eligible for replay
}

RecoverResult holds the outcome of RecoverPending.

type ReplayableApproval

type ReplayableApproval struct {
	ApprovalID    string
	Question      string
	SenderContext string
}

ReplayableApproval describes a recovered pending approval that can be replayed through the chat handler once it's resolved.

type ResolveRequest

type ResolveRequest struct {
	ApprovalID string
	Decision   ApprovalStatus
	ResolvedBy string
	Feedback   string
}

ResolveRequest contains the fields needed to resolve (approve/reject) a request.

Directories

Path Synopsis
Code generated by counterfeiter.
Code generated by counterfeiter.

Jump to

Keyboard shortcuts

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