approval

package
v0.8.3 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0, MIT Imports: 15 Imported by: 0

Documentation

Overview

Package approval implements human-in-the-loop gating for dangerous MCP tool calls (issue #1173).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrPendingNotFound is returned when Resolve is called with an unknown id.
	ErrPendingNotFound = errors.New("tool approval pending not found")
	// ErrTenantMismatch is returned when Resolve tenant does not match the pending request.
	ErrTenantMismatch = errors.New("workspace mismatch for tool approval")
	// ErrAlreadyResolved is returned when Resolve loses the race against a
	// timeout / cancellation: the waiter is still in the map but its decision
	// channel was already consumed by the timer/ctx branch.
	ErrAlreadyResolved = errors.New("tool approval already resolved")
	// ErrUserMismatch is returned when Resolve is invoked by a user that does
	// not own the originating session.
	ErrUserMismatch = errors.New("user mismatch for tool approval")
)

Functions

func EnabledTools

func EnabledTools(
	ctx context.Context,
	checker enabledChecker,
	tenantID uint64,
	serviceID string,
	names []string,
) (map[string]bool, error)

EnabledTools uses batch policies where available while retaining compatibility with custom gates implementing only the original single-tool contract.

Types

type Adapter

type Adapter struct {
	Svc interface {
		IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
		IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
	}
}

Adapter makes MCPToolApprovalService satisfy Checker without importing the service package here.

func (*Adapter) EnabledTools

func (a *Adapter) EnabledTools(
	ctx context.Context,
	tenantID uint64,
	serviceID string,
	names []string,
) (map[string]bool, error)

EnabledTools reads service policies in one batch when supported by the service.

func (*Adapter) IsEnabled

func (a *Adapter) IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)

IsEnabled implements Checker.

func (*Adapter) IsRequired

func (a *Adapter) IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)

IsRequired implements Checker.

type BulkEnabledChecker

type BulkEnabledChecker interface {
	EnabledTools(context.Context, uint64, string, []string) (map[string]bool, error)
}

BulkEnabledChecker avoids a query per tool when enumerating a directory. Implementations return an explicit decision for each requested name.

type Checker

type Checker interface {
	IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
	IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
}

Checker answers per-tool MCP policy questions used during registration and execution.

type Decision

type Decision struct {
	Approved        bool
	ModifiedArgs    json.RawMessage // optional JSON object; when set and Approved, replaces original args
	Reason          string
	TimedOut        bool
	ContextCanceled bool
}

Decision is the outcome of a pending tool approval.

type Gate

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

Gate coordinates wait/resolve for MCP tool approvals.

Pending waiters live in-memory on the instance that started RequestAndWait. When a redis client is supplied, Resolve calls hitting any replica are published over Redis Pub/Sub so the owning instance can deliver the decision (issue #1173 cross-instance support). Without redis, the gate degrades to single-process behavior (deployments must use sticky sessions).

func NewGate

func NewGate(cfg *config.Config, checker Checker, rdb *redis.Client) *Gate

NewGate builds a gate. checker may be nil (disables gating). cfg may be nil (defaults apply). rdb may be nil (single-instance mode).

func (*Gate) EnabledTools

func (g *Gate) EnabledTools(
	ctx context.Context,
	tenantID uint64,
	serviceID string,
	names []string,
) (map[string]bool, error)

EnabledTools delegates directory policy checks to the configured checker.

func (*Gate) IsEnabled

func (g *Gate) IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)

IsEnabled reports whether a per-tool MCP policy allows registration or execution. An absent gate/checker keeps tools enabled (policy store is optional). A missing tenant or tool identity is fail-closed: the caller cannot attribute a tenant-scoped disable flag, so the tool is not allowed.

func (*Gate) NeedsApproval

func (g *Gate) NeedsApproval(ctx context.Context, tenantID uint64, serviceID, toolName string) bool

NeedsApproval returns whether execution should pause for human confirmation.

func (*Gate) RequestAndWait

func (g *Gate) RequestAndWait(ctx context.Context, req PendingRequest) (Decision, error)

RequestAndWait emits a UI event, then blocks until Resolve, timeout, or ctx cancellation.

func (*Gate) RequestOAuthAndWait

func (g *Gate) RequestOAuthAndWait(ctx context.Context, req OAuthPendingRequest) (Decision, error)

RequestOAuthAndWait emits an "MCP OAuth required" UI event, then blocks until the user authorizes (delivered via Resolve), the wait times out, or the request ctx is canceled. A returned Decision.Approved==true means the user completed authorization and the tool call should be retried.

Unlike RequestAndWait this does NOT consult the approval checker — it is driven reactively by an authorization-required error from the MCP transport.

func (*Gate) Resolve

func (g *Gate) Resolve(tenantID uint64, userID, pendingID string, d Decision) error

Resolve completes a pending approval. tenantID must match the tenant that started the wait; userID, when non-zero, must match the session owner.

If the waiter is not on this instance and Redis Pub/Sub is configured, the decision is fanned out to all replicas and ack'd via a reply key in Redis so the caller can distinguish "delivered elsewhere" from "no instance had the pending" (issue #1173 follow-up).

type MCPApproval

type MCPApproval interface {
	NeedsApproval(ctx context.Context, tenantID uint64, serviceID, toolName string) bool
	IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
	RequestAndWait(ctx context.Context, req PendingRequest) (Decision, error)
}

MCPApproval is the surface used by MCPTool (mockable in tests).

type OAuthPendingRequest

type OAuthPendingRequest struct {
	TenantID           uint64
	UserID             string // session owner; required for Resolve authorization
	SessionID          string
	AssistantMessageID string
	RequestID          string
	EventBus           *event.EventBus
	ServiceID          string
	ServiceName        string
	MCPToolName        string
	ToolCallID         string
	// WaitTimeout overrides the gate's default wait timeout when > 0. The wait
	// is always bounded (either by this value, the gate default, or ctx
	// cancellation) so the blocked goroutine never leaks.
	WaitTimeout time.Duration
}

OAuthPendingRequest carries everything needed to prompt the user to authorize an OAuth-enabled MCP service mid-conversation and block until they do.

type PendingRequest

type PendingRequest struct {
	TenantID           uint64
	UserID             string // owner of the session that initiated the call (used for Resolve authorization); empty disables user check
	SessionID          string
	AssistantMessageID string
	RequestID          string
	EventBus           *event.EventBus
	ServiceID          string
	ServiceName        string
	MCPToolName        string // name on MCP server
	RegisteredToolName string // registry name e.g. mcp_svc_tool
	Description        string
	Args               json.RawMessage
	ToolCallID         string
}

PendingRequest carries everything needed to block and notify the UI.

Jump to

Keyboard shortcuts

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