agentadapters

package
v0.5.0-alpha.4 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 50 Imported by: 0

Documentation

Overview

Package agentadapters: approvals.go declares the wire types, constants, store interfaces, and shared sentinel errors for the adapter approval system. The coordinator implementation lives in approvals_coordinator.go; the operator-facing Resolve/Cancel/list API lives in approvals_resolve.go; ID helpers live in approvals_ids.go; the in-memory store lives in approvals_memory.go; the SQL-backed store lives in approvals_sqlite.go.

Index

Constants

View Source
const (
	AuthenticateStatusAuthenticated = "authenticated"
	LogoutStatusLoggedOut           = "logged_out"
	ACPAuthMethodAgentLogin         = "agent-login"
)
View Source
const (
	ProbeStatusUnverified   = "unverified"
	ProbeStatusReady        = "ready"
	ProbeStatusNotInstalled = "not_installed"
	ProbeStatusAuthRequired = "auth_required"
	ProbeStatusError        = "error"
)

Probe statuses. "unverified" is reserved for passive catalog/health reads; every other status is an operator-facing classification from an explicit probe or a lookup failure. Stable strings — once exported on the wire they are part of the frontend contract.

View Source
const (
	ProbeStageLookup     = "lookup"
	ProbeStageSpawn      = "spawn"
	ProbeStageInitialize = "initialize"
	ProbeStageNewSession = "new_session"
	ProbeStageReady      = "ready"
)

Probe stages. Track which step in the runtime-and-handshake sequence failed so the UI can give a concrete next-action hint without the operator having to read the raw error.

View Source
const (
	StatusAvailable = "available"
	StatusMissing   = "missing"
)
View Source
const (
	AuthStatusOK              = "ok"
	AuthStatusUnauthenticated = "unauthenticated"
	AuthStatusBilling         = "billing"
	AuthStatusUnknown         = "unknown"
)
View Source
const (
	CredentialModeLocalLogin      = "local_login"
	CredentialModeAPIKey          = "api_key"
	CredentialModeEnterpriseToken = "enterprise_token"
	CredentialModeVendorHosted    = "vendor_hosted"
)
View Source
const (
	CapabilityPromptSession    = "prompt_session"
	CapabilityStructuredStream = "structured_stream"
	CapabilityCancel           = "cancel"
	CapabilityPermissions      = "permissions"
	CapabilityMCPServers       = "mcp_servers"
	CapabilityConfigOptions    = "config_options"
	CapabilityTerminalRPC      = "terminal_rpc"
	CapabilityAuthenticate     = "authenticate"
	CapabilityLogout           = "logout"
)
View Source
const (
	CapabilityStatusSupported        = "supported"
	CapabilityStatusAdapterDependent = "adapter_dependent"
	CapabilityStatusOperatorOptIn    = "operator_opt_in"
	CapabilityStatusNotSupported     = "not_supported"
)
View Source
const (
	// MaxPromptFiles matches Hecate Chat's public per-message attachment
	// ceiling. Keeping the transport boundary independently bounded prevents a
	// non-HTTP caller from constructing an unexpectedly large ACP prompt.
	MaxPromptFiles = 4
	// MaxPromptFileBytes is the public per-file upload ceiling.
	MaxPromptFileBytes = int64(5 << 20)
	// MaxPromptFilesBytes preserves the public combined-message attachment
	// limit. ACP dispatch independently stages rich blocks that would exceed
	// its conservative inline wire budget.
	MaxPromptFilesBytes = int64(12 << 20)
)
View Source
const (
	ToolKindFileWrite  = "file_write"
	ToolKindFileRead   = "file_read"
	ToolKindShellExec  = "shell_exec"
	ToolKindNetwork    = "network"
	ToolKindFileMove   = "file_move"
	ToolKindFileDelete = "file_delete"
	ToolKindSearch     = "search"
	ToolKindThink      = "think"
	ToolKindMCP        = "mcp"
	ToolKindOther      = "other"
)

Tool-kind taxonomy used by approval records and grants.

We deliberately choose a small, stable closed set rather than the adapter's free-form tool names. Operators reason about "did Codex just want to write a file" not "did Codex just want to call its `apply_patch` v3.1 tool." The mapping below normalizes adapter terms into Hecate's set; unrecognized terms land as "other" and keep the raw adapter name in Approval.ToolName for diagnostics.

View Source
const DriverKindACP = "acp"

Variables

View Source
var ErrAmbiguousOption = errors.New("ambiguous adapter option; selected_option required")

ErrAmbiguousOption is returned by Coordinator helpers when the operator's decision could match multiple ACP options and no explicit selected_option was provided. Surfaced as 409 by the HTTP handler.

View Source
var ErrApprovalAlreadyResolved = errors.New("approval already resolved")

ErrApprovalAlreadyResolved is returned by ResolveApproval when the row is already in a terminal state. Resolutions are append-only from a state-machine perspective even though they're a single row.

View Source
var ErrApprovalNotFound = errors.New("approval not found")

ErrApprovalNotFound is returned by ApprovalStore.GetApproval and ResolveApproval when the id doesn't match a known row.

View Source
var ErrInvalidDecision = errors.New("decision must be approve or deny")

ErrInvalidDecision / ErrInvalidScope are returned for malformed resolve requests. Surface as 400 invalid_request.

View Source
var ErrInvalidScope = errors.New("scope must be once, session, workspace_tool, or adapter_tool")
View Source
var ErrLaunchModelRequired = errors.New("launch model required")

ErrLaunchModelRequired means Hecate owns the model launch flag for an adapter and the operator has not selected a concrete model yet.

View Source
var ErrNoMatchingOption = errors.New("no ACP option matches the requested decision")

ErrNoMatchingOption is returned by Resolve when the operator's decision has no matching ACP option family on the recorded request (e.g. operator says deny but the adapter offered no reject_* option). Surfaces as 409 conflict; caller may retry as Cancel.

View Source
var ErrRemoteCredentialRequired = errors.New("remote-safe external-agent credential required")

ErrRemoteCredentialRequired means a remote-mode request tried to start an external agent without an explicitly allowed credential mode.

View Source
var ErrSessionNotActive = errors.New("agent chat session is not active")
View Source
var ErrUnknownOption = errors.New("selected_option does not match any option recorded for this approval")

