approval

package
v0.9.0 Latest Latest
Warning

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

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

Documentation

Overview

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

Gate owns the per-tool policy (enabled / requires approval) and adapts tool approvals and in-conversation MCP OAuth to the generic prompt broker in package hitl, which handles persistence, waiting and cross-replica resolve.

Index

Constants

View Source
const DefaultPageActionTimeout = 20 * time.Second

DefaultPageActionTimeout bounds how long a run waits for the browser. The browser answers by itself, so a page that does not answer quickly is gone.

View Source
const DeferredApprovalReason = "it needs the user's approval, and this run cannot wait for it"

DeferredApprovalReason is the refusal of a tool approval raised in a deferred run that cannot save its state to wait for the answer.

View Source
const HostApprovalKindCommand = "host_command"

HostApprovalKindCommand marks a Lite host shell command approval.

View Source
const UnattendedApprovalReason = "it needs the user's approval, which nobody can give in this run"

UnattendedApprovalReason is the refusal of a tool approval raised in a run nobody can answer (types.RunModeUnattended).

Variables

View Source
var (
	ErrPendingNotFound = hitl.ErrPromptNotFound
	ErrTenantMismatch  = hitl.ErrTenantMismatch
	ErrAlreadyResolved = hitl.ErrAlreadyResolved
	ErrUserMismatch    = hitl.ErrUserMismatch
	ErrInvalidDecision = hitl.ErrInvalidResponse
)

Errors returned by Resolve; they alias the prompt broker's errors.

AnswerableKinds are the prompt kinds a user may answer with a raw response. MCP OAuth is absent: only the OAuth callback may report an authorization, never the user's own claim.

Functions

func DeclaredRisk added in v0.9.0

func DeclaredRisk(tool *types.MCPTool) policy.Risk

DeclaredRisk is the risk a service's annotations give its tool. Only readOnlyHint lowers it: MCP defaults destructiveHint to true, so reading it would make every unannotated tool destructive. Destructive is set by the workspace.

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.

func PendingDowngrade added in v0.9.0

func PendingDowngrade(row *types.MCPToolApproval, declared policy.Risk) bool

PendingDowngrade reports whether the service now declares a lower risk than the one in force, which only the workspace can accept.

func ToolRisk added in v0.9.0

func ToolRisk(row *types.MCPToolApproval, declared policy.Risk) policy.Risk

ToolRisk is the risk a stored policy and the service's declaration give a tool. The workspace's explicit risk wins; otherwise the declaration counts unless the service declared a higher risk before, so a service cannot lower its own tool's risk without the workspace agreeing.

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)
		Policy(ctx context.Context, tenantID uint64, serviceID, toolName string) (*types.MCPToolApproval, error)
		ObserveRisk(ctx context.Context, tenantID uint64, serviceID, toolName string, risk string) 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.

func (*Adapter) ObserveRisk added in v0.9.0

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

ObserveRisk implements RiskChecker.

func (*Adapter) Policy added in v0.9.0

func (a *Adapter) Policy(
	ctx context.Context, tenantID uint64, serviceID, toolName string,
) (*types.MCPToolApproval, error)

Policy implements RiskChecker.

type Answer added in v0.9.0

type Answer struct {
	PromptID string
	Answered bool
	Response types.AskUserPromptResponse
	Reason   string
	TimedOut bool
	Canceled bool
	// Suspended is set when the run must end and wait for the answer.
	Suspended *types.PromptSuspension
	// contains filtered or unexported fields
}

Answer is how an ask_user prompt ended.

type AskRequest added in v0.9.0

type AskRequest struct {
	TenantID           uint64
	UserID             string
	SessionID          string
	AssistantMessageID string
	RequestID          string
	ToolCallID         string
	EventBus           *event.EventBus
	Question           types.AskUserPromptRequest
	// Snapshot, when set, lets the run suspend after the hot wait.
	Snapshot        []byte
	SnapshotVersion int
}

AskRequest raises an ask_user prompt.

type BuiltinApproval added in v0.9.0

