ADR 0004: Secrets

Status: accepted, 2026-10-06

Context

The product promise is that the same engine and the same step file run on a laptop and in CI. Secrets are the one thing that legitimately differs per environment, and issue #5 asks where their values come from. A secret is a value the file refers to by name and each environment resolves; it is never a plain string that can be logged, interpolated, or handed to an agent.

ADR 0001 already decided the language half: secrets.<name> in .ks is typed Secret. This ADR builds on that type and does not repeat 0001's type rules; it decides the other half — where the values come from and the rules around resolving them.

Three options were considered:

Decision

Option A. A SecretRef (crates/core/src/ports/secrets.rs) is a resolver and a key; it displays as resolver:key, and a reference is not secret — it appears in diagnostics and logs.

Capabilities::secret_resolvers. The StateStore named in the redaction rule is likewise a decided port (ADR 0005) with no code yet; that rule binds it once it lands. The decision stands that check warns when a step needs a secret the current context cannot resolve: SecretsError::NotAvailableInContext already maps to CheckSeverity::Warning (crates/core/src/ports/secrets.rs), but the checker never calls check_severity — today check --secrets lists references statically only. The follow-up work this ADR unblocks — calling the funnel and wrapping the gate in a run, enforcing ci-only with its check diagnostic, calling check_severity from the checker, the two resolvers, and filling in the run context's resolvers — is not attributed to specific issues here, only the intent. The issues it unblocks are #111, #49 and #91.

Consequences