ErrUnknownOption is returned by Resolve when SelectedOption was supplied but doesn't match any option_id on the recorded approval. Surfaces as 400 invalid_request.

Functions

func DetectAuthStatus

func DetectAuthStatus(adapter Adapter) (string, string)

DetectAuthStatus is an explicit diagnostic. Most branches inspect local credential hints, while Claude may run its bounded `auth status` command. Callers must keep it behind an operator-owned execution boundary.

func DetectVersion

func DetectVersion(ctx context.Context, path string) string

DetectVersion runs the adapter binary at path with --version and returns the first semver-shaped token found in combined stdout/stderr output. Returns an empty string if the binary is not reachable, does not respond within detectVersionTimeout, or prints no recognisable version string.

func DetectVersionProbe

func DetectVersionProbe(ctx context.Context, probe VersionProbe, lookup LookupFunc) string

func DevOverrideActive

func DevOverrideActive(adapterID string) bool

DevOverrideActive reports whether HECATE_AGENT_ADAPTER_DEV_OVERRIDES has a valid fixture for adapterID. API handlers use it to keep visual smoke-test state synthetic end-to-end instead of letting a probe response "correct" the catalog row from the real machine.

func IsFailedPromptCommandLifecycleRaw

func IsFailedPromptCommandLifecycleRaw(raw string) bool

IsFailedPromptCommandLifecycleRaw recognizes the persisted form of the command bridge's outer prompt-subprocess start/failed-finish pair. It rejects extra records, provider updates, mismatched sessions, and malformed JSON.

func IsLaunchConfigOption

func IsLaunchConfigOption(adapterID, configID string) bool

IsLaunchConfigOption reports whether configID is owned by Hecate's launch-time adapter wrapper instead of the live ACP session.

func IsPrivateACPRawOutputWithheld

func IsPrivateACPRawOutputWithheld(raw string) bool

IsPrivateACPRawOutputWithheld reports whether raw ACP diagnostics were intentionally replaced because a turn carried private staged file inputs.

func LaunchConfigOptionForSet

func LaunchConfigOptionForSet(adapterID, configID, value string) (agentcontrols.ConfigOption, bool)

LaunchConfigOptionForSet builds the Hecate-owned launch option for a pending config update. It is used when an older persisted chat session has no copy of the launch controls that the current adapter registry exposes.

func LaunchConfigOptions

func LaunchConfigOptions(ctx context.Context, status Status) []agentcontrols.ConfigOption

LaunchConfigOptions returns Hecate-managed adapter options that can be selected before a concrete ACP session exists. It may execute bounded help/model discovery and therefore belongs only behind an explicit operator action, never passive catalog discovery. Built-ins must not require one of these options until a separate passive schema path exists.

func NormalizeError

func NormalizeError(adapterName string, err error) string

func NormalizeOutput

func NormalizeOutput(adapterID, raw string) string

func SetProbeMetrics

func SetProbeMetrics(metrics *telemetry.AgentAdapterMetrics)

SetProbeMetrics installs the AgentAdapterMetrics used by every subsequent Probe call. Pass nil to disable. Callers wire this once at startup; Probe uses an atomic load so the setter is safe to call after handlers are already serving.

func ValidateWorkspace

func ValidateWorkspace(path string) (string, error)

Types

type Activity

type Activity struct {
	ID              string
	Type            string
	Status          string
	Kind            string
	Title           string
	Detail          string
	ArtifactPreview string
}

type Adapter

type Adapter struct {
	ID             string
	Name           string
	Command        string
	Args           []string
	CandidatePaths []string
	// Embedded means Hecate serves this adapter in-process. Command and
	// CandidatePaths then identify the provider CLI used by the embedded
	// adapter. TestProcess* exists only for hermetic protocol fixtures; it is not
	// a supported product runtime mode.
	Embedded                  bool
	TestProcessCommand        string
	TestProcessArgs           []string
	TestProcessCandidatePaths []string
	AgentVersion              VersionProbe
	LaunchSuffixArgs          []string
	LaunchModel               LaunchModelConfig
	LaunchOptions             []LaunchSelectConfig
	Kind                      string
	Description               string
	CostMode                  string
	DocsURL                   string
	SupportedRange            string
	SupportsAuthenticate      bool
	SupportsLogout            bool
	// NativeSessionScope describes whether a stored ACP session id can be
	// restored after the adapter runtime exits. Process-scoped adapters require
	// an explicit fresh-session replacement after restart; the zero value fails
	// closed when session/load fails.
	NativeSessionScope NativeSessionScope
	CredentialModes    []CredentialMode
	Capabilities       []Capability
}

func BuiltInByID

func BuiltInByID(id string) (Adapter, bool)

func BuiltIns

func BuiltIns() []Adapter

func FindAdapter

func FindAdapter(id string) (Adapter, bool)

FindAdapter returns the built-in adapter matching id (exact match, case-sensitive — adapter ids are stable identifiers chosen by the gateway, not user-typed). Second return is false when id doesn't match any registered adapter.

type AmbiguousOptionError

type AmbiguousOptionError struct {
	Decision ApprovalDecision
	Options  []ApprovalOption
}

AmbiguousOptionError carries the candidate options when Resolve can't pick one unambiguously. The HTTP layer surfaces this as 409 conflict with the option list in the body so the operator UI can re-render the choices.

func (*AmbiguousOptionError) Error

func (e *AmbiguousOptionError) Error() string

type Approval

type Approval struct {
	ID             string                 `json:"id"`
	SessionID      string                 `json:"session_id"`
	AdapterID      string                 `json:"adapter_id"`
	Workspace      string                 `json:"workspace,omitempty"`
	ToolKind       string                 `json:"tool_kind"`
	ToolName       string                 `json:"tool_name,omitempty"`
	Status         ApprovalStatus         `json:"status"`
	ACPPayload     json.RawMessage        `json:"acp_payload,omitempty"`
	ACPOptions     []ApprovalOption       `json:"acp_options"`
	ScopeChoices   []ApprovalScope        `json:"scope_choices,omitempty"`
	SelectedOption string                 `json:"selected_option,omitempty"`
	Scope          ApprovalScope          `json:"scope,omitempty"`
	Decision       ApprovalDecision       `json:"decision,omitempty"`
	Path           ApprovalResolutionPath `json:"path,omitempty"`
	DecisionNote   string                 `json:"decision_note,omitempty"`
	CreatedAt      time.Time              `json:"created_at"`
	ResolvedAt     *time.Time             `json:"resolved_at,omitempty"`
	ExpiresAt      time.Time              `json:"expires_at"`
}

