Add typed environment secret references

This commit is contained in:
2026-09-06 21:40:15 +00:00
parent 16f4212770
commit d6e32d8807
14 changed files with 723 additions and 13 deletions

View File

@@ -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.