type BuiltinApproval interface {
	RequestBuiltinApproval(ctx context.Context, req PendingRequest) (Decision, error)
	ResumeDecision(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Decision
	MarkExecuted(ctx context.Context, pendingID string)
}

BuiltinApproval confirms built-in tool calls that change user data. It has no per-tool policy: every call it is asked about needs the user's answer.

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 {

	// PendingID identifies the prompt the decision belongs to; callers that
	// execute an approved call report back with MarkExecuted(PendingID).
	PendingID       string
	Approved        bool
	ModifiedArgs    json.RawMessage // optional JSON object; when set and Approved, replaces original args
	Reason          string
	TimedOut        bool
	ContextCanceled bool
	// AllowInSession, on approval, also allows later calls with the same
	// grant in the session.
	AllowInSession bool
	// Granted marks an approval given earlier in the session: no prompt was
	// raised, so there is no PendingID.
	Granted bool
	// Suspended is set when no decision arrived during the in-memory wait
	// and the prompt now outlives the run; the call must not execute.
	Suspended *types.PromptSuspension
	// Scope is "once" or "session" for host approvals; empty means once.
	Scope string
	// Access narrows a host grant to "read"; empty keeps the proposal.
	Access string
	// contains filtered or unexported fields
}

Decision is the outcome of a pending tool approval.

type Gate

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

Gate applies per-tool MCP policy and raises approval / OAuth prompts.

func NewGate

func NewGate(cfg *config.Config, checker Checker, broker *hitl.Broker) *Gate

NewGate builds a gate. checker may be nil (disables gating). cfg may be nil (defaults apply). broker may be nil (process-local prompts, single replica).

func (*Gate) Action added in v0.9.0

func (g *Gate) Action(ctx context.Context, tenantID uint64, serviceID string, tool *types.MCPTool) policy.Action

Action is the policy action of calling tool: its risk, and whether the workspace marked it as always requiring approval.

func (*Gate) AskUser added in v0.9.0

func (g *Gate) AskUser(ctx context.Context, req AskRequest) (Answer, error)

AskUser shows the question and waits for the answer, the timeout, or the suspension of the run.

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) MarkExecuted added in v0.9.0

func (g *Gate) MarkExecuted(ctx context.Context, pendingID string)

MarkExecuted records that an approved call has finished running.

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) RequestBuiltinApproval added in v0.9.0

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

RequestBuiltinApproval asks the user to approve a built-in tool call. Unlike RequestAndWait it has no MCP policy to consult and never approves on its own: without a broker or an event bus to reach the user, the call is refused.

func (*Gate) RequestHostAndWait added in v0.9.0

func (g *Gate) RequestHostAndWait(ctx context.Context, req HostPendingRequest) (Decision, error)

RequestHostAndWait blocks until the session owner answers a Lite host command approval, the wait times out, or ctx is canceled. Unlike RequestAndWait it never consults the MCP checker: a nil checker must not turn a host question into an automatic yes.

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) RequestPageAction added in v0.9.0

func (g *Gate) RequestPageAction(ctx context.Context, req PageActionRequest) (PageActionResult, error)

RequestPageAction sends the action to the browser and waits for its answer. The prompt never suspends the run: the page is read or filled now or not at all.

func (*Gate) Resolve

func (g *Gate) Resolve(ctx context.Context, req ResolveRequest) error

Resolve records a user decision on a pending prompt and wakes its waiter. A suspended prompt answered this way is not resumed; use ResolveForResume.

func (*Gate) ResolveForResume added in v0.9.0

func (g *Gate) ResolveForResume(ctx context.Context, req ResolveRequest) (hitl.Resolution, error)

ResolveForResume records a user decision. When the prompt was suspended, the returned resolution asks the caller to resume its run.

func (*Gate) ResolveResponse added in v0.9.0

func (g *Gate) ResolveResponse(ctx context.Context, req hitl.ResolveRequest) (hitl.Resolution, error)

ResolveResponse records a user's answer given as the kind's response JSON. Authorization and validation are the same as ResolveForResume.

func (*Gate) ResumeAnswer added in v0.9.0

