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
- Variables
- func DeclaredRisk(tool *types.MCPTool) policy.Risk
- func EnabledTools(ctx context.Context, checker enabledChecker, tenantID uint64, serviceID string, ...) (map[string]bool, error)
- func PendingDowngrade(row *types.MCPToolApproval, declared policy.Risk) bool
- func ToolRisk(row *types.MCPToolApproval, declared policy.Risk) policy.Risk
- 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)
- func (a *Adapter) ObserveRisk(ctx context.Context, tenantID uint64, serviceID, toolName string, risk string) error
- func (a *Adapter) Policy(ctx context.Context, tenantID uint64, serviceID, toolName string) (*types.MCPToolApproval, error)
- type Answer
- type AskRequest
- type BuiltinApproval
- type BulkEnabledChecker
- type Checker
- type Decision
- type Gate
- func (g *Gate) Action(ctx context.Context, tenantID uint64, serviceID string, tool *types.MCPTool) policy.Action
- func (g *Gate) AskUser(ctx context.Context, req AskRequest) (Answer, error)
- 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) MarkExecuted(ctx context.Context, pendingID string)
- func (g *Gate) RequestAndWait(ctx context.Context, req PendingRequest) (Decision, error)
- func (g *Gate) RequestBuiltinApproval(ctx context.Context, req PendingRequest) (Decision, error)
- func (g *Gate) RequestHostAndWait(ctx context.Context, req HostPendingRequest) (Decision, error)
- func (g *Gate) RequestOAuthAndWait(ctx context.Context, req OAuthPendingRequest) (Decision, error)
- func (g *Gate) RequestPageAction(ctx context.Context, req PageActionRequest) (PageActionResult, error)
- func (g *Gate) Resolve(ctx context.Context, req ResolveRequest) error
- func (g *Gate) ResolveForResume(ctx context.Context, req ResolveRequest) (hitl.Resolution, error)
- func (g *Gate) ResolveResponse(ctx context.Context, req hitl.ResolveRequest) (hitl.Resolution, error)
- func (g *Gate) ResumeAnswer(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Answer
- func (g *Gate) ResumeDecision(ctx context.Context, prompt *types.AgentPrompt, bus *event.EventBus) Decision
- func (g *Gate) ToolRisks(ctx context.Context, tenantID uint64, serviceID string, tools []*types.MCPTool) (map[string]policy.Risk, error)
- func (g *Gate) Verdict(ctx context.Context, tenantID uint64, serviceID string, tool *types.MCPTool) policy.Verdict
- func (g *Gate) WithGrants(store GrantStore) *Gate
- type GrantStore
- type HostPendingRequest
- type MCPApproval
- type OAuthPendingRequest
- type PageActionRequest
- type PageActionResult
- type PageActor
- type PendingRequest
- type ResolveRequest
- type RiskChecker
- type UserAsker
Constants ¶
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.
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.
const HostApprovalKindCommand = "host_command"
HostApprovalKindCommand marks a Lite host shell command approval.
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 ¶
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.
var AnswerableKinds = map[string]bool{ types.AgentPromptKindAskUser: true, types.AgentPromptKindToolApproval: true, types.AgentPromptKindPageAction: true, }
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
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
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.
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 ¶
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
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
MarkExecuted records that an approved call has finished running.
func (*Gate) RequestAndWait ¶
RequestAndWait emits a UI event, then blocks until Resolve, timeout, or ctx cancellation.
func (*Gate) RequestBuiltinApproval ¶ added in v0.9.0
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
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 ¶
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).