Documentation
¶
Overview ¶
Package capture is a provider-independent substrate for acquiring a credential by driving an interactive CLI login inside a container and reading the produced token back out.
It is the tool-agnostic core extracted from the bespoke `aileron auth github` device-flow capture (#1286). All tool-specific knowledge — the container image, the container name, the login arg vector, the token-read arg vector, the config-dir env, the optional BROWSER shim, the vault path, and the kind metadata stamp — is a parameter on Driver. Nothing about any particular CLI (gh, github.com, user/github, --web, …) is compiled into this package.
The driver runs the SAME named container for both execs: an interactive login writes the tool's credential into the container's config home, and a second exec reads it back. An ephemeral second container would not see what the login wrote, so the shared container is load-bearing — the lifecycle (clear stale name, start, login, read, teardown-on-every-exit) is the substrate this package owns.
The vault PUT is NOT owned here. `internal` cannot import `cmd`, where the daemon vault transport lives, so storage is expressed as an injected StoreFunc seam. The driver stamps the captured bytes with the configured kind and hands them to Store; the HTTP status → message mapping stays caller-side (#1288). The driver surfaces the store error verbatim.
This file defines the declarative capture-descriptor format: the "config, not code" half of credential acquisition. The capture.Driver (capture.go) is the tool-agnostic "code" half — it drives an interactive CLI login inside a container and reads the token back out, with every tool-specific value a field. This file supplies the missing declarative format so a CLI's acquisition knowledge (the login arg vector, the token-read arg vector, the optional config-dir env and BROWSER shim, the vault path, and the kind stamp) ships as YAML data rather than bespoke Go. `gh` is the one built-in descriptor; adding a second tool is a new YAML under defaults/, never new Go.
The format is the capture-scoped sibling of internal/proxybinding's host->credential binding descriptor. It deliberately uses distinct identifiers (CaptureDescriptor, ParseCaptureDescriptor, CaptureSchemaVersion) so the two formats never blur together, even though they live in different packages. This package neither imports nor depends on proxybinding.
Index ¶
Constants ¶
const CaptureSchemaVersion = "v1"
CaptureSchemaVersion is the only capture-descriptor schema version this loader understands. The format is versioned so it can evolve under 0.0.x without a silent misparse: a descriptor that names any other version is a load-time error rather than a best-effort decode against the wrong field set.
Variables ¶
var ErrEmptyToken = errors.New("capture: token read returned an empty value; login did not complete")
ErrEmptyToken is returned by Acquire when the login exec completes but the token read yields only whitespace. The token is never stored in that case: an empty credential would fail closed at the binding boundary, so the driver reports the incomplete login rather than persisting a useless entry.
Functions ¶
func DefaultUserCapturePath ¶
func DefaultUserCapturePath() string
DefaultUserCapturePath returns the per-user capture-descriptor file path, `~/.aileron/capture-descriptors.yaml`, the highest-precedence layer of the two-layer config convention. When the home directory cannot be resolved it falls back to a relative path under `.aileron`, matching the rest of the config handling. The file need not exist; an absent user layer contributes no descriptors.
Types ¶
type CaptureDescriptor ¶
type CaptureDescriptor struct {
// Version is the schema version. It must equal [CaptureSchemaVersion];
// any other value is rejected at parse time so the format can evolve
// under 0.0.x without a silent misparse.
Version string `yaml:"version"`
// Name is the registry key, unique within a single layer. The registry
// resolves a descriptor by this name (e.g. "gh"); across layers a later
// layer's descriptor with the same Name overrides an earlier one.
Name string `yaml:"name"`
// Image is an optional pre-resolved container image. It is normally
// empty: image / base-image resolution policy stays caller-side, and
// [CaptureDescriptor.Apply] takes a resolved image string from the
// caller. A non-empty value here is a descriptor-supplied default the
// caller may override.
Image string `yaml:"image"`
// ContainerName is the deterministic name for the long-lived capture
// container (e.g. "aileron-auth-github"). Required.
ContainerName string `yaml:"container_name"`
// LoginCmd is the interactive login command run inside the container,
// minus the exec scaffolding — e.g.
// {"gh","auth","login","--web"}. The driver prepends the exec/-i/-t/
// --env tokens and the container name. Required and non-empty; maps to
// Driver.LoginArgs.
LoginCmd []string `yaml:"login_cmd"`
// TokenCmd is the token-read command run inside the same container,
// e.g. {"gh","auth","token"}. Required and non-empty; maps to
// Driver.TokenArgs.
TokenCmd []string `yaml:"token_cmd"`
// BrowserShim, when non-empty, is passed as `--env=BROWSER=<shim>` on
// the login exec only (e.g. "echo"), to turn a missing-browser open
// into a clean no-op. Optional; maps to Driver.BrowserShim.
BrowserShim string `yaml:"browser_shim"`
// ConfigDir, when non-empty, is a single `K=V` env token (e.g.
// "GH_CONFIG_DIR=/path") passed on BOTH execs so the token read sees
// the same config home the login wrote to. Optional; omitted/empty when
// unset; maps to Driver.ConfigDirEnv. `gh` leaves this empty (it writes
// the default ~/.config/gh/hosts.yml), so setting it would diverge the
// exec arg vectors from the bespoke flow.
ConfigDir string `yaml:"config_dir"`
// StoreAt is the logical vault key the captured credential is stored
// at (e.g. "user/github"), not a full daemon path. Required; maps to
// Driver.StoreAt. The production StoreFunc expands it to
// /vault/<store_at>/credentials.
StoreAt string `yaml:"store_at"`
// Kind is the metadata Type stamped on the stored credential (e.g.
// "user"). Required; maps to Driver.Kind.
Kind string `yaml:"kind"`
}
CaptureDescriptor is a parsed, versioned capture-descriptor document. One document declares a single tool's credential-acquisition knowledge, keyed by Name. A CLI vendor or community profile ships a CaptureDescriptor; the loader merges the built-in and user layers into a single validated set keyed on Name.
Every field is non-secret data. The descriptor carries no credential bytes: it names where the captured token is stored (StoreAt) and the kind stamp, but the actual capture, the resolved image, and the vault transport are supplied by the caller via CaptureDescriptor.Apply.
func LoadCaptureDescriptors ¶
func LoadCaptureDescriptors(opts CaptureLoadOptions) (map[string]CaptureDescriptor, []CaptureDescriptor, error)
LoadCaptureDescriptors merges the configuration layers (built-in defaults, then the optional in-memory unit-derived layer, then user) into a single validated set of descriptors keyed on name. This mirrors the layered config convention used elsewhere: the later layer overrides the earlier one per name, so a user descriptor can replace a shipped tool descriptor for the same name without editing it.
Precedence is strictly built-in < unit-derived < user. The unit-derived layer (opts.ExtraDescriptors) is the image-projected layer; an unset (nil) extra layer is a no-op that reproduces the two-layer result. The returned map is keyed on descriptor name. The returned slice is the same set ordered deterministically by name so callers that need a stable order (help output, tests) get a reproducible result.
Every layer is parsed strictly (unknown keys, wrong version, malformed YAML, and invalid descriptors are errors). An invalid layer fails the whole load with a clear error and never silently drops descriptors: a typo must not degrade to a partial, surprising set. A missing user file is not an error (an absent layer is an empty layer); only a present-but-unreadable or present-but-invalid file fails. A malformed shipped default fails the load: it is embedded at build time, so a bad default is a programming error, not a runtime condition.
func ParseCaptureDescriptor ¶
func ParseCaptureDescriptor(data []byte) (CaptureDescriptor, error)
ParseCaptureDescriptor strictly decodes a single capture-descriptor document and validates it. Decoding is strict: an unknown YAML key is an error rather than a silently ignored field, so a typo in a descriptor fails fast instead of shipping an acquisition flow that does nothing. Malformed YAML, a wrong or missing version, and any field that fails CaptureDescriptor.Validate are all errors.
ParseCaptureDescriptor never reads secret bytes; a descriptor carries only non-secret acquisition knowledge.
func (*CaptureDescriptor) Apply ¶
func (d *CaptureDescriptor) Apply(drv *Driver, image string, store StoreFunc)
Apply maps the descriptor's fields onto a *Driver, binding the caller-resolved image and the StoreFunc the descriptor never owns. It is the adapter from declarative data to the executable driver: every tool-specific field (LoginArgs/TokenArgs/ConfigDirEnv/BrowserShim/ StoreAt/Kind/ContainerName/Image) comes from the descriptor, while the transport (Store) and the resolved image come from the caller.
image is the caller-resolved container image. When image is empty the descriptor's own Image is used (normally empty for gh). store is the vault PUT seam; it is required. Apply leaves the Driver's Runner and RuntimeExe untouched so a caller may build the Driver via New (which defaults them) and then Apply the descriptor on top.
func (*CaptureDescriptor) Validate ¶
func (d *CaptureDescriptor) Validate() error
Validate checks that a CaptureDescriptor's required fields are present. It enforces the non-empty name/container_name/store_at/kind and the non-empty login_cmd/token_cmd lists. It does not resolve the image or touch any secret.
type CaptureLoadOptions ¶
type CaptureLoadOptions struct {
// UserPath is the per-user descriptor file (e.g. under ~/.aileron),
// the highest-precedence layer. It overrides built-in descriptors with
// the same name. Empty or absent contributes nothing.
UserPath string
// ExtraDescriptors is the in-memory unit-derived layer applied between
// the built-in defaults and the user layer (built-in < unit-derived <
// user). It carries descriptors projected from a sandbox image's
// devcontainer.metadata CLI units (#1322). A descriptor here overrides a
// built-in of the same name and is overridden by a user descriptor of the
// same name. Nil or empty contributes nothing, so a caller that sets it to
// nil reproduces today's two-layer behavior exactly.
ExtraDescriptors []CaptureDescriptor
}
CaptureLoadOptions selects the user descriptor layer that overrides the built-in defaults. UserPath is optional: an empty path or an absent file contributes no descriptors, so an operator who ships nothing gets exactly the built-in tools.
func DefaultCaptureLoadOptions ¶
func DefaultCaptureLoadOptions() CaptureLoadOptions
DefaultCaptureLoadOptions returns the standard user descriptor layer path. The built-in defaults layer is always embedded; the user path selects the optional override layer.
type Driver ¶
type Driver struct {
// Runner executes runtime commands. Defaults via New to
// sandboxcontainer.DefaultRunner(); tests inject a fake.
Runner sandboxcontainer.Runner
// RuntimeExe is the resolved runtime executable, e.g. "docker".
RuntimeExe string
// Image is the fully-resolved container image to run. Image / base-image
// resolution policy stays in the caller; the driver takes a ready string.
Image string
// ContainerName is the deterministic name for the long-lived container.
ContainerName string
// LoginArgs is the interactive login command run inside the container,
// minus the exec scaffolding — e.g. {"gh","auth","login","--web"}. The
// driver prepends exec/-i/-t/--env tokens and the container name.
LoginArgs []string
// TokenArgs is the token-read command run inside the same container,
// e.g. {"gh","auth","token"}. The driver prepends `exec <name>`.
TokenArgs []string
// ConfigDirEnv, when non-empty, is passed as a single `--env=K=V` token
// (e.g. "GH_CONFIG_DIR=/path") on BOTH the login and token execs so the
// token read sees the same config home the login wrote to. Empty omits it.
ConfigDirEnv string
// BrowserShim, when non-empty, is passed as `--env=BROWSER=<shim>` on the
// login exec only, to turn a missing-browser open into a clean no-op.
// Empty omits it. The non-interactive token read never carries it.
BrowserShim string
// StoreAt is the logical vault key the captured credential is stored
// at (e.g. "user/github"), passed verbatim to Store. The Driver does
// not interpret it: the configured StoreFunc owns the convention. The
// production StoreFunc expands it to the daemon path
// /vault/<StoreAt>/credentials; a test StoreFunc may record it as-is.
StoreAt string
// Kind is the metadata Type stamped on the stored credential.
Kind string
// Store persists the credential. Required.
Store StoreFunc
}
Driver captures a credential by driving an interactive CLI login inside a container and reading the produced token back out. Every tool-specific value is a field; the zero Driver is not usable — Runner, RuntimeExe, Image, ContainerName, LoginArgs, TokenArgs, StoreAt, and Store must be set.
func New ¶
New constructs a Driver with the runtime resolved and the Runner defaulted to the production exec runner, mirroring the github flow's constructor. Image/base-image resolution stays caller-side: pass a fully-resolved image. The returned Driver still needs LoginArgs, TokenArgs, ContainerName, StoreAt, Kind, and Store set by the caller.
func (*Driver) Acquire ¶
Acquire is the full driver entrypoint: it captures the token, guards the empty-after-trim case (no store, ErrEmptyToken), then stamps the configured kind and hands the bytes to Store. Store's error is returned verbatim. Store is never called on a capture failure or an empty token.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry resolves a capture descriptor by name. It is a thin, name-keyed wrapper over the loaded, layer-merged descriptor set. The registry settles acquisition by tool name (e.g. "gh"); there is no per-tool flag — the name is the only selector.
func DefaultRegistry ¶
DefaultRegistry builds a Registry from the embedded built-in defaults plus the standard user descriptor layer path (DefaultUserCapturePath). It is the production constructor.
func NewRegistry ¶
func NewRegistry(opts CaptureLoadOptions) (*Registry, error)
NewRegistry builds a Registry from the merged built-in + user descriptor layers selected by opts. A load error (malformed shipped default, present-but-invalid user file) is surfaced rather than degrading to a partial registry.
func (*Registry) Bind ¶
Bind resolves name, builds a Driver via New (resolving the runtime and defaulting the Runner), applies the descriptor onto it with the caller-resolved image and store seam, and returns the ready Driver. It is the convenience path from a tool name to an executable Driver. An unknown name is an error naming the registered tools.
type StoreFunc ¶
StoreFunc persists the captured credential. It is the seam that keeps the driver transport-agnostic: production wiring supplies a closure over the daemon vault PUT (#1288); tests supply a recording fake.
storeAt is the vault path, kind is the metadata Type stamp, and value is the trimmed credential bytes. The returned error is surfaced verbatim by Acquire — the caller owns any status → message mapping.