Add typed environment secret references
This commit is contained in:
@@ -42,7 +42,8 @@ The current lifecycle is:
|
||||
validate options → parse the selected source → traverse/compose enabled imports
|
||||
→ build schema → check the optional application version → apply a selected profile
|
||||
→ create and clone defaults → strictly decode → recorded application resolution
|
||||
→ semantic validation → classify secrets → snapshot configuration and metadata
|
||||
→ resolve typed secret references → semantic validation → classify secrets
|
||||
→ snapshot configuration and metadata
|
||||
```
|
||||
|
||||
`File`, `Bytes`, and `Reader` each contribute the first base source with deterministic
|
||||
@@ -144,6 +145,50 @@ configuration values. Applications must still keep source labels, resolution
|
||||
reasons, and diagnostic messages value-free because ConfigKit cannot infer and
|
||||
sanitize secrets embedded in arbitrary application prose.
|
||||
|
||||
## Typed secret references
|
||||
|
||||
Credential-bearing fields use the opaque `Secret` type and store an environment
|
||||
variable name in YAML, such as `api_key: OPENROUTER_API_KEY`. `SecretEnv(name)`
|
||||
constructs the same unresolved reference for application defaults or recorded
|
||||
resolution. Names must match `[A-Za-z_][A-Za-z0-9_]*`; they are public
|
||||
configuration and remain visible through `EnvName`, canonical projection, history,
|
||||
and effective output unless the path is separately classified for redaction.
|
||||
|
||||
A non-pointer `Secret` is required. An absent, null, or invalid reference, or an
|
||||
unset or empty environment value, fails with a safe `secret.*` diagnostic at the
|
||||
configuration path. `*Secret` is optional only when nil; every non-nil reference
|
||||
must resolve to a nonempty value. Secret references work in typed structs, maps,
|
||||
pointers, slices, and arrays. A dynamic `any` value does not establish secret
|
||||
semantics and cannot contain `Secret`; applications must use a typed slot.
|
||||
|
||||
ConfigKit resolves only the effective names after recorded application resolution
|
||||
and before semantic validation. Each unique name is read with `os.LookupEnv` once
|
||||
per load. Validation can inspect the credential through the deliberately named
|
||||
`Value()` accessor and distinguish success with `IsResolved()`. Later loads read
|
||||
the environment again, and cloned defaults are rebuilt from names so a previously
|
||||
resolved result cannot retain a stale credential. `Load` never writes to the
|
||||
environment.
|
||||
|
||||
Credential bytes are held only in the per-load lookup state and the trusted returned
|
||||
configuration, and are returned only by `Value`. Secret formatting uses a safe
|
||||
public-name representation, JSON marshals only the name, and projection, metadata,
|
||||
histories, diagnostics, effective output,
|
||||
and resolution snapshots never retain the credential. Validation remains read-only:
|
||||
in addition to ordinary projected-value comparison, ConfigKit checks the private
|
||||
resolved state against this load's lookup result. Applications must keep their
|
||||
validation messages value-free and must not include `Value()` in diagnostics.
|
||||
|
||||
For example, a typed pipeline field is declared as:
|
||||
|
||||
```go
|
||||
type PipelineConfig struct {
|
||||
APIKey configkit.Secret `yaml:"api_key"`
|
||||
}
|
||||
```
|
||||
|
||||
The executable `ExampleSecretEnv` demonstrates loading and inspecting the safe
|
||||
reference state without printing its value.
|
||||
|
||||
## Explicit secrets-directory bootstrap
|
||||
|
||||
`LoadSecretsDir(directory)` is a separate, opt-in startup operation. It is never
|
||||
@@ -410,7 +455,8 @@ it keeps no global mutable descriptor cache. A configuration root must be a stru
|
||||
or string-keyed map. Nested supported shapes are:
|
||||
|
||||
- booleans, strings, signed and unsigned integers, and finite floating-point types;
|
||||
- `time.Duration` and paired `encoding.TextMarshaler`/`TextUnmarshaler` scalars;
|
||||
- `time.Duration`, typed `Secret` references, and paired
|
||||
`encoding.TextMarshaler`/`TextUnmarshaler` scalars;
|
||||
- pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and
|
||||
- recursive type definitions whose eventual application values are acyclic.
|
||||
|
||||
@@ -434,7 +480,8 @@ Schema path traversal uses these same bindings. Explicit key segments address
|
||||
mappings, explicit index segments address sequences, and parsed numeric pointer
|
||||
tokens become indices only in a sequence. Sequence indices cannot have leading
|
||||
zeroes; fixed-array indices are bounds checked. Dynamic `any` and inline-map entries
|
||||
remain schema-valid for application-defined descendants.
|
||||
remain schema-valid for application-defined descendants, but dynamic values cannot
|
||||
introduce typed secret references.
|
||||
|
||||
Descriptors are reused by typed decoding, projection, and cloning so those operations
|
||||
cannot acquire different field bindings.
|
||||
|
||||
@@ -5,7 +5,8 @@ below before making changes. The repository is in early implementation: its publ
|
||||
loader currently covers strict selected sources, explicit path/glob import graphs,
|
||||
composition, selected profile overlays, typed defaults, schema-version checks,
|
||||
recorded application resolution, semantic validation, layered metadata, secret
|
||||
classification, and redacted effective rendering. The
|
||||
classification, typed environment references, explicit secrets bootstrap, and
|
||||
redacted effective rendering. The
|
||||
[component overview](internal/overview.md) owns the implementation inventory.
|
||||
|
||||
## Initial orientation
|
||||
|
||||
@@ -31,14 +31,20 @@ module and package documentation boundary. Its implemented public primitives are
|
||||
intent, private before/after snapshots, and resolution metadata integration; and
|
||||
- post-resolution schema-aware report collection and validator mutation detection in
|
||||
`validation.go`, completing the public callback lifecycle after composed input;
|
||||
- opaque typed environment references and per-load name resolution in
|
||||
`secret_reference.go`, including credential-free projection/cloning and private
|
||||
validator-state auditing;
|
||||
- explicit bounded startup environment bootstrap and safe partial reporting in
|
||||
`secrets.go`;
|
||||
- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`; and
|
||||
- frozen-snapshot redaction and canonical effective output in `render.go`, with the
|
||||
only upstream dumper integration isolated in `internal/yaml/write.go`.
|
||||
|
||||
The [configuration contract](../config.md) owns implemented input, schema, decoding,
|
||||
loading lifecycle, path imports, composition, profile overlay, version checking,
|
||||
recorded resolution, semantic validation, metadata, secret classification, redacted
|
||||
effective rendering, and ownership behavior.
|
||||
recorded resolution, typed secret references/bootstrap, semantic validation,
|
||||
metadata, secret classification, redacted effective rendering, and ownership
|
||||
behavior.
|
||||
|
||||
The source-extraction inventory records the application foundations assigned to
|
||||
future implementation work:
|
||||
|
||||
@@ -1233,7 +1233,39 @@ Acceptance:
|
||||
|
||||
## Stage 17 — Add environment-backed typed secret references
|
||||
|
||||
Status: pending.
|
||||
Status: complete.
|
||||
|
||||
Completion evidence (2026-09-06):
|
||||
|
||||
- Added the opaque `Secret` and `SecretEnv` API in
|
||||
[`secret_reference.go`](../../secret_reference.go), with name/value/resolution
|
||||
accessors, name-only JSON, and credential-free formatting for every `fmt` verb.
|
||||
The zero value remains an absent reference and no public constructor can inject
|
||||
resolved credential state.
|
||||
- Extended the shared descriptor, decode, projection, clone, comparison, resolution,
|
||||
and validation paths with a first-class secret-reference descriptor. References
|
||||
project only their public names, cloned defaults are rebuilt unresolved, dynamic
|
||||
`any` values cannot introduce secret semantics, and existing tag/predicate
|
||||
classification continues to hide additionally sensitive names or derived data.
|
||||
- Inserted deterministic per-load environment resolution after recorded application
|
||||
resolution and before semantic validation. Only effective valid names are read,
|
||||
each unique success/unset/empty outcome is cached for that load, required and
|
||||
optional shapes receive path-specific `secret.*` diagnostics, and `Load` performs
|
||||
no environment writes. Validation compares private reference state with the lookup
|
||||
cache in addition to the ordinary projected mutation audit.
|
||||
- Added focused coverage in
|
||||
[`secret_reference_test.go`](../../secret_reference_test.go) for structs, maps,
|
||||
pointers and sequences; successful and failed lookup deduplication; absent/null/
|
||||
empty/invalid/unset cases; resolver-selected names; resolved-value validation;
|
||||
same-name unresolved validator replacement; later-load environment changes;
|
||||
stale resolved defaults; dynamic values; bootstrap blocked-empty integration; and
|
||||
non-disclosure through fmt, JSON, effective projection, history, and diagnostics.
|
||||
Added executable `ExampleSecretEnv` and updated the
|
||||
[current secret-reference contract](../config.md#typed-secret-references), component
|
||||
inventory, developer guide, and README. Verified `go test ./...`,
|
||||
`go test -race ./...`, `go vet ./...`, `go build ./...`, `go mod tidy -diff`, and
|
||||
`git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required;
|
||||
CLI overrides remain later work.
|
||||
|
||||
Read: [secret reference contract](initial.md#secret-references-and-explicit-bootstrap),
|
||||
shared descriptor/projection rules and lifecycle, Stage 16 report boundary.
|
||||
|
||||
Reference in New Issue
Block a user