Approval is one recorded approval row. The shape mirrors the wire shape in the RFC; endpoints and persistence backends serialize this directly.

type ApprovalCoordinator

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

ApprovalCoordinator applies the configured ApprovalMode to incoming ACP RequestPermission calls. It records every request, looks up matching grants, applies the mode default, and produces the ACP response that gets sent back to the adapter. In prompt mode, a blocked RequestPermission registers a process-local waiter that the operator's Resolve / Cancel call wakes via the wake() helper.

Waiters are intentionally not persisted: a Hecate restart cannot resurrect an in-flight ACP RequestPermission, so any pending row found in storage on startup is invalid as far as live blocking goes — the SQL backend startup reconcile pass marks such rows as `timed_out`.

func NewApprovalCoordinator

func NewApprovalCoordinator(opts CoordinatorOptions) *ApprovalCoordinator

NewApprovalCoordinator constructs a coordinator with the given options. Sensible defaults are filled in for empty fields.

func (*ApprovalCoordinator) Cancel

func (c *ApprovalCoordinator) Cancel(ctx context.Context, id string) (Approval, error)

Cancel transitions a pending approval to cancelled (ACP Cancelled outcome). Different from a deny resolution: deny selects a reject option (telling the adapter the action is forbidden); cancel says "the operator declined to decide; back off and ask again later."

func (*ApprovalCoordinator) DeleteGrant

func (c *ApprovalCoordinator) DeleteGrant(ctx context.Context, id string) error

DeleteGrant removes a grant by id.

func (*ApprovalCoordinator) GetApproval

func (c *ApprovalCoordinator) GetApproval(ctx context.Context, id string) (Approval, error)

GetApproval is a thin store-backed accessor for the HTTP GET handler.

func (*ApprovalCoordinator) ListApprovals

func (c *ApprovalCoordinator) ListApprovals(ctx context.Context, sessionID string, status ApprovalStatus) ([]Approval, error)

ListApprovals proxies to the store. Empty status returns all rows.

func (*ApprovalCoordinator) ListGrants

func (c *ApprovalCoordinator) ListGrants(ctx context.Context, filter GrantFilter) ([]Grant, error)

ListGrants returns grants matching the filter. Empty filter returns all live grants. Expired grants are excluded.

func (*ApprovalCoordinator) Mode

Mode returns the configured mode (test introspection).

func (*ApprovalCoordinator) RequestPermission

RequestPermission records the incoming ACP RequestPermission, applies the configured mode, and returns the ACP response to send back to the adapter.

Behavior by mode:

  • All modes record the approval row.
  • ModeAuto resolves immediately by selecting the first allow option (the original auto-approve behavior, opt-in only).
  • ModeDeny resolves immediately with the first reject option, or Cancelled if none.
  • ModePrompt records a pending approval and waits for operator resolve/cancel through the HTTP API, the configured timeout, or context cancellation.

func (*ApprovalCoordinator) Resolve

Resolve transitions a pending approval to approved/denied per the operator's decision, persists a grant when scope > once, and (when a prompt-mode RequestPermission is blocked on this id) wakes the waiter so the adapter receives the operator's chosen ACP option without further delay.

Returns the resolved row. Errors:

  • ErrApprovalNotFound — id doesn't match a known row
  • ErrApprovalAlreadyResolved — row is already terminal
  • ErrInvalidDecision / ErrInvalidScope — malformed input
  • ErrUnknownOption — selected_option not in row's options
  • ErrNoMatchingOption — decision has no matching option family
  • *AmbiguousOptionError — multiple options match, no selected_option supplied

func (*ApprovalCoordinator) Store

Store returns the configured store (test introspection).

func (*ApprovalCoordinator) Timeout

func (c *ApprovalCoordinator) Timeout() time.Duration

Timeout returns the configured timeout.

type ApprovalDecision

type ApprovalDecision string

ApprovalDecision is the operator's high-level intent. Selected option is recorded separately so we don't lose adapter-named choices.

const (
	ApprovalDecisionApprove ApprovalDecision = "approve"
	ApprovalDecisionDeny    ApprovalDecision = "deny"
)

type ApprovalMode

type ApprovalMode string

ApprovalMode controls what the coordinator does with an incoming RequestPermission. The package default is ModeAuto, which preserves legacy behavior (auto-select the first allow option). Operator-facing runtime flips this default to ModePrompt — see internal/config.

See docs/design/external-adapter-approvals-v1.md.

const (
	// ModeAuto auto-resolves every approval with the first allow option.
	// Danger mode — preserved for batch / CI / local smoke and for
	// backward compat in tests that don't construct an approval store.
	ModeAuto ApprovalMode = "auto"

	// ModePrompt blocks waiting for an operator decision (or a matching
	// grant). If no operator resolves the request before
	// HECATE_AGENT_ADAPTER_APPROVAL_TIMEOUT, it resolves to a Cancelled
	// outcome. Operators who need fully unattended adapters can set
	// HECATE_AGENT_ADAPTER_APPROVAL_MODE=auto.
	ModePrompt ApprovalMode = "prompt"

	// ModeDeny auto-rejects every approval. Audit / compliance
	// scenarios where adapter tool use must not proceed.
	ModeDeny ApprovalMode = "deny"
)

type ApprovalOption

type ApprovalOption struct {
	OptionID string `json:"option_id"`
	Kind     string `json:"kind"`
	Name     string `json:"name"`
}

ApprovalOption mirrors acp.PermissionOption in a stable wire shape we own. We keep the original adapter-supplied option_id verbatim so the resolution can route back to the exact ACP option the adapter expects.

type ApprovalResolutionPath

type ApprovalResolutionPath string

ApprovalResolutionPath labels how a pending approval got resolved. Telemetry uses this label to distinguish operator-driven decisions from grant-cache hits and from default-mode auto-resolutions.

