Skip to content

Commit 06f7626

Browse files
markturanskyAmbient Code Botclaude
authored
fix(runner): wire CP-fetched OIDC token into get_bot_token() for backend API calls (#1217)
## Summary - After fetching from the CP token endpoint, `_fetch_token_from_cp()` now calls `set_bot_token(token)` to store the OIDC token in a module-level cache in `utils.py` - `get_bot_token()` checks that cache first, so `auth.py` credential fetches to the backend API use the OIDC token instead of an empty string - Adds a regression test that verifies `get_bot_token()` is empty before a CP fetch and returns the token after — confirmed to fail without the fix ## Root cause The CP-fetched OIDC token was stored only in `AmbientGRPCClient._token` (gRPC channel auth). `auth.py`'s `get_bot_token()` had no access to it, so credential token fetches went out unauthenticated → HTTP 401 on every session run. 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added caching for bot tokens fetched from the control plane for improved performance. * Updated token sourcing priority to prefer control-plane tokens over other sources. * **Tests** * Added integration tests for token fetching and caching mechanisms. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b0ed2b8 commit 06f7626

3 files changed

Lines changed: 65 additions & 5 deletions

File tree

components/runners/ambient-runner/ambient_runner/_grpc_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from pathlib import Path
1212
from typing import Optional
1313

14+
import grpc
1415
from cryptography.hazmat.primitives import hashes, serialization
1516
from cryptography.hazmat.primitives.asymmetric import padding
1617

17-
import grpc
18+
from ambient_runner.platform.utils import set_bot_token
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -95,6 +96,7 @@ def _fetch_token_from_cp(cp_token_url: str, public_key_pem: str, session_id: str
9596
if not token:
9697
raise RuntimeError("CP /token response missing 'token' field")
9798
logger.info("[GRPC CLIENT] Fetched fresh API token from CP token endpoint")
99+
set_bot_token(token)
98100
return token
99101
except urllib.error.HTTPError as e:
100102
resp_body = ""

components/runners/ambient-runner/ambient_runner/platform/utils.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,27 @@
2323
# Kubelet automatically refreshes this file when the Secret is updated.
2424
_BOT_TOKEN_FILE = Path("/var/run/secrets/ambient/bot-token")
2525

26+
# In-process cache for the token fetched from the CP token endpoint.
27+
# Set once at startup by _grpc_client.py after a successful CP token fetch.
28+
_cp_fetched_token: str = ""
29+
30+
31+
def set_bot_token(token: str) -> None:
32+
"""Store a token fetched from the CP token endpoint for use by get_bot_token()."""
33+
global _cp_fetched_token
34+
_cp_fetched_token = token.strip()
35+
2636

2737
def get_bot_token() -> str:
28-
"""Return the current BOT_TOKEN, preferring the file mount over env var.
38+
"""Return the current BOT_TOKEN.
2939
30-
The operator mounts the runner-token Secret as a file so kubelet refreshes
31-
it automatically when the token is rotated. Falls back to the BOT_TOKEN
32-
env var for backward-compatibility with local / non-Kubernetes runs.
40+
Priority:
41+
1. Token fetched from CP token endpoint (set via set_bot_token()).
42+
2. File mount at _BOT_TOKEN_FILE (kubelet-refreshed Secret).
43+
3. BOT_TOKEN env var (local / non-Kubernetes fallback).
3344
"""
45+
if _cp_fetched_token:
46+
return _cp_fetched_token
3447
try:
3548
if _BOT_TOKEN_FILE.exists():
3649
return _BOT_TOKEN_FILE.read_text().strip()

components/runners/ambient-runner/tests/test_grpc_client.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,51 @@ def test_raises_on_missing_token_field(self):
200200
)
201201

202202

203+
class TestSetBotTokenIntegration:
204+
def test_get_bot_token_returns_cp_fetched_token_after_successful_fetch(self):
205+
import ambient_runner.platform.utils as utils
206+
utils._cp_fetched_token = ""
207+
208+
_, _, public_pem = generate_keypair()
209+
mock_resp = MagicMock()
210+
mock_resp.read.return_value = json.dumps({"token": "oidc-token-for-api-calls"}).encode()
211+
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
212+
mock_resp.__exit__ = MagicMock(return_value=False)
213+
214+
assert utils.get_bot_token() == "", "get_bot_token() must be empty before any CP fetch"
215+
216+
with patch("urllib.request.urlopen", return_value=mock_resp):
217+
_fetch_token_from_cp("http://cp.svc:8080/token", public_pem, "session-12345678")
218+
219+
assert utils.get_bot_token() == "oidc-token-for-api-calls", (
220+
"get_bot_token() must return the CP-fetched token so backend API credential "
221+
"calls are authenticated — regression for HTTP 401 on credential refresh"
222+
)
223+
utils._cp_fetched_token = ""
224+
225+
def test_fetch_from_cp_calls_set_bot_token(self):
226+
from cryptography.hazmat.primitives.asymmetric import rsa as _rsa
227+
private_key = _rsa.generate_private_key(public_exponent=65537, key_size=2048)
228+
public_pem = private_key.public_key().public_bytes(
229+
encoding=serialization.Encoding.PEM,
230+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
231+
).decode()
232+
233+
mock_resp = MagicMock()
234+
mock_resp.read.return_value = json.dumps({"token": "oidc-api-token-abc"}).encode()
235+
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
236+
mock_resp.__exit__ = MagicMock(return_value=False)
237+
238+
import ambient_runner.platform.utils as utils
239+
utils._cp_fetched_token = ""
240+
241+
with patch("urllib.request.urlopen", return_value=mock_resp):
242+
_fetch_token_from_cp("http://cp.svc:8080/token", public_pem, "session-12345678")
243+
244+
assert utils.get_bot_token() == "oidc-api-token-abc"
245+
utils._cp_fetched_token = ""
246+
247+
203248
class TestFromEnvIntegration:
204249
def test_uses_encrypted_session_id_when_cp_token_url_set(self):
205250
_, _, public_pem = generate_keypair()

0 commit comments

Comments
 (0)