extension

package
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 17 Imported by: 6

Documentation

Overview

Package extension is the client side of the extension protocol: the HTTP protocol between a product and an extension daemon, a separate process that serves providers the product does not carry itself.

A product keeps one Client per daemon. It reaches each provider the daemon serves through the Client, or through a Provider, which adapts one of them to core.Provider so a Runtime routes to it like any other provider.

Routes

Every route lives under PathPrefix. A daemon started with a shared secret accepts only requests that carry it as "Authorization: Bearer <secret>".

GET  info                       the providers served, as InfoResponse
GET  {provider}/models          the catalog for the credential, as ModelsResponse
POST {provider}/invoke          one non-streaming operation
POST {provider}/stream          one streaming operation
POST {provider}/refresh         RefreshRequest, answered with RefreshResponse
POST {provider}/oauth/start     OAuthStartRequest, answered with OAuthStartResponse
POST {provider}/oauth/poll      OAuthPollRequest, answered with OAuthPollResponse
POST {provider}/oauth/exchange  OAuthExchangeRequest, answered with OAuthExchangeResponse

An operation carries the surface's request body unchanged, with the surface in HeaderSurface, the model in HeaderModel and the body's encoding in Content-Type. invoke answers with the surface's response body; HeaderLosses, when present, holds the operation's losses. stream answers with the surface's SSE records.

Credentials

The daemon keeps no credentials. The product stores each one, sends it with every operation in the credential headers (see SetCredentialHeaders), and serializes refreshes: it calls refresh under its own lease, as tokenstore.Coordinator does with RefreshFunc, and stores the record that comes back. Sign-in works the same way: a completed oauth exchange or poll returns a record for the product to store. ProviderInfo.CredentialKind says which credential a provider needs.

Errors

A failed route answers with an error status and a JSON body, either ErrorResponse or {"error": "<message>"}. Client turns a failed operation into a *core.ProviderError classified from the status the way core classifies any upstream failure, so a router retries and fails over as it would for any other provider. A failed refresh returns a *RefreshError, whose Terminal method tells tokenstore.Coordinator whether to revoke the credential.

Security

Anyone who can reach a daemon can use its keyless providers, and every credential a product sends passes through it. Bind a daemon to the loopback interface and start it with a secret; Client sends the secret on every request.

Index

Constants

View Source
const (
	// HeaderSurface names the operation's core.ModelSurface.
	HeaderSurface = "X-Surface"
	// HeaderModel names the model the operation addresses.
	HeaderModel = "X-Model"
	// HeaderCredentialToken is the credential's secret: an access token, a
	// pasted token or an API key.
	HeaderCredentialToken = "X-Credential-Token"
	// HeaderCredentialAccount is the upstream account the credential acts for.
	HeaderCredentialAccount = "X-Credential-Account-ID"
	// HeaderCredentialType is the credential's token type, such as "Bearer"
	// or core.TokenTypeAPIKey.
	HeaderCredentialType = "X-Credential-Token-Type"
	// HeaderCredentialMetadata is the base64 of the JSON object of the
	// credential's metadata.
	HeaderCredentialMetadata = "X-Credential-Metadata"
	// HeaderLosses, on an invoke or stream answer, is the base64 of the JSON
	// list of the operation's losses.
	HeaderLosses = "X-Losses"
)

Headers of an operation.

View Source
const DefaultControlTimeout = 30 * time.Second

DefaultControlTimeout bounds info, models, refresh and every oauth step when the caller's context has no deadline. It keeps a refresh well inside tokenstore.Coordinator's default lease wait, which must exceed it.

View Source
const DefaultTimeout = 180 * time.Second

DefaultTimeout bounds an invoke when the caller's context has no deadline, and bounds how long the default client waits for the daemon to answer any request, a stream included.

View Source
const PathPrefix = "/extension/v1/"

PathPrefix is the path under which every route of version 1 of the protocol lives.

Variables

This section is empty.

Functions

func CredentialFromHeaders

func CredentialFromHeaders(header http.Header) *core.Credential

CredentialFromHeaders reads the credential SetCredentialHeaders put on an operation, or nil when the operation carries none. A token of type core.TokenTypeAPIKey becomes the credential's APIKey, as core.CredentialFromRecord does, and any other token its Token. Undecodable metadata is ignored.

func LossesFromHeader

func LossesFromHeader(header http.Header) []core.Loss

LossesFromHeader reads the losses SetLossesHeader put on an answer, or nil when there are none or they do not decode.

func SetCredentialHeaders

func SetCredentialHeaders(header http.Header, credential *core.Credential)

SetCredentialHeaders puts credential on an operation's headers. The token is the credential's Token, or its APIKey when it has no token, in which case the token type defaults to core.TokenTypeAPIKey. A nil credential sets nothing.