const (
	PathOperator    ApprovalResolutionPath = "operator"
	PathGrant       ApprovalResolutionPath = "grant"
	PathDefaultMode ApprovalResolutionPath = "default_mode"
	PathTimeout     ApprovalResolutionPath = "timeout"
	// PathRequestCancelled labels approvals that resolved because
	// the request context was cancelled — session shutdown, adapter
	// teardown, HTTP context cancellation, or process stop. Distinct
	// from PathOperator (an explicit operator decision) so telemetry
	// and the SSE stream can distinguish "operator declined to act"
	// from "the request died under us."
	PathRequestCancelled ApprovalResolutionPath = "request_cancelled"
)

type ApprovalRetentionStore

type ApprovalRetentionStore interface {
	ApprovalStore

	// PruneApprovals deletes resolved (non-pending) approval rows
	// older than maxAge OR beyond maxCount, whichever fires.
	// Pending rows are never pruned. Returns total deleted.
	PruneApprovals(ctx context.Context, now time.Time, maxAge time.Duration, maxCount int) (int64, error)

	// PruneExpiredGrants removes grants whose ExpiresAt is in the
	// past. Live grants (no expiry, or future expiry) are never
	// touched. Returns total deleted.
	PruneExpiredGrants(ctx context.Context, now time.Time) (int64, error)

	// ReconcilePending sweeps any pending approval rows from a prior
	// process and marks them status=timed_out, path=startup_reconcile.
	// Process-local waiters can't be resurrected; callers must invoke
	// this at startup before serving requests. Returns rows
	// reconciled.
	ReconcilePending(ctx context.Context, now time.Time) (int64, error)

	// Prune implements retention.Pruner — one call that runs both
	// the resolved-approval sweep (subject to maxAge / maxCount)
	// and the expired-grant sweep (grants honor only ExpiresAt;
	// maxAge / maxCount don't apply). Returns the sum so operators
	// see total rows removed by this subsystem in one number.
	// Captures `now := time.Now().UTC()` internally; the
	// per-deletion `now`-tolerant methods stay on the interface for
	// tests and ad-hoc callers.
	Prune(ctx context.Context, maxAge time.Duration, maxCount int) (int, error)
}

ApprovalRetentionStore extends ApprovalStore with maintenance operations the retention worker calls. Memory and SQL stores both satisfy it; the type assertion in the worker keeps the smaller ApprovalStore interface clean for the coordinator.

type ApprovalScope

type ApprovalScope string

ApprovalScope is the breadth of an operator's "always" decision. `once` means no persistence; the others persist as a grant entry.

const (
	ApprovalScopeOnce          ApprovalScope = "once"
	ApprovalScopeSession       ApprovalScope = "session"
	ApprovalScopeWorkspaceTool ApprovalScope = "workspace_tool"
	ApprovalScopeAdapterTool   ApprovalScope = "adapter_tool"
)

type ApprovalStatus

type ApprovalStatus string

ApprovalStatus is the lifecycle state of a single approval row.

const (
	ApprovalStatusPending   ApprovalStatus = "pending"
	ApprovalStatusApproved  ApprovalStatus = "approved"
	ApprovalStatusDenied    ApprovalStatus = "denied"
	ApprovalStatusTimedOut  ApprovalStatus = "timed_out"
	ApprovalStatusCancelled ApprovalStatus = "cancelled"
)

type ApprovalStore

type ApprovalStore interface {
	// CreateApproval persists a pending approval and returns the row
	// with its assigned ID + timestamps filled in.
	CreateApproval(ctx context.Context, a Approval) (Approval, error)

	// ResolveApproval transitions a pending approval to a terminal
	// status. Returns the updated row. Returns ErrApprovalNotFound
	// when id is unknown and ErrApprovalAlreadyResolved when the row
	// is already terminal — the second writer always loses the race.
	ResolveApproval(ctx context.Context, id string, status ApprovalStatus, decision ApprovalDecision, selectedOption string, scope ApprovalScope, path ApprovalResolutionPath, note string, resolvedAt time.Time) (Approval, error)

	// GetApproval fetches an approval by id.
	GetApproval(ctx context.Context, id string) (Approval, error)

	// ListApprovals returns approvals for a session, oldest-first.
	// status="" returns all statuses; otherwise filters.
	ListApprovals(ctx context.Context, sessionID string, status ApprovalStatus) ([]Approval, error)

	// CreateGrant persists an operator-authored "always allow / always
	// deny" grant. Used by the coordinator when scope > once.
	CreateGrant(ctx context.Context, g Grant) (Grant, error)

	// ListGrants returns grants matching the filter, newest-first.
	// Expired grants (ExpiresAt <= now) are excluded.
	ListGrants(ctx context.Context, filter GrantFilter, now time.Time) ([]Grant, error)

	// DeleteGrant removes a grant by id. Returns ErrApprovalNotFound
	// when the id is unknown so the HTTP layer can surface 404.
	DeleteGrant(ctx context.Context, id string) error

	// FindMatchingGrant returns the most-specific live grant for the
	// given (sessionID, workspace, adapterID, toolKind) tuple, or
	// (Grant{}, false). Lookup walks scopes session → workspace_tool →
	// adapter_tool, returning the first match. Expired grants are
	// ignored. The returned grant carries the operator's decision.
	FindMatchingGrant(ctx context.Context, sessionID, workspace, adapterID, toolKind string, now time.Time) (Grant, bool, error)
}

ApprovalStore is the persistence interface. Memory and SQL backends both implement it. Backend selection follows HECATE_BACKEND in cmd/hecate.

type AuthenticateResult

type AuthenticateResult struct {
	AdapterID  string `json:"adapter_id"`
	Status     string `json:"status"`
	MethodID   string `json:"method_id"`
	Path       string `json:"path,omitempty"`
	DurationMS int64  `json:"duration_ms"`
}

func Authenticate

func Authenticate(ctx context.Context, adapterID string) (AuthenticateResult, error)

type AvailableCommandsUpdate

type AvailableCommandsUpdate struct {
	SessionID string
	AdapterID string
	Commands  []agentcontrols.Command
	// contains filtered or unexported fields
}

type Capability

type Capability struct {
	ID          string
	Name        string
	Description string
	Status      string
}

type CoordinatorHooks

