approval

package
v0.8.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

View Source
const DefaultTTL = 24 * time.Hour

DefaultTTL is the time an approval request stays pending before it is automatically expired by the background worker.

Variables

View Source
var (
	ErrNotFound        = errors.New("approval: not found")
	ErrAlreadyResolved = errors.New("approval: already resolved")
)

Sentinel errors returned by Store implementations.

View Source
var ErrStaleCallback = fmt.Errorf("approval: callback refers to a non-pending request")

ErrStaleCallback is returned by ResolveByCallback when the callback refers to an approval that exists but is no longer pending (already resolved, expired, or approved). The caller should surface its Status to the user.

Functions

func ValidStatus

func ValidStatus(s Status) bool

ValidStatus reports whether s is one of the four known status values. An empty string is also accepted (means "all" in list queries).

Types

type ActionFunc

type ActionFunc func(ctx context.Context, payload string) error

ActionFunc is the callback invoked when an approval is resolved. It receives the stored payload and performs the approved action.

type ActionKind

type ActionKind string

ActionKind categorises what kind of action is awaiting approval.

const (
	// ActionKindUserUpdate is a request to update the agent's USER.md persona file.
	ActionKindUserUpdate ActionKind = "user_update"

	// ActionKindCreateSkill is a request to create a new skill file in the agent's skills directory.
	ActionKindCreateSkill ActionKind = "create_skill"

	// ActionKindModifySchedule is a request to register a new schedule entry at runtime.
	ActionKindModifySchedule ActionKind = "modify_schedule"

	// ActionKindInstallTool is a request to add or remove an MCP tool or plugin at runtime.
	ActionKindInstallTool ActionKind = "install_tool"

	// ActionKindModifyConfig is a request to change a runtime configuration setting (e.g. fallback rules).
	ActionKindModifyConfig ActionKind = "modify_config"

	// ActionKindBrowserProfile is a request to clear or delete a browser profile.
	ActionKindBrowserProfile ActionKind = "browser_profile"
)

type Handler

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

Handler implements the adapter.CallbackResolver interface for approval callbacks. It maps Telegram inline keyboard button data (e.g. "appr:{id}:approve") to human-readable confirmation strings by delegating resolution to the Manager.

It satisfies adapter.CallbackResolver without importing that package:

var _ adapter.CallbackResolver = (*Handler)(nil)

func NewCallbackHandler

func NewCallbackHandler(m *Manager, logger *slog.Logger) *Handler

NewCallbackHandler returns a Handler that resolves approval callbacks via m.

func (*Handler) Resolve

func (h *Handler) Resolve(ctx context.Context, data string) (string, error)

Resolve maps a Telegram callback data string to a human-readable response. Returns ("", nil) for unrecognised callbacks (not approval-related). The returned string is suitable for sending directly to the user.

type Manager

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

Manager coordinates the persistent Store with the in-memory action Registry. It is the primary API used by the Engine and REST API server.

func NewManager

func NewManager(store Store, logger *slog.Logger) *Manager

NewManager creates a Manager backed by the given store.

func (*Manager) ExpirePending

func (m *Manager) ExpirePending(ctx context.Context) (int, error)

ExpirePending expires all pending approvals. Call at startup.

func (*Manager) Get

func (m *Manager) Get(ctx context.Context, id string) (*Request, error)

Get returns a single approval by ID.

func (*Manager) GetByCallbackData

func (m *Manager) GetByCallbackData(ctx context.Context, callbackData string) (*Request, error)

GetByCallbackData fetches an approval by its callback_data prefix regardless of status. Used to provide informative feedback when a user clicks an already-resolved or expired Telegram button.

func (*Manager) List

func (m *Manager) List(ctx context.Context, status Status) ([]Request, error)

List returns approvals filtered by status ("" = all).

func (*Manager) Resolve

func (m *Manager) Resolve(ctx context.Context, id string, approved bool, resolvedBy string) (*Request, error)

Resolve marks an approval as approved or denied and, if approved, invokes the registered action closure. Returns the updated Request.

func (*Manager) ResolveByCallback

func (m *Manager) ResolveByCallback(ctx context.Context, callbackData string, resolvedBy string) (*Request, error)

ResolveByCallback parses the full Telegram callback data string ("appr:{id}:approve" or "appr:{id}:deny"), resolves the approval, and returns the updated Request. Returns ErrNotFound for unknown callbacks, ErrStaleCallback when the approval is no longer pending.

func (*Manager) StartExpiryWorker

func (m *Manager) StartExpiryWorker(ctx context.Context, interval time.Duration)

StartExpiryWorker starts a background goroutine that expires pending approvals whose TTL has elapsed. It ticks every interval until ctx is cancelled. Expired closures are removed from the in-memory registry. Safe to call once per process lifetime.

func (*Manager) Submit

func (m *Manager) Submit(
	ctx context.Context,
	agentName string,
	kind ActionKind,
	summary string,
	payload string,
	externalID string,
	adapterName string,
	conversationID string,
	action ActionFunc,
) (*Request, error)

Submit creates a new pending approval, registers the action closure, and returns the persisted Request with its ID populated. The request expires after DefaultTTL if not resolved.

type Registry

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

Registry holds in-memory action closures keyed by approval ID. These are ephemeral: on restart the registry is empty, and ExpirePending ensures no stale DB entries are left in "pending" state.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty Registry.

func (*Registry) Delete

func (r *Registry) Delete(id string)

Delete removes the action for the given ID without invoking it.

func (*Registry) Pop