func SetLossesHeader

func SetLossesHeader(header http.Header, losses []core.Loss)

SetLossesHeader puts an operation's losses on its answer. No losses set nothing.

Types

type Client

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

Client talks to one extension daemon. It keeps no state between requests and is safe for concurrent use.

func NewClient

func NewClient(config Config) (*Client, error)

NewClient returns a Client for the daemon config names.

func (*Client) Info

func (c *Client) Info(ctx context.Context) (InfoResponse, error)

Info returns the providers the daemon serves.

func (*Client) Invoke

func (c *Client) Invoke(ctx context.Context, provider string, request core.Request) (core.Response, error)

Invoke performs one non-streaming operation on provider. The response's ContentType is the daemon's, and its Losses are the ones the daemon reported.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, provider string, credential *core.Credential) ([]core.ModelInfo, error)

ListModels returns provider's catalog for credential, which may be nil.

func (*Client) OAuthDriver

func (c *Client) OAuthDriver(provider string) *OAuthDriver

OAuthDriver returns the sign-in driver of provider.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, provider string, record tokenstore.Record) (tokenstore.Record, error)

Refresh asks the daemon to renew record for provider and returns the renewed record. The product calls it under its own lease; RefreshFunc adapts it to tokenstore.Coordinator. A daemon's refusal is a *RefreshError.

func (*Client) RefreshFunc

func (c *Client) RefreshFunc(provider string) tokenstore.RefreshFunc

RefreshFunc returns the refresh of provider, for tokenstore.NewCoordinator.

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, provider string, request core.Request) (core.StreamIter, error)

Stream performs one streaming operation on provider. Each frame is one complete SSE record that carries data. No deadline applies but the caller's context. The stream reports the losses the daemon put on its answer, as core.StreamLosses reads them.

type Config

type Config struct {
	// BaseURL is the daemon's origin, such as "http://127.0.0.1:18888". A
	// path, for a daemon behind a reverse proxy, is kept; PathPrefix is
	// appended to it.
	BaseURL string
	// Secret is the daemon's shared secret, sent as a bearer token. A daemon
	// started without one accepts any caller.
	Secret string
	// HTTPClient performs every request. Nil uses a client that does not
	// follow redirects and waits DefaultTimeout for response headers. A
	// client with a Timeout also cuts off streams that run longer.
	HTTPClient *http.Client
}

Config configures a Client. Products supply every value; the package reads no environment.

type CredentialKind

type CredentialKind string

CredentialKind is what a provider needs from the product to serve it.

const (
	// CredentialOAuth is a record the owner obtains by signing in through the
	// provider's oauth routes. ProviderInfo.HasRefresh says whether the
	// daemon can refresh it.
	CredentialOAuth CredentialKind = "oauth"
	// CredentialToken is a token the owner obtains elsewhere and pastes.
	CredentialToken CredentialKind = "token"
	// CredentialNone means the provider needs no credential.
	CredentialNone CredentialKind = "none"
)

type ErrorDetail

type ErrorDetail struct {
	Message string `json:"message"`
	Code    int    `json:"code,omitempty"`
}

ErrorDetail describes a failed operation.

type ErrorResponse

type ErrorResponse struct {
	Error ErrorDetail `json:"error"`
}

ErrorResponse is the body of a failed operation. Code repeats the status.

type InfoResponse

type InfoResponse struct {
	// Version is the daemon's own version.
	Version   string         `json:"version"`
	Providers []ProviderInfo `json:"providers"`
}

InfoResponse answers info.

type ModelsResponse

type ModelsResponse struct {
	Models []core.ModelInfo `json:"models"`
}

ModelsResponse answers models.

type OAuthDriver

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

OAuthDriver runs one daemon provider's sign-in for oauthflow.Service. It is a device driver and a code driver; which methods the provider accepts is in its ProviderInfo.OAuthMethods. Its errors carry the daemon's reason, which never holds token material.

func (*OAuthDriver) Exchange

func (d *OAuthDriver) Exchange(ctx context.Context, flow oauthflow.Flow, code string) (tokenstore.Record, error)

Exchange trades the authorization code of a browser or manual sign-in for a credential.

func (*OAuthDriver) Poll

Poll asks once whether the owner approved a device sign-in. The flow, secrets included, travels to the daemon, which keeps none.

func (*OAuthDriver) Start

Start begins a sign-in with the request's method, redirect URI and parameters.

type OAuthExchangeRequest

type OAuthExchangeRequest struct {
	Code string         `json:"code"`
	Flow oauthflow.Flow `json:"flow"`
}

OAuthExchangeRequest asks oauth/exchange to trade an authorization code for a credential.

type OAuthExchangeResponse