type CoordinatorHooks struct {
	OnRequested func(approval Approval)
	OnResolved  func(approval Approval, durationMS int64)
	OnTimedOut  func(approval Approval, durationMS int64)
	// OnGrantCreated fires after a successful CreateGrant inside the
	// coordinator's resolve path (i.e. when the operator's decision
	// scope is broader than `once`). Used to drive the
	// `approval.grants_active` UpDownCounter; nil-safe.
	OnGrantCreated func(grant Grant)
	// OnGrantDeleted fires after a successful DeleteGrant. Symmetric
	// with OnGrantCreated; nil-safe.
	OnGrantDeleted func()
}

CoordinatorHooks are optional callbacks invoked by the coordinator at well-defined lifecycle points. The coordinator is the single place that knows the (adapter, tool_kind, mode, path) tuple, so all telemetry instrumentation goes through here. Implementations live in the telemetry package; the coordinator package keeps no metric dependencies of its own.

type CoordinatorOptions

type CoordinatorOptions struct {
	Mode    ApprovalMode
	Timeout time.Duration
	Store   ApprovalStore
	Logger  *slog.Logger
	// NowFunc is used by tests; defaults to time.Now.UTC.
	NowFunc func() time.Time
	// IDFunc generates approval ids; defaults to ULID-shaped hex.
	IDFunc func() string
	// Hooks are optional callbacks for telemetry. Zero values are no-ops.
	Hooks CoordinatorHooks
}

CoordinatorOptions configure an ApprovalCoordinator.

type CredentialMode

type CredentialMode struct {
	ID            string
	Name          string
	Description   string
	RemoteAllowed bool
	EnvKeys       []string
}

type Grant

type Grant struct {
	ID        string           `json:"id"`
	Scope     ApprovalScope    `json:"scope"`
	AdapterID string           `json:"adapter_id"`
	ToolKind  string           `json:"tool_kind"`
	Workspace string           `json:"workspace,omitempty"`
	SessionID string           `json:"session_id,omitempty"`
	Decision  ApprovalDecision `json:"decision"`
	GrantedBy string           `json:"granted_by,omitempty"`
	GrantedAt time.Time        `json:"granted_at"`
	ExpiresAt *time.Time       `json:"expires_at,omitempty"`
}

Grant is a persisted "always" decision. Memory and SQL backends both implement it.

type GrantFilter

type GrantFilter struct {
	AdapterID string
	Scope     ApprovalScope
	ToolKind  string
}

GrantFilter narrows ListGrants results.

type LaunchModel

type LaunchModel struct {
	ID          string
	Name        string
	Description string
	Default     bool
}

type LaunchModelConfig

type LaunchModelConfig struct {
	ConfigID       string
	ArgTemplate    []string
	ListArgs       []string
	FallbackModels []LaunchModel
}

type LaunchSelectConfig

type LaunchSelectConfig struct {
	ConfigID         string
	Name             string
	Description      string
	Category         string
	Default          string
	UnsetValue       string
	UnsetName        string
	UnsetDescription string
	ArgTemplate      []string
	Options          []LaunchSelectOption
}

type LaunchSelectOption

type LaunchSelectOption struct {
	ID          string
	Name        string
	Description string
}

type LogoutResult

type LogoutResult struct {
	AdapterID  string `json:"adapter_id"`
	Status     string `json:"status"`
	Path       string `json:"path,omitempty"`
	DurationMS int64  `json:"duration_ms"`
}

func Logout

func Logout(ctx context.Context, adapterID string) (LogoutResult, error)

type LookupFunc

type LookupFunc func(file string) (string, error)

type MemoryApprovalStore

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

MemoryApprovalStore is a goroutine-safe in-process ApprovalStore. All state lives in maps and is discarded on process exit. Suitable for tests, dev, and anyone running with HECATE_BACKEND=memory (the default).

func NewMemoryApprovalStore

func NewMemoryApprovalStore() *MemoryApprovalStore

NewMemoryApprovalStore returns an empty in-memory store.

func (*MemoryApprovalStore) CreateApproval

func (s *MemoryApprovalStore) CreateApproval(_ context.Context, a Approval) (Approval, error)

func (*MemoryApprovalStore) CreateGrant

func (s *MemoryApprovalStore) CreateGrant(_ context.Context, g Grant) (Grant, error)

CreateGrant inserts a grant row. Used by tests + the resolve handler when an operator picks a scope broader than `once`.

func (*MemoryApprovalStore) DeleteGrant

func (s *MemoryApprovalStore) DeleteGrant(_ context.Context, id string) error

DeleteGrant removes a grant by id. Returns ErrApprovalNotFound (the shared not-found sentinel) when the id is unknown.

func (*MemoryApprovalStore) FindMatchingGrant

func (s *MemoryApprovalStore) FindMatchingGrant(_ context.Context, sessionID, workspace, adapterID, toolKind string, now time.Time) (Grant, bool, error)

func (*MemoryApprovalStore) GetApproval

func (s *MemoryApprovalStore) GetApproval(_ context.Context, id string) (Approval, error)

func (*MemoryApprovalStore) ListApprovals

func (s *MemoryApprovalStore) ListApprovals(_ context.Context, sessionID string, status ApprovalStatus) ([]Approval, error)

func (*MemoryApprovalStore) ListGrants

func (s *MemoryApprovalStore) ListGrants(_ context.Context, filter GrantFilter, now time.Time) ([]Grant, error)

ListGrants returns grants matching the filter. Expired grants are dropped using the supplied now timestamp.

func (*MemoryApprovalStore) Prune

func (s *MemoryApprovalStore) Prune(ctx context.Context, maxAge time.Duration, maxCount int) (int, error)

Prune implements retention.Pruner. See ApprovalRetentionStore.Prune for the contract.

func (*MemoryApprovalStore) PruneApprovals

func (s *MemoryApprovalStore) PruneApprovals(_ context.Context, now time.Time, maxAge time.Duration, maxCount int) (int64, error)

PruneApprovals deletes resolved approval rows older than maxAge or beyond maxCount. Mirrors the SQL store's behavior so the retention worker can dispatch through ApprovalRetentionStore without caring which backend is wired. Pending rows are never auto-pruned.

func (*MemoryApprovalStore) PruneExpiredGrants

func (s *MemoryApprovalStore) PruneExpiredGrants(_ context.Context, now time.Time) (int64, error)