func (g *Gate) ResumeAnswer(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Answer

ResumeAnswer implements UserAsker.

func (*Gate) ResumeDecision added in v0.9.0

func (g *Gate) ResumeDecision(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Decision

ResumeDecision turns the answered prompt a resumed run was suspended on into the decision for its blocked call, as if RequestAndWait had just returned it.

func (*Gate) ToolRisks added in v0.9.0

func (g *Gate) ToolRisks(
	ctx context.Context, tenantID uint64, serviceID string, tools []*types.MCPTool,
) (map[string]policy.Risk, error)

ToolRisks returns the risk of each tool of a service in one policy read, without recording what the service declares.

func (*Gate) Verdict added in v0.9.0

func (g *Gate) Verdict(ctx context.Context, tenantID uint64, serviceID string, tool *types.MCPTool) policy.Verdict

Verdict decides a call of an MCP tool under the run's access level: the tool's risk comes from the workspace's policy and the service's annotations, and a tool the workspace marked as requiring approval always asks.

func (*Gate) WithGrants added in v0.9.0

func (g *Gate) WithGrants(store GrantStore) *Gate

WithGrants makes the gate remember "allow for the rest" approvals in store. Without one, every call asks.

type GrantStore added in v0.9.0

type GrantStore interface {
	FindGrant(ctx context.Context, tenantID uint64, userID string, scope types.GrantScope, key string) (
		*types.AgentGrant, error)
	SaveGrant(ctx context.Context, grant *types.AgentGrant) error
	TouchGrant(ctx context.Context, id string, at time.Time) error
}

GrantStore keeps the approvals a user allowed to be remembered.

type HostPendingRequest added in v0.9.0

type HostPendingRequest struct {
	TenantID           uint64
	UserID             string
	SessionID          string
	AssistantMessageID string
	RequestID          string
	ToolCallID         string
	EventBus           *event.EventBus
	Host               event.HostApprovalPayload
}

HostPendingRequest asks the session owner about one Lite host command.

type MCPApproval

type MCPApproval interface {
	// Action is the policy action of calling tool; the caller decides it
	// under the run's access level.
	Action(ctx context.Context, tenantID uint64, serviceID string, tool *types.MCPTool) policy.Action
	IsEnabled(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
	RequestAndWait(ctx context.Context, req PendingRequest) (Decision, error)
	// ResumeDecision is the decision carried by the answered prompt a resumed
	// run was suspended on.
	ResumeDecision(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Decision
	// MarkExecuted records that an approved call has finished running, so an
	// approval is never mistaken for one still in flight.
	MarkExecuted(ctx context.Context, pendingID string)
}

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 PageActionRequest added in v0.9.0

type PageActionRequest struct {
	TenantID           uint64
	UserID             string
	SessionID          string
	AssistantMessageID string
	RequestID          string
	ToolCallID         string
	EventBus           *event.EventBus
	Action             types.PageActionPromptRequest
	// Timeout overrides DefaultPageActionTimeout when > 0.
	Timeout time.Duration
}

PageActionRequest asks the user's browser to read or fill the open page.

type PageActionResult added in v0.9.0

type PageActionResult struct {
	Answered bool
	Response types.PageActionPromptResponse
	Reason   string
	TimedOut bool
	Canceled bool
}

PageActionResult is how the browser answered.

type PageActor added in v0.9.0

type PageActor interface {
	RequestPageAction(ctx context.Context, req PageActionRequest) (PageActionResult, error)
}

PageActor is the surface the page tools use (mockable in tests).

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
	// Grant, when set, names the kind of call for matching remembered
	// grants; see types.ToolApprovalPromptRequest. GrantRisk is the call's
	// risk, empty meaning write: only a write may be remembered from the
	// card, while a standing grant may also cover a destructive call.
	Grant      string
	GrantScope string
	GrantRisk  policy.Risk
	// Snapshot, when set, is the run state to resume from: the wait is cut
	// to the hot-wait window and then suspends instead of timing out.
	Snapshot        []byte
	SnapshotVersion int
}

PendingRequest carries everything needed to block and notify the UI.

type ResolveRequest added in v0.9.0

type ResolveRequest struct {
	TenantID  uint64
	UserID    string
	SessionID string
	Kind      string
	PendingID string
	Decision  Decision
}

ResolveRequest is a user decision on a tool approval or OAuth prompt. SessionID, when set, must match the prompt. Kind selects how Decision is encoded and must match the prompt; empty means a tool approval.

type RiskChecker added in v0.9.0

type RiskChecker interface {
	// Policy returns the stored policy of a tool, or nil when it has none.
	Policy(ctx context.Context, tenantID uint64, serviceID, toolName string) (*types.MCPToolApproval, error)
	// ObserveRisk records a risk the service declared; the stored one only rises.
	ObserveRisk(ctx context.Context, tenantID uint64, serviceID, toolName string, risk string) error
}

RiskChecker reads and records the risk policy of MCP tools. A Checker that does not implement it leaves every MCP tool at RiskWrite.

type UserAsker added in v0.9.0

type UserAsker interface {
	AskUser(ctx context.Context, req AskRequest) (Answer, error)
	// ResumeAnswer is the answer carried by the prompt a resumed run was
	// suspended on.
	ResumeAnswer(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Answer
}

UserAsker is the surface the ask_user tool uses (mockable in tests).

Jump to

Keyboard shortcuts

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