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.

View File

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

View File

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

View File

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