PruneExpiredGrants removes grants whose ExpiresAt has passed. Live grants are never touched; the retention worker must not erase operator-authored intent.

func (*MemoryApprovalStore) ReconcilePending

func (s *MemoryApprovalStore) ReconcilePending(_ context.Context, now time.Time) (int64, error)

ReconcilePending sweeps pending rows and marks them timed_out with path=startup_reconcile. The memory backend never has rows surviving a restart in practice (the map is process-local), but the method exists so memory and sqlite share the ApprovalRetentionStore surface. Returns 0 on a normal startup; non-zero only if the same process is restarted in-place (rare; e.g. tests).

func (*MemoryApprovalStore) ResolveApproval

func (s *MemoryApprovalStore) ResolveApproval(_ context.Context, id string, status ApprovalStatus, decision ApprovalDecision, selectedOption string, scope ApprovalScope, path ApprovalResolutionPath, note string, resolvedAt time.Time) (Approval, error)

type NativeSessionReplacement

type NativeSessionReplacement struct {
	PreviousNativeSessionID string
	NativeSessionID         string
	Reason                  string
}

type NativeSessionScope

type NativeSessionScope string
const (
	NativeSessionScopeProcess NativeSessionScope = "process"
	NativeSessionScopeDurable NativeSessionScope = "durable"
)

type PrepareSessionRequest

type PrepareSessionRequest struct {
	SessionID               string
	AdapterID               string
	Workspace               string
	PreviousNativeSessionID string
	ConfigOptions           []agentcontrols.ConfigOption
	MCPServers              []types.MCPServerConfig
}

type PrepareSessionResult

type PrepareSessionResult struct {
	Adapter                Adapter
	DriverKind             string
	NativeSessionID        string
	AgentInfo              *agentcontrols.ImplementationInfo
	SessionStarted         bool
	SessionResumed         bool
	SessionRecovery        string
	ConfigOptions          []agentcontrols.ConfigOption
	AvailableCommands      []agentcontrols.Command
	AvailableCommandsKnown bool
}

type ProbeAgentInfo

type ProbeAgentInfo = agentcontrols.ImplementationInfo

ProbeAgentInfo keeps the health-probe name for the shared ACP implementation metadata projection.

type ProbeAuthMethod

type ProbeAuthMethod struct {
	ID          string `json:"id"`
	Kind        string `json:"kind"`
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
}

ProbeAuthMethod is the non-secret subset of ACP Initialize authMethods that Hecate can safely surface in health responses. Env var names and terminal env payloads intentionally stay inside the adapter runtime boundary.

type ProbeResult

type ProbeResult struct {
	AdapterID            string            `json:"adapter_id"`
	Status               string            `json:"status"`
	Stage                string            `json:"stage"`
	Path                 string            `json:"path,omitempty"`
	Error                string            `json:"error,omitempty"`
	Stderr               string            `json:"stderr,omitempty"`
	Hint                 string            `json:"hint,omitempty"`
	AgentInfo            *ProbeAgentInfo   `json:"agent_info,omitempty"`
	CapabilitiesKnown    bool              `json:"capabilities_known,omitempty"`
	SupportsAuthenticate bool              `json:"supports_authenticate"`
	SupportsLogout       bool              `json:"supports_logout"`
	SupportsLoadSession  bool              `json:"supports_load_session"`
	AuthMethods          []ProbeAuthMethod `json:"auth_methods,omitempty"`
	DurationMS           int64             `json:"duration_ms"`
}

ProbeResult is the typed outcome of a single Probe call. Stage names the step that completed (on success) or that failed (on error). When Status == "auth_required" or "error", Error and Stderr carry the raw diagnostic; the UI surfaces them verbatim because adapters phrase auth failures inconsistently and any forced normalization would destroy operator-actionable detail.

func Probe

func Probe(ctx context.Context, adapterID string) (res ProbeResult)

Probe attempts a minimal start-and-handshake against the named adapter to determine whether it can prepare an ACP session. It does not prove that an embedded bridge can launch its prompt-serving vendor process, authenticate, or serve a turn. Session setup may still run bounded provider discovery.

Sequence: resolve provider/runtime command → start runtime → ACP Initialize → ACP NewSession (against a temporary workspace) → terminate. On any failure, the stage that failed is recorded and the error + captured stderr are surfaced as-is. We deliberately do NOT issue a chat prompt — the goal is "would startACPSession succeed?", not "does the adapter launch its deferred prompt-serving vendor process or produce useful output?".

Side effects: a fresh ACP session is created and immediately abandoned. Adapters that bill on session-create will see a single no-op session per probe; adapters that bill on prompt completion will see no charge. The `cwd` passed to NewSession is a temporary directory that's removed before Probe returns.

type PromptFile

type PromptFile struct {
	Filename  string
	MediaType string
	SizeBytes int64
	SHA256    string
	Data      []byte
}

PromptFile is an immutable attachment snapshot passed to one selected ACP session. Size and digest are checked again at the adapter boundary before any content block or staged file is created.

type PromptInput

type PromptInput struct {
	Text  string
	Files []PromptFile
}

PromptInput is the provider-neutral input for one External Agent turn. Attachment bodies remain Hecate-owned and are copied into this short-lived request only after session-scoped claim admission succeeds.

type RecordingContext

type RecordingContext struct {
	SessionID string
	AdapterID string
	Workspace string
}

RecordingContext bundles the per-request context that the session manager carries about an in-flight ACP RequestPermission. Kept separate from CoordinatorOptions so the coordinator is reusable across many sessions without rebuilding.

type ResolveRequest

type ResolveRequest struct {
	Decision       ApprovalDecision `json:"decision"`
	Scope          ApprovalScope    `json:"scope"`
	SelectedOption string           `json:"selected_option,omitempty"`
	Note           string           `json:"note,omitempty"`
	GrantedBy      string           `json:"granted_by,omitempty"`
}

ResolveRequest is the input to ApprovalCoordinator.Resolve. The HTTP layer parses POST bodies into this shape; coordinator-level callers (tests, future programmatic users) build it directly.

type RunRequest

