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:
- The principal who created the approval can always resolve it.
- Users with the "admin" role can resolve any approval.
- 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 ¶
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 ¶
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 ¶
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.