func (r *Registry) Pop(id string) (ActionFunc, bool)

Pop retrieves and removes the action for the given ID atomically. Returns (nil, false) if no action is registered for that ID.

func (*Registry) Register

func (r *Registry) Register(id string, fn ActionFunc)

Register stores an action closure under the given ID.

type Request

type Request struct {
	ID        string     `db:"id"      json:"id"`
	AgentName string     `db:"agent_name" json:"agent_name"`
	Kind      ActionKind `db:"kind"    json:"kind"`
	Status    Status     `db:"status"  json:"status"`

	// Summary is a human-readable one-liner shown in the approval UI.
	Summary string `db:"summary" json:"summary"`

	// Payload is the content to apply when approved (e.g. full USER.md text).
	Payload string `db:"payload" json:"payload"`

	// CallbackData is the base prefix embedded in Telegram inline button data.
	// Format: "appr:{id}" — buttons append ":approve" or ":deny".
	CallbackData string `db:"callback_data" json:"callback_data,omitempty"`

	// ExternalID is the adapter-level chat/channel ID to reply to after resolution.
	ExternalID string `db:"external_id" json:"external_id"`

	// AdapterName identifies which adapter to use for confirmation messages.
	AdapterName string `db:"adapter_name" json:"adapter_name"`

	// ConversationID links this approval to the engine conversation that created it.
	ConversationID string `db:"conversation_id" json:"conversation_id"`

	CreatedAt  time.Time  `db:"created_at"  json:"created_at"`
	ExpiresAt  *time.Time `db:"expires_at"  json:"expires_at,omitempty"`
	ResolvedAt *time.Time `db:"resolved_at" json:"resolved_at,omitempty"`

	// ResolvedBy records who resolved the approval: "telegram", "api", or "expired".
	ResolvedBy string `db:"resolved_by" json:"resolved_by,omitempty"`
}

Request is the persisted record of a pending or resolved approval.

type SQLiteStore

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

SQLiteStore implements Store using SQLite.

func NewInMemoryStore

func NewInMemoryStore() (*SQLiteStore, error)

NewInMemoryStore creates an in-memory SQLite approval store (for testing).

func NewSQLiteStore

func NewSQLiteStore(dbPath string) (*SQLiteStore, error)

NewSQLiteStore opens or creates a SQLite database at the given path and applies the approval schema. The file is opened with WAL mode so it can coexist with the memory store's connection to the same file.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

func (*SQLiteStore) Create

func (s *SQLiteStore) Create(ctx context.Context, req Request) (string, error)

func (*SQLiteStore) ExpireBefore

func (s *SQLiteStore) ExpireBefore(ctx context.Context, deadline time.Time) (int, error)

func (*SQLiteStore) ExpirePending

func (s *SQLiteStore) ExpirePending(ctx context.Context) (int, error)

func (*SQLiteStore) Get

func (s *SQLiteStore) Get(ctx context.Context, id string) (*Request, error)

func (*SQLiteStore) GetByCallbackData

func (s *SQLiteStore) GetByCallbackData(ctx context.Context, callbackData string) (*Request, error)

func (*SQLiteStore) List

func (s *SQLiteStore) List(ctx context.Context, status Status) ([]Request, error)

func (*SQLiteStore) Resolve

func (s *SQLiteStore) Resolve(ctx context.Context, id string, status Status, resolvedBy string) error

func (*SQLiteStore) ResolveByCallbackPrefix

func (s *SQLiteStore) ResolveByCallbackPrefix(ctx context.Context, prefix string, status Status, resolvedBy string) (*Request, error)

type Status

type Status string

Status represents the lifecycle state of an approval request.

const (
	StatusPending  Status = "pending"
	StatusApproved Status = "approved"
	StatusDenied   Status = "denied"
	StatusExpired  Status = "expired"
)

type Store

type Store interface {
	// Create persists a new approval request and returns the assigned ID.
	Create(ctx context.Context, req Request) (string, error)

	// Get fetches a single approval by ID. Returns ErrNotFound if absent.
	Get(ctx context.Context, id string) (*Request, error)

	// GetByCallbackData fetches a single approval by its callback_data prefix,
	// regardless of status. Returns ErrNotFound if absent.
	GetByCallbackData(ctx context.Context, callbackData string) (*Request, error)

	// List returns approvals filtered by status. Pass an empty string for all.
	List(ctx context.Context, status Status) ([]Request, error)

	// Resolve transitions the status of a pending approval.
	// Returns ErrNotFound if the ID does not exist.
	// Returns ErrAlreadyResolved if the approval is not currently pending.
	Resolve(ctx context.Context, id string, status Status, resolvedBy string) error

	// ResolveByCallbackPrefix looks up by callback_data prefix, then resolves.
	// Returns ErrNotFound if no pending approval matches.
	ResolveByCallbackPrefix(ctx context.Context, prefix string, status Status, resolvedBy string) (*Request, error)

	// ExpirePending marks all pending approvals as expired. Call at startup.
	// Returns the number of rows affected.
	ExpirePending(ctx context.Context) (int, error)

	// ExpireBefore marks all pending approvals whose expires_at is before
	// deadline as expired. Used by the background expiry worker.
	// Returns the number of rows affected.
	ExpireBefore(ctx context.Context, deadline time.Time) (int, error)

	Close() error
}

Store defines the persistence interface for approval requests. In-memory action closures are managed separately by the Registry, since closures cannot be serialised. On restart, any pending rows are expired by ExpirePending so stale entries are never silently lost.

Jump to

Keyboard shortcuts

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