Decompose Phase 2 (Core Bridge) into 8 dependency-ordered tasks

Phase 2 completes the interface-to-protocol bridge and adds core types
that external crates depend on. The 8 tasks are organized into 5
generations with clear dependencies:

- Gen 1: StreamInterface/MessageInterface trait split (must go first)
- Gen 2: SshSession bridge, RawFraming impl, CredentialProvider (parallel)
- Gen 3: API keys in DynamicConfig (depends on CredentialProvider)
- Gen 4: ListenerConfig HTTP/DNS stubs + axum scaffold
- Gen 5: Review gate before Phase 3

Key design decisions:
- 2.4a/2.4b split: SecretStoreCredentialProvider deferred to Phase 3
- API keys (2.6) must land before axum scaffold (2.7)
- ListenerConfig (2.5) must land before axum scaffold (2.7)
- Gen 2 tasks are parallelizable (separate modules)
This commit is contained in:
2026-06-09 09:33:22 +00:00
parent 8f24fa6c09
commit aafee72f4c
8 changed files with 565 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
---
id: credential-provider-trait
name: Define CredentialProvider trait, CredentialSet enum, and ConfigCredentialProvider implementation
status: pending
depends_on: [stream-interface-message-interface-split]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
Define the `CredentialProvider` trait and `CredentialSet` enum in `alknet_core::credentials`, implementing the outbound authentication abstraction that complements the inbound `IdentityProvider`. This is the "Phase A" / "Phase 2.4a" work — the trait and enum must exist in core before alknet-secret (Phase 3) can wire `SecretStoreCredentialProvider` against them.
Per ADR-036 and research/phase2/credential-provider.md:
- `CredentialProvider` resolves **outbound** credentials: "how does alknet authenticate TO external services?"
- `CredentialSet` is a structured enum of credential types: `ApiKey`, `Basic`, `Bearer`, `S3AccessKey`, `OidcToken`, `Custom`
- `ConfigCredentialProvider` reads API keys and static credentials from `DynamicConfig` — the Phase 2 default (simple, no secret service dependency)
- `SecretStoreCredentialProvider` is a **stub** that returns `None` for all lookups until Phase 3 provides the alknet-secret dependency
- Wire `CredentialProvider` into `OperationEnv`/`OperationContext` so handlers can access credentials
**Relationship to IdentityProvider**: These are opposite-direction abstractions. `IdentityProvider` resolves inbound auth (who is calling alknet). `CredentialProvider` resolves outbound auth (how alknet calls others). Both live at the same architectural layer.
**Relationship to OperationEnv**: Handlers compose through `context.env`. The `OperationEnv` needs access to `CredentialProvider` so that handlers calling external services can resolve credentials. This could be a dedicated field on `OperationContext` or accessible through the `env` — the implementation detail is flexible, but the behavioral contract must match: given a service name, return credentials for that service.
## Acceptance Criteria
- [ ] `CredentialProvider` trait defined in `crates/alknet-core/src/credentials/mod.rs` with `get_credentials(&self, service: &str) -> Option<CredentialSet>` and `refresh_credentials(&self, service: &str) -> Option<CredentialSet>`
- [ ] `CredentialSet` enum defined with variants: `ApiKey { header_name, token }`, `Basic { username, password }`, `Bearer { token }`, `S3AccessKey { access_key, secret_key, session_token }`, `OidcToken { access_token, refresh_token, expires_at }`, `Custom { scheme, params }`
- [ ] `ConfigCredentialProvider` struct implemented — reads credentials from `DynamicConfig.auth` (or a new `DynamicConfig.credentials` section). For Phase 2, this is a simple config-backed lookup returning `CredentialSet::Bearer` or `CredentialSet::ApiKey` entries.
- [ ] `SecretStoreCredentialProvider` struct defined as a stub — `get_credentials()` always returns `None`. Full implementation deferred to Phase 3.
- [ ] `CredentialProvider` wired into `OperationContext` or `OperationEnv` so handlers can access outbound credentials
- [ ] `credentials` module re-exported from `crates/alknet-core/src/lib.rs`
- [ ] Unit test: `ConfigCredentialProvider` returns configured credentials for a service name
- [ ] Unit test: `ConfigCredentialProvider` returns `None` for unknown service names
- [ ] Unit test: `SecretStoreCredentialProvider` stub returns `None` for all service names
- [ ] Unit test: `OperationEnv`/`OperationContext` provides access to `CredentialProvider` from handler context
- [ ] `CredentialSet` derives `Clone`, `Debug`, `serde::Serialize`, `serde::Deserialize`
## References
- docs/architecture/decisions/036-credentialprovider-core-type.md — ADR-036
- docs/research/phase2/credential-provider.md — Full design rationale
- docs/research/integration-plan.md — Phase 2.4
- crates/alknet-core/src/auth/identity.rs — IdentityProvider (opposite direction, same pattern)
- crates/alknet-core/src/call/env.rs — OperationEnv
- crates/alknet-core/src/call/context.rs — OperationContext
## Notes
> This task is "2.4a" — the core types and config-backed implementation. "2.4b" (SecretStoreCredentialProvider backed by SecretProtocol::Decrypt) is deferred to Phase 3 when alknet-secret exists.
> For `ConfigCredentialProvider`, consider whether to add a `[[credentials]]` section to `DynamicConfig` or to reuse a subsection. The simplest Phase 2 approach is a new `credentials: HashMap<String, CredentialSet>` field on `DynamicConfig` that stores static bearer tokens/API keys from config.
> The `OperationEnv`/`OperationContext` wiring can follow either pattern: (a) a `credential_provider: Arc<dyn CredentialProvider>` field on `OperationContext`, or (b) `CredentialProvider` accessible through a registry-style `env.credentials(service)` method. The integration plan says "wire into OperationEnv so handlers can access credentials through context.env" — approach (b) aligns with the OperationEnv composition model. This is an implementation detail to resolve during implementation.
## Summary
> To be filled on completion