Documentation
¶
Overview ¶
Package approval implements human-in-the-loop gating for dangerous MCP tool calls (issue #1173).
Index ¶
- Variables
- func EnabledTools(ctx context.Context, checker enabledChecker, tenantID uint64, serviceID string, ...) (map[string]bool, error)
- type Adapter
- func (a *Adapter) EnabledTools(ctx context.Context, tenantID uint64, serviceID string, names []string) (map[string]bool, error)
- func (a *Adapter) IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
- func (a *Adapter) IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
- type BulkEnabledChecker
- type Checker
- type Decision
- type Gate
- func (g *Gate) EnabledTools(ctx context.Context, tenantID uint64, serviceID string, names []string) (map[string]bool, error)
- func (g *Gate) IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
- func (g *Gate) NeedsApproval(ctx context.Context, tenantID uint64, serviceID, toolName string) bool
- func (g *Gate) RequestAndWait(ctx context.Context, req PendingRequest) (Decision, error)
- func (g *Gate) RequestOAuthAndWait(ctx context.Context, req OAuthPendingRequest) (Decision, error)
- func (g *Gate) Resolve(tenantID uint64, userID, pendingID string, d Decision) error
- type MCPApproval
- type OAuthPendingRequest
- type PendingRequest
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
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 ¶
NeedsApproval returns whether execution should pause for human confirmation.
func (*Gate) RequestAndWait ¶
RequestAndWait emits a UI event, then blocks until Resolve, timeout, or ctx cancellation.
func (*Gate) RequestOAuthAndWait ¶
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 ¶
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.