type RunRequest struct {
	SessionID               string
	AdapterID               string
	Workspace               string
	PreviousNativeSessionID string
	Prompt                  PromptInput
	ConfigOptions           []agentcontrols.ConfigOption
	MCPServers              []types.MCPServerConfig
	Timeout                 time.Duration
	MaxOutputBytes          int64
	OnOutput                func(string)
	OnActivity              func(Activity)
	// OnTerminalActivity is bound to terminals created by this turn and may be
	// called after Run returns, until OnTerminalClosed reports that terminal's
	// authoritative settlement. Callers must return promptly, keep it safe for
	// concurrent late delivery, and bind it to the originating durable message
	// rather than mutable current-turn state.
	OnTerminalActivity func(Activity)
	// OnTerminalClosed runs exactly once for each successfully-created terminal,
	// after its final OnTerminalActivity callback. A session-close deadline makes
	// the emitted cancellation authoritative: no transcript callbacks occur for
	// that terminal after OnTerminalClosed returns. Callers must return promptly.
	OnTerminalClosed func(terminalID string)
	// AllowNativeSessionReplacement is explicit host authority to replace a
	// provider-native conversation that the adapter proves is missing. The
	// zero value fails closed. OnNativeSessionReplaced must durably persist the
	// new native ID before Hecate sends the prompt again.
	AllowNativeSessionReplacement bool
	OnNativeSessionReplaced       func(NativeSessionReplacement) error
}

type RunResult

type RunResult struct {
	Adapter                Adapter
	DriverKind             string
	NativeSessionID        string
	AgentInfo              *agentcontrols.ImplementationInfo
	StopReason             string
	SessionStarted         bool
	SessionResumed         bool
	SessionRecovery        string
	Output                 string
	RawOutput              string
	ExitCode               int
	StartedAt              time.Time
	CompletedAt            time.Time
	DiffStat               string
	Diff                   string
	Usage                  Usage
	ConfigOptions          []agentcontrols.ConfigOption
	AvailableCommands      []agentcontrols.Command
	AvailableCommandsKnown bool
	// contains filtered or unexported fields
}

func Run

func Run(ctx context.Context, req RunRequest) (RunResult, error)

type SQLiteApprovalStore

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

SQLiteApprovalStore is the durable backend for agent-chat approvals and operator-authored grants. It satisfies ApprovalStore plus the grant-management surface the coordinator's HTTP layer uses.

Schema is created by migrate() on construction; the store is safe to instantiate multiple times against the same client. Two tables:

  • chat_approvals — one row per RequestPermission. The full ACP payload + options are stored as JSON blobs so the wire shape can evolve without schema churn.
  • chat_approval_grants — operator-authored "always allow / always deny" decisions broader than `once` scope.

Process-local waiters are NOT persisted: a Hecate restart cannot resurrect an in-flight ACP RequestPermission. ReconcilePending() (called from cmd/hecate at startup, before the gateway accepts requests) sweeps any pending rows from a prior process and marks them status=timed_out, path=startup_reconcile.

func NewPostgresApprovalStore

func NewPostgresApprovalStore(ctx context.Context, client *storage.PostgresClient) (*SQLiteApprovalStore, error)

func NewSQLiteApprovalStore

func NewSQLiteApprovalStore(ctx context.Context, client *storage.SQLiteClient) (*SQLiteApprovalStore, error)

NewSQLiteApprovalStore opens the store and runs migrations. Returns the store ready for use.

func (*SQLiteApprovalStore) Backend

func (s *SQLiteApprovalStore) Backend() string

Backend identifies the backend choice for diagnostics + tests.

func (*SQLiteApprovalStore) CreateApproval

func (s *SQLiteApprovalStore) CreateApproval(ctx context.Context, a Approval) (Approval, error)

func (*SQLiteApprovalStore) CreateGrant

func (s *SQLiteApprovalStore) CreateGrant(ctx context.Context, g Grant) (Grant, error)

CreateGrant persists an operator-authored grant. Used by the coordinator's Resolve method when scope > once.

func (*SQLiteApprovalStore) DeleteGrant

func (s *SQLiteApprovalStore) DeleteGrant(ctx context.Context, id string) error

DeleteGrant removes a grant by id. Returns ErrApprovalNotFound when the id is unknown so the HTTP layer can surface 404 uniformly.

func (*SQLiteApprovalStore) FindMatchingGrant

func (s *SQLiteApprovalStore) FindMatchingGrant(ctx context.Context, sessionID, workspace, adapterID, toolKind string, now time.Time) (Grant, bool, error)

func (*SQLiteApprovalStore) GetApproval

func (s *SQLiteApprovalStore) GetApproval(ctx context.Context, id string) (Approval, error)

func (*SQLiteApprovalStore) ListApprovals

func (s *SQLiteApprovalStore) ListApprovals(ctx context.Context, sessionID string, status ApprovalStatus) ([]Approval, error)

func (*SQLiteApprovalStore) ListGrants

func (s *SQLiteApprovalStore) ListGrants(ctx context.Context, filter GrantFilter, now time.Time) ([]Grant, error)

ListGrants returns grants matching the filter, newest-first. Expired grants are excluded by the SQL predicate.

func (*SQLiteApprovalStore) Prune

func (s *SQLiteApprovalStore) Prune(ctx context.Context, maxAge time.Duration, maxCount int) (int, error)

Prune implements retention.Pruner. See ApprovalRetentionStore.Prune for the contract.

func (*SQLiteApprovalStore) PruneApprovals

func (s *SQLiteApprovalStore) PruneApprovals(ctx context.Context, now time.Time, maxAge time.Duration, maxCount int) (int64, error)

PruneApprovals deletes resolved (non-pending) approval rows older than maxAge or beyond maxCount, whichever fires first. Pending rows are never auto-pruned — they're caller state, not history. Returns total rows deleted.

func (*SQLiteApprovalStore) PruneExpiredGrants

func (s *SQLiteApprovalStore) PruneExpiredGrants(ctx context.Context, now time.Time) (int64, error)

PruneExpiredGrants removes grants whose ExpiresAt is in the past. Returns the number deleted. Called by the retention worker. Live grants (no expiry, or expiry in the future) are never touched.

func (*SQLiteApprovalStore) ReconcilePending

func (s *SQLiteApprovalStore) ReconcilePending(ctx context.Context, now time.Time) (int64, error)

