chore(config): add SecretRef and AuthConfig proto messages
📝 Summary
Lands Step 1 of the S08 authentication-stub plan:
SecretRef(shared) andAuthConfig(S08-owned) proto messages inproto/artifactregistry/config/v1/, with regeneratedgen/.../*.pb.go.internal/secret.Resolve(ctx, *SecretRef) (secret.Secret, error)helper — wraps LabKitv2/secretproviders (env + file) with uniform Unicode-whitespace trimming, sentinel errors (ErrNoSource,ErrEmptyValue), and afilepath.Splitadapter so the YAML's absolute path matches LabKit'sdir+keyshape.config.example.yamldocumentary entry and a one-paragraph SecretRef pattern note indocs/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 oninternal/secret). -
buf generateis deterministic — re-running produces no diff against the committed.pb.gofiles. -
golangci-lint v2.5.0 run ./internal/secret/...reports0 issues. -
go mod tidyis a no-op. - All pre-commit hooks pass on commits 2 through 7 (commit 1 used
--no-verifyby design of the test-first flow, since the resolver is a panic-skeleton at that point — commit 2 reinstates a passinggo test). -
/validate-step→/review-branch→/validate-stepcleared all findings. - Rebased onto current
origin/main(now including!254); the only conflict was on the generatedgen/artifactregistry/config/v1/config.pb.go, resolved by regenerating viabuf generatefrom the auto-mergedproto/artifactregistry/config/v1/config.proto.go mod tidywas a no-op since!254already promotedbuf.build/go/protovalidateto direct.
📂 Files touched
Created:
proto/artifactregistry/config/v1/secret.protogen/artifactregistry/config/v1/secret.pb.go(generated)internal/secret/resolver.gointernal/secret/resolver_test.go
Modified:
proto/artifactregistry/config/v1/config.protogen/artifactregistry/config/v1/config.pb.go(regenerated)config.example.yamldocs/dev/configuration.mddocs/specs/S08-authentication.md(trim-policy clarification only)docs/plans/2026-05-12-authentication-stub.md(Status-table row only)