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
- func CredentialFromHeaders(header http.Header) *core.Credential
- func LossesFromHeader(header http.Header) []core.Loss
- func SetCredentialHeaders(header http.Header, credential *core.Credential)
- func SetLossesHeader(header http.Header, losses []core.Loss)
- type Client
- func (c *Client) Info(ctx context.Context) (InfoResponse, error)
- func (c *Client) Invoke(ctx context.Context, provider string, request core.Request) (core.Response, error)
- func (c *Client) ListModels(ctx context.Context, provider string, credential *core.Credential) ([]core.ModelInfo, error)
- func (c *Client) OAuthDriver(provider string) *OAuthDriver
- func (c *Client) Refresh(ctx context.Context, provider string, record tokenstore.Record) (tokenstore.Record, error)
- func (c *Client) RefreshFunc(provider string) tokenstore.RefreshFunc
- func (c *Client) Stream(ctx context.Context, provider string, request core.Request) (core.StreamIter, error)
- type Config
- type CredentialKind
- type ErrorDetail
- type ErrorResponse
- type InfoResponse
- type ModelsResponse
- type OAuthDriver
- func (d *OAuthDriver) Exchange(ctx context.Context, flow oauthflow.Flow, code string) (tokenstore.Record, error)
- func (d *OAuthDriver) Poll(ctx context.Context, flow oauthflow.Flow) (oauthflow.PollResult, error)
- func (d *OAuthDriver) Start(ctx context.Context, request oauthflow.StartRequest) (oauthflow.Authorization, error)
- type OAuthExchangeRequest
- type OAuthExchangeResponse
- type OAuthPollRequest
- type OAuthPollResponse
- type OAuthStartRequest
- type OAuthStartResponse
- type Provider
- func (p *Provider) Info() ProviderInfo
- func (p *Provider) Invoke(ctx context.Context, request core.Request) (core.Response, error)
- func (p *Provider) ListModels(ctx context.Context, credential *core.Credential) ([]core.ModelInfo, error)
- func (p *Provider) NativeSurfaces(string) []core.ModelSurface
- func (p *Provider) Stream(ctx context.Context, request core.Request) (core.StreamIter, error)
- type ProviderInfo
- type RefreshError
- type RefreshRequest
- type RefreshResponse
Constants ¶
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.
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.
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.
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 ¶
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.
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 (*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 ¶
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 ¶
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 ¶
func (d *OAuthDriver) Poll(ctx context.Context, flow oauthflow.Flow) (oauthflow.PollResult, error)
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 ¶
func (d *OAuthDriver) Start(ctx context.Context, request oauthflow.StartRequest) (oauthflow.Authorization, error)
Start begins a sign-in with the request's method, redirect URI and parameters.
type OAuthExchangeRequest ¶
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 ¶
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) 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.
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.