ReconcilePending sweeps any approval rows that survived a Hecate restart in the pending state and marks them status=timed_out, path=startup_reconcile. Process-local waiters can't be resurrected, so the operator UI must not surface these as actionable.

Called from cmd/hecate at startup, after migrate() and before the gateway accepts requests. Returns the number of rows reconciled.

func (*SQLiteApprovalStore) ResolveApproval

func (s *SQLiteApprovalStore) ResolveApproval(ctx context.Context, id string, status ApprovalStatus, decision ApprovalDecision, selectedOption string, scope ApprovalScope, path ApprovalResolutionPath, note string, resolvedAt time.Time) (Approval, error)

type SessionManager

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

func NewSessionManager

func NewSessionManager() *SessionManager

func (*SessionManager) CloseSession

func (m *SessionManager) CloseSession(ctx context.Context, sessionID string) error

func (*SessionManager) Coordinator

func (m *SessionManager) Coordinator() *ApprovalCoordinator

Coordinator returns the installed approval coordinator (or nil). HTTP handlers route operator resolve/cancel calls through this.

func (*SessionManager) DeleteSession

func (m *SessionManager) DeleteSession(ctx context.Context, sessionID string) error

func (*SessionManager) PrepareSession

func (*SessionManager) Run

func (*SessionManager) SetAdapterMetrics

func (m *SessionManager) SetAdapterMetrics(metrics *telemetry.AgentAdapterMetrics)

SetAdapterMetrics installs the AgentAdapterMetrics used by every acpChatClient spawned from this manager (currently for the terminal-RPC-unsupported counter; probe metrics are wired separately via SetProbeMetrics). Nil is safe and matches the construction pattern used by tests that don't care about metrics.

func (*SessionManager) SetApprovalCoordinator

func (m *SessionManager) SetApprovalCoordinator(coordinator *ApprovalCoordinator)

SetApprovalCoordinator installs the coordinator used to handle ACP RequestPermission calls. When unset, the legacy auto-approve behavior is preserved (matches existing tests + dev workflows that build a SessionManager without going through internal/config).

func (*SessionManager) SetAvailableCommandsUpdateHook

func (m *SessionManager) SetAvailableCommandsUpdateHook(hook func(AvailableCommandsUpdate))

func (*SessionManager) SetLogger

func (m *SessionManager) SetLogger(logger *slog.Logger)

func (*SessionManager) SetSecretCipher

func (m *SessionManager) SetSecretCipher(cipher secrets.Cipher)

func (*SessionManager) SetSessionConfigOption

func (*SessionManager) SetTerminalSupportEnabled

func (m *SessionManager) SetTerminalSupportEnabled(enabled bool)

SetTerminalSupportEnabled controls whether ACP sessions advertise and honor client-side terminal/* callbacks. It is intentionally opt-in because terminal/create is a command-execution surface.

func (*SessionManager) SetWorkspaceCoordinator

func (m *SessionManager) SetWorkspaceCoordinator(registry *workspacecoord.Registry)

SetWorkspaceCoordinator installs the process-scoped registry used by ACP terminal callbacks. A terminal receives its own writer lease before spawn so it remains visible to destructive workspace operations even after the ACP turn that created it has returned. Wire it before preparing or running sessions; like the other manager launch settings, an existing session keeps the composition it started with.

func (*SessionManager) Shutdown

func (m *SessionManager) Shutdown(ctx context.Context) error

type SetSessionConfigOptionRequest

type SetSessionConfigOptionRequest struct {
	SessionID string
	ConfigID  string
	Value     string
	BoolValue *bool
}

type SetSessionConfigOptionResult

type SetSessionConfigOptionResult struct {
	ConfigOptions          []agentcontrols.ConfigOption
	AvailableCommands      []agentcontrols.Command
	AvailableCommandsKnown bool
}

type SetupCommandStatus

type SetupCommandStatus struct {
	Available      bool   `json:"available"`
	Command        string `json:"command,omitempty"`
	ExecutablePath string `json:"executable_path,omitempty"`
}

func DetectClaudeCodeCLI

func DetectClaudeCodeCLI(probe VersionProbe, lookup LookupFunc) SetupCommandStatus

type Status

type Status struct {
	Adapter
	Available            bool
	Status               string
	Path                 string
	Error                string
	AdapterVersion       string
	AgentVersion         string
	VersionOutsideRange  bool
	AuthStatus           string
	AuthError            string
	RemoteCredentialMode string
	RemoteCredentialOK   bool
	RemoteCredentialHint string
	ClaudeCodeCLI        SetupCommandStatus
}

func ApplyProbeCapabilities

func ApplyProbeCapabilities(status Status, result ProbeResult) Status

ApplyProbeCapabilities makes live ACP Initialize capabilities authoritative for a freshly probed adapter row. Static registry flags remain the fallback before a probe has successfully completed Initialize.

func CatalogStatusForAdapter

func CatalogStatusForAdapter(ctx context.Context, id string, lookup LookupFunc) (Status, bool)

CatalogStatusForAdapter returns one adapter's passive discovery state. It resolves and validates paths but never runs version, auth, or ACP commands.

func List

func List(ctx context.Context) []Status

List returns passive discovery state. It never executes a provider command.

func ListCatalog

func ListCatalog(ctx context.Context) []Status

ListCatalog returns the built-in adapter catalog using only cheap local discovery. It intentionally avoids version/auth subprocess probes so app startup and the Connections list can render immediately; explicit probe endpoints refresh the expensive health details on demand.

func ListCatalogWithLookup

func ListCatalogWithLookup(ctx context.Context, lookup LookupFunc) []Status

func ListWithLookup

func ListWithLookup(ctx context.Context, lookup LookupFunc) []Status

func StatusForAdapter

func StatusForAdapter(ctx context.Context, id string, lookup LookupFunc) (Status, bool)

func StatusForAdapterAfterExplicitProbe

func StatusForAdapterAfterExplicitProbe(ctx context.Context, id string, lookup LookupFunc) (Status, bool)

type Usage

type Usage struct {
	ContextSize          int
	ContextUsed          int
	ReportedCostAmount   string
	ReportedCostCurrency string
}

func (Usage) Empty

func (u Usage) Empty() bool

type VersionProbe

type VersionProbe struct {
	Command        string
	Args           []string
	CandidatePaths []string
}

Jump to

Keyboard shortcuts

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