[Maintainer] Upgrade to v0.12.0
Current version: 0.10.0
Target version: 0.12.0
⚠️ This is a breaking release. Manual review is recommended.
Upgrade Notes
Highlights
New opt-in OAuth2 client_credentials client for service-to-service auth, plus a matching integration in the fleet-registry emitter. Caretaker can now attach a bearer JWT to outbound heartbeats alongside (or instead of) the existing HMAC signature path.
OAuth2 client (src/caretaker/auth/oauth_client.py)
OAuth2ClientCredentials posts to the token endpoint using client_secret_basic, caches the returned JWT in-process with a 30-second skew against the server-reported expires_in, and coalesces concurrent callers through an asyncio.Lock so a burst of requests triggers a single refresh.
build_client_from_env() reads OAUTH2_CLIENT_ID / OAUTH2_CLIENT_SECRET / OAUTH2_TOKEN_URL (and optional OAUTH2_SCOPE) and returns None when any required var is unset — callers fall through to their unauthenticated path so existing installs stay byte-identical.
- Failures surface as
OAuth2TokenError; the module never retries silently.
Fleet emitter integration
FleetRegistryConfig.oauth2 → new OAuth2ClientConfig block (off by default). When enabled, emit_heartbeat decorates the heartbeat POST with Authorization: Bearer <jwt>.
- Module-level client cache keyed on
(client_id, client_secret, token_url, scope_env, scope, timeout) keeps the JWT cache warm across heartbeats and invalidates automatically on rotation.
- Fail-open contract preserved: an auth-server outage logs a
WARNING and the heartbeat still goes out (unauthenticated) rather than breaking the run loop.
Configuration
fleet_registry:
enabled: true
endpoint: https://fleet.example/api/fleet/heartbeat
oauth2:
enabled: true
client_id_env: OAUTH2_CLIENT_ID
client_secret_env: OAUTH2_CLIENT_SECRET
token_url_env: OAUTH2_TOKEN_URL
scope_env: OAUTH2_SCOPE
default_scope: ""
timeout_seconds: 10.0
Consumer rollout
Five caretaker-topic consumer repos were provisioned against the shared roauth2.cat-herding.net authorization server (client_credentials, scope read write):
audio_engineer, python_dsa, kubernetes-apply-vscode, flashcards, Example-React-AI-Chat-App
Each carries per-repo OAUTH2_CLIENT_ID / OAUTH2_CLIENT_SECRET secrets plus OAUTH2_TOKEN_URL / OAUTH2_ISSUER_URL variables. The caretaker side remains opt-in; flip fleet_registry.oauth2.enabled to start using them.
Compatibility
- No breaking changes. Default behaviour unchanged — OAuth2 only engages when both the config block and the env vars are populated.
min_compatible: 0.10.0.
Tests
12 new unit tests for OAuth2ClientCredentials (cache hit/miss, concurrent coalescing, expiry clamping, transport errors, authorization header shape) plus 2 new emitter tests covering the bearer-header wiring and fail-open behaviour on token-fetch failure.
🤖 Generated with Claude Code
📋 Full Changelog
@copilot Please apply this upgrade.
See .github/agents/maintainer-upgrade.md for instructions.
FROM: 0.10.0
TO: 0.12.0
BREAKING: True
Steps:
- Update version pins in
pyproject.toml / requirements.txt
- Update any workflow references
- Run tests to verify compatibility
- Update the version in config if applicable
Acceptance criteria:
[Maintainer] Upgrade to v0.12.0
Current version:
0.10.0Target version:
0.12.0Upgrade Notes
Highlights
New opt-in OAuth2
client_credentialsclient for service-to-service auth, plus a matching integration in the fleet-registry emitter. Caretaker can now attach a bearer JWT to outbound heartbeats alongside (or instead of) the existing HMAC signature path.OAuth2 client (
src/caretaker/auth/oauth_client.py)OAuth2ClientCredentialsposts to the token endpoint usingclient_secret_basic, caches the returned JWT in-process with a 30-second skew against the server-reportedexpires_in, and coalesces concurrent callers through anasyncio.Lockso a burst of requests triggers a single refresh.build_client_from_env()readsOAUTH2_CLIENT_ID/OAUTH2_CLIENT_SECRET/OAUTH2_TOKEN_URL(and optionalOAUTH2_SCOPE) and returnsNonewhen any required var is unset — callers fall through to their unauthenticated path so existing installs stay byte-identical.OAuth2TokenError; the module never retries silently.Fleet emitter integration
FleetRegistryConfig.oauth2→ newOAuth2ClientConfigblock (off by default). When enabled,emit_heartbeatdecorates the heartbeat POST withAuthorization: Bearer <jwt>.(client_id, client_secret, token_url, scope_env, scope, timeout)keeps the JWT cache warm across heartbeats and invalidates automatically on rotation.WARNINGand the heartbeat still goes out (unauthenticated) rather than breaking the run loop.Configuration
Consumer rollout
Five
caretaker-topic consumer repos were provisioned against the sharedroauth2.cat-herding.netauthorization server (client_credentials, scoperead write):audio_engineer,python_dsa,kubernetes-apply-vscode,flashcards,Example-React-AI-Chat-AppEach carries per-repo
OAUTH2_CLIENT_ID/OAUTH2_CLIENT_SECRETsecrets plusOAUTH2_TOKEN_URL/OAUTH2_ISSUER_URLvariables. The caretaker side remains opt-in; flipfleet_registry.oauth2.enabledto start using them.Compatibility
min_compatible:0.10.0.Tests
12 new unit tests for
OAuth2ClientCredentials(cache hit/miss, concurrent coalescing, expiry clamping, transport errors, authorization header shape) plus 2 new emitter tests covering the bearer-header wiring and fail-open behaviour on token-fetch failure.🤖 Generated with Claude Code
📋 Full Changelog
@copilot Please apply this upgrade.
See
.github/agents/maintainer-upgrade.mdfor instructions.FROM: 0.10.0
TO: 0.12.0
BREAKING: True
Steps:
pyproject.toml/requirements.txtAcceptance criteria: