feat(aicore): reactive credential reload and proactive secret watcher - #256
Conversation
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
Production validation — proactive credential reload (
|
| Pod | Image | Threads in PID 1 |
|---|---|---|
Pre-fix (6ad5b0854983) |
0.3.3-20260825... (no watcher call) |
14 |
Post-fix (1858f9060d95) |
0.3.3-20260825200836_fd446f2+1 |
15 |
The extra thread is aicore-secret-watcher (daemon, polls /etc/secrets/appfnd/aicore/aicore-instance/ mtime every 30 s). It will call set_aicore_config() on the next kubelet symlink-swap rotation without requiring a pod restart.
autonomous-documentation-org unit tests: 286 passed, 1 skipped — coverage 90.51%
E2E validation — reactive credential reload (live pod test)Environment: Kyma managed runtime, namespace Test script
Execution outputWhat this proves
|
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
439f8d8 to
ccadb06
Compare
Introduces two security improvements for AI Core credential handling: 1. Transparent TLS mode (AICORE_TRANSPARENT_TLS=true): when active, set_aicore_config() skips writing AICORE_CLIENT_SECRET to os.environ and removes any stale value. The infrastructure sidecar proxy adds the mTLS certificate transparently on the SDK's behalf — no secret material needed in the agent process. Addresses HASI2026203 / SEC-309 (credentials exposed as env vars with excessive scope). 2. Reactive credential reload on AuthenticationError: completion() and acompletion() now intercept litellm.AuthenticationError, re-read credentials from the mounted secret volume, and retry once. Covers client_secret rotation and mTLS certificate rotation (cert-manager updates the volume file; the next failed token refresh triggers the reload) without requiring a pod restart. Relates-to: AFSDK-4306
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
- Add watch_aicore_config() daemon thread that polls secret directory mtime every 30s; on change calls set_aicore_config() proactively before LiteLLM's cached OAuth token expires (avoids 401 entirely) - Add _get_secret_dir_mtime() helper — returns 0.0 on OSError so missing dirs are handled safely - Fix ruff format: add blank line after local imports inside except blocks in completion.py (sync and async paths) - Add test_aicore_watcher.py (10 cases) and test_credential_rotation_flow.py (7 cases) covering watcher unit behavior and the LiteLLM env-update contract
- Remove __wrapped__ introspection in test_credential_rotation_flow that caused ty call-non-callable error; watcher call is already verified via reloaded.wait() - Fix trailing blank lines in test_aicore.py (end-of-file-fixer) - Bump version 0.38.0 → 0.41.0 (new public API: watch_aicore_config)
ccadb06 to
10bfdc8
Compare
… agents
Agents using ChatLiteLLM (LangGraph) call litellm.completion directly,
bypassing the SDK completion() wrapper and its reactive 401 handler.
patch_litellm_for_credential_rotation() wraps litellm.completion/acompletion
at the module level so ALL callers get transparent credential reload on
AuthenticationError — including ChatLiteLLM — without any code changes to
the agent's LLM call patterns.
Recommended startup pattern for LangGraph agents:
set_aicore_config()
patch_litellm_for_credential_rotation() # reactive reload for ChatLiteLLM
watch_aicore_config() # proactive reload on rotation
Adds test_langgraph_compat.py (15 tests) covering: env sharing across callers,
reactive path scope documentation, watcher sufficiency, patch behaviour,
idempotency, and full startup pattern end-to-end.
Addresses review comment from thiagob on PR #256 re: LangGraph template compat.
…tellm_for_credential_rotation Address Betina's review comments on PR #256: both public API functions now emit telemetry via @record_metrics, consistent with set_aicore_config and all other SDK public functions. Adds AICORE_WATCH_CONFIG and AICORE_PATCH_LITELLM to the Operation enum.
Documents watch_aicore_config, patch_litellm_for_credential_rotation, and the recommended startup pattern for LangGraph/ChatLiteLLM agents.
…TCH_CONFIG and AICORE_PATCH_LITELLM
Replace the startup-only @record_metrics on watch_aicore_config with two operation-level metrics that fire on every actual reload event: - AICORE_PROACTIVE_RELOAD: emitted each time the watcher detects a secret volume change and calls set_aicore_config proactively - AICORE_REACTIVE_RELOAD: emitted each time a 401 triggers a credential reload in completion(), acompletion(), or the litellm patch Adds private _reload_proactive / _reload_reactive helpers decorated with the new operations. Fixes user-guide double-call appearance in watcher example. Updates operation count test to 161.
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
…A tests Add @record_metrics to _configure_proxy_mode and _configure_destination_mode (Operations.AICORE_PROXY_MODE / AICORE_DESTINATION_MODE) — fills the same telemetry gap Jean flagged on PR #256 for the two new routing mode helpers. Add 5 unit tests in TestProxyModeWithLangGraph covering: - patch_litellm_for_credential_rotation + proxy mode: 401 reload preserves AICORE_PROXY_URL and does not inject AICORE_CLIENT_SECRET - watcher reload re-enters proxy mode, litellm.api_base stays set - A2A / ChatLiteLLM direct litellm.completion path via proxy gets reactive reload after patching - destination mode: successive set_aicore_config() calls re-fetch credentials from Destination Service (mirrors AFSDK-4306 Val 3, auto-doc-dev-eu12) - proxy mode never writes AICORE_CLIENT_SECRET to env, even after reload Update test_operation.py count 161 → 163 (9 aicore ops).
…back) PY-TEL-07: module-level public functions in SDK packages should have @record_metrics when sibling functions are already instrumented. Catches the exact pattern Betina flagged on PR #256 (watch_aicore_config and patch_litellm_for_credential_rotation missing @record_metrics). PY-TEL-08: when new public SDK functions are added without a corresponding change to operation.py, emit FLAG suggesting an Operation constant. Implements Jean's suggestion from PR #256 for richer telemetry granularity. Also extends client_files detection to all src/sap_cloud_sdk/**/*.py (not just *Client.py) for broader PY-TEL-07 coverage. 5 new bats tests: FP-TEL-01 through FP-TEL-05. All 40 tests green.
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
…A tests Add @record_metrics to _configure_proxy_mode and _configure_destination_mode (Operations.AICORE_PROXY_MODE / AICORE_DESTINATION_MODE) — fills the same telemetry gap Jean flagged on PR #256 for the two new routing mode helpers. Add 5 unit tests in TestProxyModeWithLangGraph covering: - patch_litellm_for_credential_rotation + proxy mode: 401 reload preserves AICORE_PROXY_URL and does not inject AICORE_CLIENT_SECRET - watcher reload re-enters proxy mode, litellm.api_base stays set - A2A / ChatLiteLLM direct litellm.completion path via proxy gets reactive reload after patching - destination mode: successive set_aicore_config() calls re-fetch credentials from Destination Service (mirrors AFSDK-4306 Val 3, auto-doc-dev-eu12) - proxy mode never writes AICORE_CLIENT_SECRET to env, even after reload Update test_operation.py count 161 → 163 (9 aicore ops).
…ature Address reviewer feedback on PR #256: 1. Remove reload_aicore_credentials() wrapper — inline set_aicore_config() directly in the except AuthenticationError blocks. The wrapper added a named function for a single call; inlining is simpler and clearer. 2. Remove transparent TLS feature (AICORE_TRANSPARENT_TLS env var, _is_transparent_tls(), conditional client_secret handling in set_aicore_config()). This feature is blocked on an upstream LiteLLM PR and is not needed for the credential rotation fix. Nicole flagged that it belongs in a future secrets-resolver refactor. Behavior unchanged: AuthenticationError still triggers set_aicore_config() + retry, completely transparent to callers.
…A tests Add @record_metrics to _configure_proxy_mode and _configure_destination_mode (Operations.AICORE_PROXY_MODE / AICORE_DESTINATION_MODE) — fills the same telemetry gap Jean flagged on PR #256 for the two new routing mode helpers. Add 5 unit tests in TestProxyModeWithLangGraph covering: - patch_litellm_for_credential_rotation + proxy mode: 401 reload preserves AICORE_PROXY_URL and does not inject AICORE_CLIENT_SECRET - watcher reload re-enters proxy mode, litellm.api_base stays set - A2A / ChatLiteLLM direct litellm.completion path via proxy gets reactive reload after patching - destination mode: successive set_aicore_config() calls re-fetch credentials from Destination Service (mirrors AFSDK-4306 Val 3, auto-doc-dev-eu12) - proxy mode never writes AICORE_CLIENT_SECRET to env, even after reload Update test_operation.py count 161 → 163 (9 aicore ops).

