Documentation
¶
Overview ¶
Package credential implements the host-side credential mediation layer ratified by ADR-0005 and ADR-0011.
The architectural promise is that connectors never see raw credential bytes. The runtime resolves a binding (a vault entry pointed at by an action's [[bindings]] block) at outbound-request time, injects the credential into the actual request headers, and hands the connector only an opaque "capability handle" — for v1, the kind string the connector declares in its manifest's `[capabilities.credential]`.
This package owns:
- The Resolver interface — what the sandbox host functions call when a connector references a credential in an http_request envelope.
- The Credential value the resolver returns (host-side only).
- Sentinel errors so callers can distinguish "no binding" from "wrong kind" and surface them as the right ADR-0010 failure class.
- The VaultResolver reference implementation that looks up a vault.Vault entry at a given path and validates its metadata Type matches the kind the connector declared.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrBindingMissing means the action declared a binding but the // vault has no entry at the configured path. The right surface // is a `binding_required` failure to the LLM (per ADR-0010). ErrBindingMissing = errors.New("credential: no binding for this capability") // ErrCredentialKindMismatch means the vault entry exists but its // metadata Type doesn't match the kind the connector declared. // Surfaces as `capability_denied` — the connector asked for a // kind it isn't bound to. ErrCredentialKindMismatch = errors.New("credential: bound kind does not match capability") // ErrNoBindingResolver means the host-side request was attempted // without a Resolver wired up at all (e.g. a Call constructed // with a nil CredentialResolver). The runtime treats this as a // programming error, surfaced as a binding_required failure so // the user sees a recoverable message. ErrNoBindingResolver = errors.New("credential: no resolver is wired for this call") )
Sentinel errors. Resolvers wrap these via fmt.Errorf("...: %w", err) so callers can pattern-match with errors.Is.
var ErrOAuth2RefreshFailed = errors.New("credential: oauth2 refresh failed")
ErrOAuth2RefreshFailed means the runtime tried to refresh an expired access token using the stored refresh token but the provider rejected the request (revoked refresh token, expired credential, scope changed). Surfaces as `binding_failed` per ADR-0010 — the agent's tool result tells the user to run `aileron binding rebind <name>`.
var ErrRefreshFailed = errors.New("credential: oauth2 refresh failed")
ErrRefreshFailed is the typed sentinel callers wrap with errors.Is to discriminate "provider rejected our refresh" from transport errors or parse errors.
Functions ¶
func FormatBindingMissing ¶
FormatBindingMissing returns a user-facing error wrapping ErrBindingMissing with the vault path the resolver was looking for. Helpful for debugging and audit context.
func FormatKindMismatch ¶
FormatKindMismatch returns a user-facing error wrapping ErrCredentialKindMismatch.
Types ¶
type Credential ¶
type Credential struct {
// Kind is the credential's type ("oauth2" or "api_key" in v1).
// Matched against the connector's manifest declaration before
// the bytes are ever touched, so a mismatched binding fails
// fast without leaking which kind was actually stored.
Kind string
// Value is the raw credential bytes (the bearer token). Only
// crosses host-side code paths: the request signer reads it,
// then it leaves scope. Never flows into the sandbox or into
// any audit record.
Value []byte
// Region and AccessKeyID are the non-secret AWS Signature Version 4
// signing inputs a resolver may carry alongside the secret access key
// (Value) for an `aws_sigv4` binding. Both are empty for every other
// kind. They are safe to log. When non-empty they win over the
// connector manifest's region / access_key_id at signing time, which
// is how a single connector install supports several region-scoped
// bindings.
Region string
AccessKeyID string
}
Credential is the resolved binding the runtime injects into an outbound request. It is host-side only — never crossed into the sandbox guest's memory.
type OAuth2Token ¶
type OAuth2Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
TokenType string `json:"token_type,omitempty"` // "Bearer" if absent
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
TokenURL string `json:"token_url"`
Scopes []string `json:"scopes,omitempty"`
}
OAuth2Token is the structured payload persisted in the vault for an oauth2-kind binding. Stored as JSON in the entry's Value; the vault encrypts it like any other secret.
`ClientID`, `ClientSecret`, and `TokenURL` are duplicated from the connector's manifest so the resolver can refresh without re-reading the connector's `[capabilities.credential.oauth2]` block on every request. The vault encrypts the whole envelope, so persisting `ClientSecret` (when the publisher set one) does not change the security posture — see ManifestOAuth2's docstring for why it ships in the connector binary in the first place.
type OAuth2VaultResolver ¶
type OAuth2VaultResolver struct {
// Vault is the user's unlocked credential vault.
Vault vault.Vault
// VaultPath is the binding name that doubles as the vault path
// (`<kind>/<service>/<identity>`).
VaultPath string
// RefreshLeeway is how close to expiry the resolver triggers a
// refresh, to absorb clock drift and prevent a token expiring
// mid-request. 60s is a sensible default; tests inject smaller.
// Zero value means use the package default (60s).
RefreshLeeway time.Duration
// HTTPClient drives the refresh POST. nil → http.DefaultClient.
HTTPClient *http.Client
// Now returns "now" for expiry comparisons. nil → time.Now.
// Tests inject deterministic clocks.
Now func() time.Time
}
OAuth2VaultResolver wraps the api_key VaultResolver pattern with transparent token refresh. On Resolve:
- Fetches the JSON envelope from the vault.
- If the access token is within RefreshLeeway of expiring, POSTs a refresh request to the stored TokenURL using the stored RefreshToken + ClientID.
- Persists the new envelope (refresh-token rotation: Google rotates, Slack doesn't — we always store whatever the response gave back).
- Returns the (refreshed or original) access token as the Credential.Value.
The host's injectCredential is unaware of refresh. It calls Resolve and gets back access-token bytes to set as `Authorization: Bearer`.
func (*OAuth2VaultResolver) Resolve ¶
func (r *OAuth2VaultResolver) Resolve(ctx context.Context) (Credential, error)
Resolve implements Resolver.
type RefreshTokenParams ¶
type RefreshTokenParams struct {
HTTPClient *http.Client // nil → http.DefaultClient
RefreshToken string
ClientID string
ClientSecret string // empty when the OAuth client has no secret (PKCE-only)
TokenURL string
// ExtraForm adds vendor-specific fields to the POST body.
// Codex's auth.openai.com refresh, for example, sends an extra
// `scope` parameter on some flows. Empty map by default.
ExtraForm url.Values
}
RefreshTokenParams is the input bundle for DoRefresh. The caller owns the persistence shape that wraps the response; this helper owns only the HTTP exchange and parsing.
type RefreshTokenResponse ¶
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
TokenType string `json:"token_type,omitempty"`
Scope string `json:"scope,omitempty"`
}
RefreshTokenResponse is the subset of RFC 6749 §5.1 token-response fields callers need from a refresh exchange. The shape is shared across OAuth2VaultResolver (binding refresh, used by connector execution) and per-agent launch-time refresh hooks (Codex today), per ADR-0025's "factor a generic doRefresh helper" decision.
func DoRefresh ¶
func DoRefresh(ctx context.Context, p RefreshTokenParams) (RefreshTokenResponse, error)
DoRefresh POSTs an OAuth2 refresh-token grant to the provider's token endpoint and returns the parsed response. Errors are sanitized to "status N: <summary>" — the raw provider response body is NOT included in the returned error string (RFC 6749 §5.2 allows providers to include token hints in error bodies, and we don't want those leaking into user-facing error surfaces per ADR-0025 / R21). Callers that need the body for debugging can log it at debug level after applying their own redaction policy.
The helper does not persist the response. Codex's PreLaunchRefresh marshals the new tokens into its `auth.json` shape and persists via the daemon's PutAgentCredentials; OAuth2VaultResolver marshals into its OAuth2Token shape and persists via vault.Put.
type RegionalResolver ¶
type RegionalResolver interface {
Resolver
// ResolveForRegion resolves the binding whose region matches the
// supplied region. An empty region means the caller could not derive
// a region from the request; the resolver falls back to the sole
// binding when exactly one exists. When several bindings match and no
// region disambiguates them the resolver returns an error rather than
// guessing, per ADR-0006's no-silent-matching rule.
ResolveForRegion(ctx context.Context, region string) (Credential, error)
}
RegionalResolver is an optional extension of Resolver for credential kinds whose binding selection depends on the outbound request's target region. The aws_sigv4 path implements it: a single connector install may hold several region-scoped bindings, and the host parses the AWS region from the outbound host at request time and calls RegionalResolver.ResolveForRegion so the correct region's binding (and its access key id) is chosen.
Callers type-assert a Resolver to RegionalResolver; resolvers that do not implement it are resolved through the plain Resolver.Resolve.
type Resolver ¶
type Resolver interface {
// Resolve looks up the binding and returns the credential. The
// returned Credential's Kind has been validated against the
// caller's declared kind already; resolvers that detect a
// mismatch return ErrCredentialKindMismatch instead of returning
// a Credential with the wrong Kind.
Resolve(ctx context.Context) (Credential, error)
}
Resolver is the contract between the sandbox host functions and the credential mediation layer. Each call to Resolver.Resolve returns the credential bound for "the capability the connector declared, in the context of the action this Invoke is part of". Resolvers are constructed per Invoke by the executor; the sandbox host call site calls Resolve at most once per outbound request.
type VaultResolver ¶
type VaultResolver struct {
// Vault is the user's local credential vault, already unlocked
// (the launcher prompts for the passphrase before any agent
// runs, per ADR-0011).
Vault vault.Vault
// VaultPath is the path the bound credential lives at. Format
// is governed by ADR-0006's binding namespace
// (`<kind>/<identity>`); this resolver doesn't enforce the
// shape, only that the entry exists and has the right Type.
VaultPath string
// ExpectedKind is the connector's declared
// `[capabilities.credential].kind`. The fetched secret's
// Metadata.Type must equal this; otherwise the connector is
// asking for a kind it isn't bound to and we fail with
// [ErrCredentialKindMismatch].
ExpectedKind string
}
VaultResolver is the v1 reference implementation of Resolver. It looks up a single vault entry at a configured path and validates that its vault.Metadata Type matches the kind the connector declared.
Per the v1 binding model (action manifest's `[[bindings]]` block), each (connector, capability) pair maps to exactly one vault path; VaultResolver carries that mapping for one specific Invoke. The executor builds a fresh VaultResolver per step before calling the sandbox.
func (*VaultResolver) Resolve ¶
func (r *VaultResolver) Resolve(ctx context.Context) (Credential, error)
Resolve fetches the credential from the vault and validates kind.
- Vault returns no entry → ErrBindingMissing (wrapped with the path so the audit log can name what was missing).
- Vault returns an entry whose Type doesn't match → ErrCredentialKindMismatch.
- Vault returns an unrelated error → wrapped and propagated.
- Success → Credential with the exact bytes from the vault.
The bytes never leave the host process; callers are responsible for using the returned Credential immediately and not retaining a reference beyond the request signing path.