chore(config): add SecretRef and AuthConfig proto messages

📝 Summary

Lands Step 1 of the S08 authentication-stub plan:

  • SecretRef (shared) and AuthConfig (S08-owned) proto messages in proto/artifactregistry/config/v1/, with regenerated gen/.../*.pb.go.
  • internal/secret.Resolve(ctx, *SecretRef) (secret.Secret, error) helper — wraps LabKit v2/secret providers (env + file) with uniform Unicode-whitespace trimming, sentinel errors (ErrNoSource, ErrEmptyValue), and a filepath.Split adapter so the YAML's absolute path matches LabKit's dir+key shape.
  • config.example.yaml documentary entry and a one-paragraph SecretRef pattern note in docs/dev/configuration.md.
  • A small spec clarification in docs/specs/S08-authentication.md (trim policy is Unicode whitespace, not ASCII-only) that came out of the validation pass.

Spec: S08 Authentication (stub). The plan's Step 1 entry has the full file list.

This step is a foundation for both S08 and S04 — once merged, S04 adopts SecretRef for its password field in a follow-up MR.

📏 MR size

Category Files Lines Share Review burden
Generated (gen/**, from buf generate) 2 +286 / -39 38% Low — eyeball for "deterministic regenerate", not line-by-line
Hand-written Go production (internal/secret/resolver.go) 1 +95 13% High — core implementation
Hand-written Go tests (internal/secret/resolver_test.go) 1 +297 40% High — 9 cases, table-driven
Proto source (proto/**) 2 +43 6% Medium
Prose & config (yaml, dev doc, spec clarification, plan status row) 4 +21 / -2 3% Low
Total 10 +742 / -41 100%

Headline: the diff totals 742 added lines, but ~38% of that is .pb.go regeneration. The review-meaningful surface is the remaining 456 lines, of which 297 are tests and 95 are production Go (a 3.1:1 test-to-production ratio).

📦 Commits in this MR

Order Commit Purpose
1 test(config): SecretRef and AuthConfig proto messages plus resolver tests Test floor — 9 cases (7 SecretRef resolution paths + 2 protovalidate oneof violations). Includes the .proto and regenerated .pb.go because two of the assertions exercise generated-type behavior.
2 chore(config): implement SecretRef resolver for bootstrap-token loading Resolver implementation against the test floor; config.example.yaml and docs/dev/configuration.md updates.
3 refactor(config): simplify resolver tests per code-simplifier pass Explicit wantAnyErr field, switch-based outcome dispatch, dropped a thin interface. Form-only.
4 docs(s08): clarify trim policy is Unicode whitespace Validation-pass follow-up: spec now reads "whitespace (via Go's strings.TrimSpace, which matches Unicode whitespace)" instead of "ASCII whitespace".
5 docs(secret): align resolver doc comments with implementation Validation-pass follow-up: doc comments match the implementation contract.
6 test(secret): cover env-source empty-value branches Validation-pass follow-up: tightens the env-source empty/whitespace coverage symmetric with file-source.
7 docs(s08): record MR for Step 1 in plan status table Single-line status-row update per the agentic-development workflow.

Spec coverage

Spec: docs/specs/S08-authentication.md. Other rows are out-of-scope for this step and owned by sibling steps in the same plan.

Acceptance criteria

# Criterion Tests
AC-1 Dispatch table coverage Not in scope for Step 1. Owned by Step 3 (dispatch middleware).
AC-2 /v2/auth/token handler Not in scope for Step 1. Owned by Step 4.
AC-3 Constant-time comparison Not in scope for Step 1. Owned by Step 5 (bootstrap validator).
AC-4 Build-tag fence Not in scope for Step 1. Owned by Step 5.
AC-5 Configuration loading: env var unset, file path missing, file empty, file whitespace, trailing-newline trim, both set, neither set TestResolveEnv/env_source_set_returns_trimmed_value, TestResolveEnv/env_source_unset_returns_error_naming_the_field, TestResolveFile/file_source_with_trailing_newline_returns_trimmed_value, TestResolveFile/file_source_path_missing_returns_error, TestResolveFile/file_source_with_empty_contents_returns_empty-value_error, TestResolveFile/file_source_with_whitespace-only_contents_returns_empty-value_error, TestResolveFile/neither_source_set_returns_no-source_error, TestSecretRefOneofViolations/both_env_and_file_set_is_rejected_at_config_decode, TestSecretRefOneofViolations/neither_set_is_a_protovalidate_oneof.required_violation

Error cases

# Condition Tests
E-1 Missing, empty, malformed, or mismatched credential on a known prefix → 401 Not in scope for Step 1. Owned by Step 3 (middleware) and Step 4.
E-2 Unknown path prefix → 404 with S01 envelope Not in scope for Step 1. Owned by Step 3 (middleware).

Security considerations

# Concern Tests
S-1 Stub has no security model; build-tag fence is the entire security argument Not in scope for Step 1. Owned by Step 5 (build-tag fence + CI job).

🔍 Resolved spec ambiguity

The spec's Acceptance Criteria item 5 and the plan's Step 1 Files: list both ask for a "both env and file set" startup-failure / oneof-violation test. Empirically, protovalidate's oneof.required rule fires only when no oneof case is set: a *SecretRef with both fields set via proto.Unmarshal or protoreflect.Set resolves to a single case, so protovalidate sees only one. The "both set" violation is caught one layer up by protojson / protoyaml at config decode — which is exactly what the spec describes as a "startup failure" in the resolver-behavior section.

Chosen reading. The "neither set" case asserts on protovalidate.Validate; the "both set" case asserts on protojson.Unmarshal. Both layers exercise the configuration-loading pipeline that turns a YAML-side "both set" document into a startup failure, so AC-5 is covered.

🤝 Relationship to !254 (S01 Step 1)

!254 (chore(config): ServerConfig proto + YAML loader) merged first and is this MR's base. The rebase reconciled the two predicted touchpoints: both proto edits now coexist in the Config body (ServerConfig server = 2, DatabaseConfig database = 3, AuthConfig auth = 4), and the generated gen/.../config.pb.go was regenerated via buf generate to match the merged proto. !254 already promoted buf.build/go/protovalidate to a direct require, so this MR no longer carries that go.mod line. Otherwise file ownership is disjoint: !254 owns internal/config/; this MR owns internal/secret/, proto/.../secret.proto, gen/.../secret.pb.go, config.example.yaml, and docs/dev/configuration.md.

One pattern divergence worth a note (not a blocker): !254 ships typed envelope errors (DurationParseError, SizeParseError) designed for structured diagnostics; this MR ships sentinel errors (ErrNoSource, ErrEmptyValue) designed for errors.Is matching. Both are correct for their callers, but there is no shared umbrella — future config-adjacent rows will face a "which pattern do I follow?" call. Worth a follow-up discussion if the team wants alignment.

One subtle observation: the auth.bootstrap_token.env: AR_BOOTSTRAP_TOKEN entry appended to config.example.yaml is documentary at the stub stage. Step 5 of the same plan hardcodes the equivalent *configv1.SecretRef{Env: "AR_BOOTSTRAP_TOKEN"} inside internal/auth/bootstrap rather than reading it via the loader; the YAML stanza shows the post-GATE shape, not the current wiring path.

🧪 Test plan

  • go test ./... passes (cached + fresh on internal/secret).
  • buf generate is deterministic — re-running produces no diff against the committed .pb.go files.
  • golangci-lint v2.5.0 run ./internal/secret/... reports 0 issues.
  • go mod tidy is a no-op.
  • All pre-commit hooks pass on commits 2 through 7 (commit 1 used --no-verify by design of the test-first flow, since the resolver is a panic-skeleton at that point — commit 2 reinstates a passing go test).
  • /validate-step/review-branch/validate-step cleared all findings.
  • Rebased onto current origin/main (now including !254); the only conflict was on the generated gen/artifactregistry/config/v1/config.pb.go, resolved by regenerating via buf generate from the auto-merged proto/artifactregistry/config/v1/config.proto. go mod tidy was a no-op since !254 already promoted buf.build/go/protovalidate to direct.

📂 Files touched

Created:

  • proto/artifactregistry/config/v1/secret.proto
  • gen/artifactregistry/config/v1/secret.pb.go (generated)
  • internal/secret/resolver.go
  • internal/secret/resolver_test.go

Modified:

  • proto/artifactregistry/config/v1/config.proto
  • gen/artifactregistry/config/v1/config.pb.go (regenerated)
  • config.example.yaml
  • docs/dev/configuration.md
  • docs/specs/S08-authentication.md (trim-policy clarification only)
  • docs/plans/2026-05-12-authentication-stub.md (Status-table row only)
Edited by David Fernandez

Merge request reports

Loading
Loading