type OAuthExchangeResponse struct {
	Record tokenstore.Record `json:"record,omitzero"`
	Error  string            `json:"error,omitempty"`
}

OAuthExchangeResponse answers oauth/exchange.

type OAuthPollRequest

type OAuthPollRequest struct {
	Flow oauthflow.Flow `json:"flow"`
}

OAuthPollRequest asks oauth/poll whether the owner approved a device sign-in. Flow carries its secrets, because the daemon keeps none.

type OAuthPollResponse

type OAuthPollResponse struct {
	Result oauthflow.PollResult `json:"result,omitzero"`
	Error  string               `json:"error,omitempty"`
}

OAuthPollResponse answers oauth/poll.

type OAuthStartRequest

type OAuthStartRequest struct {
	Method      oauthflow.Method  `json:"method"`
	RedirectURI string            `json:"redirect_uri,omitempty"`
	Params      map[string]string `json:"params,omitempty"`
}

OAuthStartRequest asks oauth/start to begin a sign-in.

type OAuthStartResponse

type OAuthStartResponse struct {
	Authorization oauthflow.Authorization `json:"authorization,omitzero"`
	Error         string                  `json:"error,omitempty"`
}

OAuthStartResponse answers oauth/start.

type Provider

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

Provider adapts one provider a daemon serves to core.Provider. It serves the surfaces its ProviderInfo lists; a request for another surface fails with a *core.SurfaceError before it reaches the daemon.

func NewProvider

func NewProvider(client *Client, info ProviderInfo) *Provider

NewProvider returns the Provider of info.ID on client's daemon, such as a provider from the daemon's Info.

func (*Provider) Info

func (p *Provider) Info() ProviderInfo

Info returns what the provider was created from.

func (*Provider) Invoke

func (p *Provider) Invoke(ctx context.Context, request core.Request) (core.Response, error)

Invoke performs one non-streaming operation on the daemon.

func (*Provider) ListModels

func (p *Provider) ListModels(ctx context.Context, credential *core.Credential) ([]core.ModelInfo, error)

ListModels returns the provider's catalog for credential.

func (*Provider) NativeSurfaces

func (p *Provider) NativeSurfaces(string) []core.ModelSurface

NativeSurfaces returns the provider's surfaces, whatever the model.

func (*Provider) Stream

func (p *Provider) Stream(ctx context.Context, request core.Request) (core.StreamIter, error)

Stream performs one streaming operation on the daemon.

type ProviderInfo

type ProviderInfo struct {
	// ID addresses the provider in every route.
	ID string `json:"id"`
	// Name is the provider's display name.
	Name string `json:"name"`
	// Surfaces are the surfaces the provider serves natively.
	Surfaces []core.ModelSurface `json:"surfaces"`
	// HasOAuth reports the oauth routes, and HasRefresh the refresh route.
	HasOAuth   bool `json:"has_oauth"`
	HasRefresh bool `json:"has_refresh"`
	// Credential is what the provider needs. A daemon that predates the
	// field leaves it empty; CredentialKind interprets that.
	Credential CredentialKind `json:"credential,omitempty"`
	// OAuthMethods are the sign-in methods the oauth routes accept.
	OAuthMethods []oauthflow.Method `json:"oauth_methods,omitempty"`
}

ProviderInfo describes one provider a daemon serves.

func (ProviderInfo) CredentialKind

func (p ProviderInfo) CredentialKind() CredentialKind

CredentialKind returns what the provider needs. For a daemon that predates the Credential field, it is CredentialOAuth when the provider has oauth routes, and otherwise empty, because such a daemon does not say whether a pasted token or no credential serves the provider.

type RefreshError

type RefreshError struct {
	Provider string
	// StatusCode is the daemon's answer status.
	StatusCode int
	// Message is the daemon's reason, bounded and on one line.
	Message string
	// Permanent reports that the provider rejected the grant for good, so
	// the credential should be revoked.
	Permanent bool
}

RefreshError reports a refresh the daemon refused.

func (*RefreshError) Error

func (e *RefreshError) Error() string

func (*RefreshError) Terminal

func (e *RefreshError) Terminal() bool

Terminal reports a grant the provider rejected for good, as tokenstore.IsTerminal asks.

type RefreshRequest

type RefreshRequest struct {
	Record tokenstore.Record `json:"record"`
}

RefreshRequest asks refresh to renew a stored credential.

type RefreshResponse

type RefreshResponse struct {
	Record   tokenstore.Record `json:"record,omitzero"`
	Terminal bool              `json:"terminal,omitempty"`
	Error    string            `json:"error,omitempty"`
}

RefreshResponse answers refresh with the renewed credential or an error. Terminal reports a grant the provider rejected for good.

Jump to

Keyboard shortcuts

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