Description
Addresses credential rotation failures in long-running agent pods.
When BTP rotates a service binding, kubelet updates the mounted secret volume within ~2 minutes. However, agent pods are never restarted — LiteLLM's cached OAuth token (up to 12h lifetime) eventually expires and the refresh attempt fails because the process still holds the old
client_secret. This PR makes credential rotation transparent to agents without requiring pod restarts.1. Reactive credential reload on
AuthenticationErrorcompletion()andacompletion()interceptlitellm.AuthenticationError, reload credentials from the mounted secret volume viaset_aicore_config(), and retry the call once. If the retry also fails, the error propagates normally.2. Proactive secret watcher (
watch_aicore_config)A daemon thread polls the secret directory mtime every 60 seconds. On change (kubelet performs an atomic symlink swap on rotation), calls
set_aicore_config()proactively — before the cached OAuth token expires. Avoids the 401 entirely rather than recovering from it.Typical usage:
3. LangGraph / ChatLiteLLM compatibility (
patch_litellm_for_credential_rotation)LangGraph agents use
ChatLiteLLMwhich callslitellm.completiondirectly, bypassing ourcompletion()wrapper.patch_litellm_for_credential_rotation()wrapslitellm.completionandlitellm.acompletionat the module level so all callers get transparent reload on 401 — includingChatLiteLLM.Related Issues
Type of Change
How to Test
python -m pytest tests/aicore/unit/ -v # 111 passedKey test files:
tests/aicore/unit/test_aicore_watcher.py— proactive watcher (8 tests)tests/aicore/unit/test_credential_rotation_flow.py— env update contract (6 tests)tests/aicore/unit/test_completion.py— reactive reload (sync + async)tests/aicore/unit/test_langgraph_compat.py— LangGraph / ChatLiteLLM compatibility (15 tests)Checklist
Breaking Changes
None. All changes are additive:
watch_aicore_config()— new public function, opt-inpatch_litellm_for_credential_rotation()— new public function, opt-in, idempotentAuthenticationErrorincompletion()— same exception type propagates if retry also fails; no contract change