authgate

package
v0.19.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package authgate implements the AARM R10 auth-required gate — the pause-and-resume primitive behind delegated MCP consent (#330).

It is the sibling of deferpolicy (R4c): where DEFER parks the executor until a human *approves an action*, authgate parks it until a user *completes an OAuth consent* and the platform holds a grant. Both share the same shape — a goroutine blocks on a handle, an out-of-band signal resolves it, a timeout auto-fails — but the semantics differ enough to justify a distinct engine:

  • Keyed by {subject, server}, NOT taskID. A grant is per user per server; once alice consents to "atl", EVERY parked call of hers to "atl" resumes — across tasks/sessions. So one gate fans out to many waiters, and one Resolve wakes them all. (deferpolicy is one waiter per taskID.)
  • The resolution is binary — granted or timed-out. There is no approver/reject: the "approver" is the OAuth flow itself, and the signal is "a grant now exists," delivered by the platform's consent callback or a bounded re-poll of the delegated resolver.

The gate never mints a token and never sees one: it only unblocks the executor so it can re-resolve through the normal delegated path, which now succeeds because the grant exists (delegation follows authorization, design-tool-registry.md §18.5). Token custody stays with the resolver.

In-process only, like deferpolicy — a restart abandons pending gates (each blocked goroutine's stack disappears). The Engine is the seam for a future persistent implementation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Decision

type Decision string

Decision is how a parked gate resolved.

const (
	// DecisionGranted: the user consented and the platform now holds a
	// grant — the executor should re-resolve and proceed.
	DecisionGranted Decision = "granted"
	// DecisionTimeout: no consent arrived within the window — the call
	// fails, same as the pre-gate ErrNoToken behavior.
	DecisionTimeout Decision = "timeout"
	// DecisionCanceled: every waiter abandoned the gate (all contexts
	// cancelled) before consent — the gate is torn down with nothing left
	// to resume.
	DecisionCanceled Decision = "canceled"
)

type Engine

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

Engine coordinates pending auth-required gates across the runtime. Thread-safe. Every gate is single-flighted per {subject, server}: the first Await creates it and is told to deliver the consent prompt; later Awaits attach silently.

func New

func New() *Engine

New constructs an empty Engine.

func (*Engine) Await

func (e *Engine) Await(subject, server string, spec Spec) (*Handle, bool, error)

Await returns the pending gate for {subject, server}, creating it if none exists. The bool is true only for the caller that CREATED the gate — that caller (and only that caller) should deliver the consent prompt; joiners get false and must not re-deliver, or one user would get N prompts for N concurrent calls.

subject and server are required. A blank subject means no requesting user is in context, which is a caller bug (a gate can't be addressed to nobody).

func (*Engine) Peek

func (e *Engine) Peek(subject, server string) (*Handle, bool)

Peek reports whether a gate is pending for {subject, server}. Used by the resume endpoint to 404 signals that target no parked call.

func (*Engine) Pending

func (e *Engine) Pending() int

Pending returns the count of currently-parked gates. For health checks and startup logs.

func (*Engine) Resolve

func (e *Engine) Resolve(subject, server string, d Decision) error

Resolve wakes every waiter on {subject, server} with the given decision (normally DecisionGranted, from a consent callback). Idempotent — a second Resolve, or a Resolve racing the timeout, is a no-op guarded by the gate's sync.Once. Returns an error only when no gate is pending (404-like).

type Handle

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

Handle is a pending gate. Executors obtain one from Engine.Await and block on WaitCtx; the resume signal (or the timeout) resolves it, broadcasting the Resolution to every waiter at once via a closed channel.

A Handle is shared: concurrent Await calls for the same {subject, server} return the SAME *Handle. It is opaque — the engine owns its lifecycle.

func (*Handle) Deadline

func (h *Handle) Deadline() time.Time

Deadline returns the absolute time the gate auto-times-out.

func (*Handle) Server

func (h *Handle) Server() string

Server returns the MCP server name the grant is for.

func (*Handle) Spec

func (h *Handle) Spec() Spec

Spec returns the delivery/audit context captured at first Await.

func (*Handle) Subject

func (h *Handle) Subject() string

Subject returns the consenting user the gate is keyed to.

func (*Handle) WaitCtx

func (h *Handle) WaitCtx(ctx context.Context) (Resolution, error)

WaitCtx blocks until the gate resolves (consent granted, timeout, or cancellation) or ctx is cancelled. On ctx cancellation it detaches this waiter — and if it was the last one holding the gate open, tears the gate down (DecisionCanceled) so an abandoned prompt doesn't linger to its timeout. Returns (Resolution{}, ctx.Err()) on cancellation.

type Resolution

type Resolution struct {
	Decision Decision
	At       time.Time
}

Resolution is the outcome delivered to every waiter on a gate.

func (Resolution) Granted

func (r Resolution) Granted() bool

Granted reports whether the resolution means "proceed."

type Spec

type Spec struct {
	// Timeout overrides defaultTimeout when > 0.
	Timeout time.Duration
	// TaskID / Session identify the request that first tripped the gate,
	// for audit and for routing the consent prompt back to the right
	// conversation. Later joiners' task/session are intentionally dropped —
	// one consent serves them all.
	TaskID  string
	Session string
	// CorrelationID is the invocation id of the request that first tripped the
	// gate. It travels with the first waiter so the resume paths (the loopback
	// OAuth callback, POST /mcp/consent) can attribute the completion egress +
	// audit back to the still-in-flight parked invocation (#366).
	CorrelationID string
}

Spec carries the delivery/audit context for a gate. None of these fields are part of the dedup key — the key is {subject, server} — they travel with the FIRST waiter so the consent prompt can be addressed and audited.

Jump to

Keyboard shortcuts

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