Skip to content

feat(mcp-sidecar): implement RSA-OAEP token exchange for dynamic token refresh#1275

Merged
markturansky merged 1 commit intoalphafrom
fix/mcp-sidecar-ambient-token
Apr 10, 2026
Merged

feat(mcp-sidecar): implement RSA-OAEP token exchange for dynamic token refresh#1275
markturansky merged 1 commit intoalphafrom
fix/mcp-sidecar-ambient-token

Conversation

@markturansky
Copy link
Copy Markdown
Contributor

@markturansky markturansky commented Apr 10, 2026

Summary

  • Replace static AMBIENT_TOKEN injection with the same RSA-OAEP token exchange protocol the runner uses — the MCP sidecar now fetches and periodically refreshes its own API token from the CP token server
  • Add tokenexchange package to ambient-mcp with RSA-OAEP encryption, retry/backoff, and background refresh (every 5 min)
  • Make client.Client thread-safe (sync.RWMutex + SetToken()) so tokens can be hot-swapped on refresh
  • Refactor mention.Resolver to use TokenFunc instead of a static string
  • Inject SESSION_ID into MCP sidecar env vars in buildMCPSidecar()
  • Default MCP_API_SERVER_URL to AMBIENT_API_SERVER_URL and remove the hardcoded runtime-int value from the mpp-openshift overlay

Changes

< /dev/null | File | What |
|------|------|
| components/ambient-mcp/tokenexchange/tokenexchange.go | New: RSA-OAEP encrypt + fetch + background refresh |
| components/ambient-mcp/client/client.go | Thread-safe token with SetToken() |
| components/ambient-mcp/main.go | Bootstrap via CP token exchange, fallback to static AMBIENT_TOKEN |
| components/ambient-mcp/mention/resolve.go | TokenFunc instead of static token |
| components/ambient-mcp/tools/sessions.go | Pass c.Token as TokenFunc |
| components/ambient-control-plane/internal/reconciler/kube_reconciler.go | buildMCPSidecar(sessionID), inject SESSION_ID, remove static token |
| components/ambient-control-plane/internal/config/config.go | MCPAPIServerURL defaults to APIServerURL |
| components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml | Remove hardcoded MCP_API_SERVER_URL |

Test plan

  • Unit tests for tokenexchange (18 tests): RSA-OAEP roundtrip, key parsing, URL validation, fetch success/retry/failure, callback, wrong-key rejection
  • Unit tests for client (7 tests): construction, SetToken, concurrent access, bearer header propagation
  • Unit tests for mention (9 tests): TokenFunc lazy eval, token rotation across requests, resolve by UUID/name, error cases
  • go build ./... and go vet ./... pass for both ambient-mcp and ambient-control-plane
  • Deploy to cluster and verify MCP sidecar bootstraps token via exchange and refreshes on rotation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Dynamic token refresh with background updates and optional token-exchange support.
    • Session-aware sidecar injection that propagates session IDs to sidecars.
    • New token-exchange component to bootstrap and refresh MCP tokens.
  • Bug Fixes

    • Token concurrency safety improved via synchronized access.
    • Resolver and mention lookups now use the current token per request.
    • Configuration now derives MCP API server URL from the primary API server when not set.
  • Tests

    • Added comprehensive tests for client, resolver, and token-exchange logic.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9943552-3f13-4ed7-90b1-475c1874a418

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8600a and 90b4ecb.

📒 Files selected for processing (11)
  • components/ambient-control-plane/internal/config/config.go
  • components/ambient-control-plane/internal/reconciler/kube_reconciler.go
  • components/ambient-mcp/client/client.go
  • components/ambient-mcp/client/client_test.go
  • components/ambient-mcp/main.go
  • components/ambient-mcp/mention/resolve.go
  • components/ambient-mcp/mention/resolve_test.go
  • components/ambient-mcp/tokenexchange/tokenexchange.go
  • components/ambient-mcp/tokenexchange/tokenexchange_test.go
  • components/ambient-mcp/tools/sessions.go
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
💤 Files with no reviewable changes (1)
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
✅ Files skipped from review due to trivial changes (3)
  • components/ambient-mcp/tokenexchange/tokenexchange_test.go
  • components/ambient-mcp/mention/resolve_test.go
  • components/ambient-mcp/tokenexchange/tokenexchange.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/ambient-control-plane/internal/config/config.go
  • components/ambient-mcp/main.go
  • components/ambient-mcp/mention/resolve.go

📝 Walkthrough

Walkthrough

Adds a token-exchange subsystem and integrates dynamic token refreshing into the MCP client and mention resolver. MCP sidecar construction became session-aware (injects SESSION_ID). Config loading no longer hardcodes MCP API server URL; manifests removed explicit MCP_API_SERVER_URL. Several tests added/updated for new behavior and concurrency.

Changes

Cohort / File(s) Summary
Token Exchange package
components/ambient-mcp/tokenexchange/tokenexchange.go, components/ambient-mcp/tokenexchange/tokenexchange_test.go
New Exchanger type: RSA-OAEP encrypted session exchange, FetchToken() with retries, cached thread-safe token, OnRefresh(), background refresh loop, Start/Stop, plus comprehensive tests.
MCP client thread-safety & tests
components/ambient-mcp/client/client.go, components/ambient-mcp/client/client_test.go
Added sync.RWMutex to protect client token, introduced SetToken(token string) and lock-protected Token() getter; do() uses Token(); tests exercise concurrency and request header behavior.
Mention resolver token injection & tests
components/ambient-mcp/mention/resolve.go, components/ambient-mcp/mention/resolve_test.go
Replaced static token with TokenFunc func() string; NewResolver accepts tokenFn; requests set Authorization: Bearer using current tokenFn() per call; tests verify per-call token use and error cases.
Main integration & token wiring
components/ambient-mcp/main.go, components/ambient-mcp/tools/sessions.go
main conditionally constructs Exchanger when AMBIENT_CP_TOKEN_URL, AMBIENT_CP_TOKEN_PUBLIC_KEY, and SESSION_ID are set, fetches initial token, registers OnRefresh to call client.SetToken, starts background refresh and defers Stop(); when unset, requires static AMBIENT_TOKEN. PushMessage now passes the token value/field (c.Token) into the resolver constructor.
Sidecar session-awareness
components/ambient-control-plane/internal/reconciler/kube_reconciler.go
buildMCPSidecar signature changed from ambientToken string to sessionID string; ensurePod no longer fetches ambient token and sidecar env always includes SESSION_ID (removed conditional AMBIENT_TOKEN injection).
Config & manifest update
components/ambient-control-plane/internal/config/config.go, components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
MCPAPIServerURL load logic changed to default to empty and then be set to APIServerURL if empty after load; manifest removed explicit MCP_API_SERVER_URL env entry from the deployment.

Sequence Diagram(s)

sequenceDiagram
    participant App as Ambient MCP (main)
    participant Exchanger as Token Exchanger
    participant CP as Control Plane Token Server
    participant Client as MCP Client
    participant API as Ambient API Server

    App->>Exchanger: New(tokenURL, pubKey, sessionID)
    Exchanger->>CP: FetchToken() with RSA-OAEP(encrypted sessionID)
    CP-->>Exchanger: { "token": access_token }
    Exchanger->>Client: OnRefresh(callback) → Client.SetToken(access_token)
    Exchanger->>Exchanger: StartBackgroundRefresh()

    rect rgba(0, 150, 136, 0.5)
        Note over Exchanger,CP: Background refresh loop
        loop every refreshPeriod
            Exchanger->>CP: FetchToken() with encrypted sessionID
            CP-->>Exchanger: { "token": new_token }
            Exchanger->>Client: callback(new_token) → Client.SetToken(new_token)
        end
    end

    rect rgba(66, 133, 244, 0.5)
        Note over Client,API: Authenticated requests use latest token
        Client->>API: GET /... Authorization: Bearer <token>
        API-->>Client: response
    end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Kubernetes Resource Safety ⚠️ Warning Child Pods created via ensurePod() lack ownerReferences and pod-level securityContext, preventing automatic garbage collection and creating inconsistent security posture. Add ownerReferences to pod metadata and pod-level securityContext with runAsNonRoot: true and seccompProfile: RuntimeDefault to match control-plane baseline.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (feat scope description) and accurately summarizes the main change: implementing RSA-OAEP token exchange for dynamic token refresh.
Performance And Algorithmic Complexity ✅ Passed PR introduces no performance regressions: bounded retries with exponential backoff, single goroutine on fixed cadence, fine-grained RWMutex locking, linear object construction, no unbounded caches or listener registrations.
Security And Secret Handling ✅ Passed PR introduces dynamic token exchange and session-ID injection without logging sensitive tokens. Token exchange URL validated, no new API endpoints bypass auth, no injection vectors added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-sidecar-ambient-token
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/mcp-sidecar-ambient-token

Comment @coderabbitai help to get the list of available commands and usage tips.

@markturansky markturansky force-pushed the fix/mcp-sidecar-ambient-token branch from 405beea to 3b8600a Compare April 10, 2026 01:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/ambient-mcp/tokenexchange/tokenexchange.go`:
- Around line 62-64: OnRefresh currently assigns e.onRefresh without
synchronization while FetchToken reads it unsafely; protect the onRefresh field
with the same mutex used elsewhere (e.g., e.mu) by either setting e.onRefresh
inside a lock in OnRefresh or copying the callback under lock in FetchToken
before invoking it. Locate the Exchanger.OnRefresh method and the
Exchanger.FetchToken method and ensure both reads and writes of the onRefresh
field are performed under the same mutex to eliminate the data race.
- Around line 104-123: StartBackgroundRefresh can spawn multiple goroutines and
Stop can panic on double-close of e.stopCh; modify Exchanger to add two
sync.Once fields (e.startOnce, e.stopOnce) and use startOnce.Do in
StartBackgroundRefresh to ensure only one goroutine is created, and use
stopOnce.Do in Stop to close e.stopCh safely; keep existing logic calling
e.FetchToken() and ticker cleanup but ensure stopCh is initialized when
Exchanger is constructed so the Once guards operate correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe92cb9b-c89b-4671-b73e-7991900dd72d

📥 Commits

Reviewing files that changed from the base of the PR and between ad938e2 and 405beea.

📒 Files selected for processing (11)
  • components/ambient-control-plane/internal/config/config.go
  • components/ambient-control-plane/internal/reconciler/kube_reconciler.go
  • components/ambient-mcp/client/client.go
  • components/ambient-mcp/client/client_test.go
  • components/ambient-mcp/main.go
  • components/ambient-mcp/mention/resolve.go
  • components/ambient-mcp/mention/resolve_test.go
  • components/ambient-mcp/tokenexchange/tokenexchange.go
  • components/ambient-mcp/tokenexchange/tokenexchange_test.go
  • components/ambient-mcp/tools/sessions.go
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
💤 Files with no reviewable changes (1)
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml

…n refresh

Replace the static AMBIENT_TOKEN injection with the same RSA-OAEP
token exchange protocol the runner uses, so the MCP sidecar can
fetch and periodically refresh its own API token from the CP token
server.

- Add tokenexchange package: RSA-OAEP encrypt session ID, fetch
  token from CP /token endpoint with retry/backoff, background
  refresh every 5 minutes
- Make client.Client thread-safe (sync.RWMutex) with SetToken()
  for hot-swapping tokens on refresh
- Refactor mention.Resolver to use TokenFunc instead of static
  string so it always reads the current token
- Inject SESSION_ID into MCP sidecar env in buildMCPSidecar()
- Default MCP_API_SERVER_URL to AMBIENT_API_SERVER_URL so the
  sidecar inherits the correct API endpoint automatically
- Remove hardcoded MCP_API_SERVER_URL from mpp-openshift overlay
- Add unit tests for tokenexchange, client, and mention packages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@markturansky markturansky force-pushed the fix/mcp-sidecar-ambient-token branch from 3b8600a to 90b4ecb Compare April 10, 2026 01:59
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
components/ambient-mcp/tokenexchange/tokenexchange.go (2)

62-64: ⚠️ Potential issue | 🔴 Critical

Protect onRefresh with the same mutex (current code races).

Line 63 writes e.onRefresh unsafely, while Lines 88-89 read/invoke it unsafely during concurrent refreshes. This is a real data race.

Proposed fix
 func (e *Exchanger) OnRefresh(fn func(string)) {
+	e.mu.Lock()
+	defer e.mu.Unlock()
 	e.onRefresh = fn
 }
@@
-		e.mu.Lock()
-		e.currentToken = token
-		e.mu.Unlock()
-
-		if e.onRefresh != nil {
-			e.onRefresh(token)
-		}
+		e.mu.Lock()
+		e.currentToken = token
+		callback := e.onRefresh
+		e.mu.Unlock()
+
+		if callback != nil {
+			callback(token)
+		}

As per coding guidelines, "Flag only errors, security risks, or functionality-breaking problems."

Also applies to: 84-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-mcp/tokenexchange/tokenexchange.go` around lines 62 - 64,
The OnRefresh setter races with concurrent readers; protect writes to
e.onRefresh by acquiring the same Exchanger mutex used when invoking it during
refresh (i.e., use the Exchanger's mutex around OnRefresh assignment) and ensure
the refresh path (where e.onRefresh is read/invoked in the refresh routine at
lines ~84-90) also holds that same mutex while accessing e.onRefresh; update the
OnRefresh method to lock/unlock the mutex and keep the existing read/invoke
sites locked with the identical mutex to eliminate the data race.

104-123: ⚠️ Potential issue | 🟠 Major

Guard lifecycle methods: multiple starts and double-stop are unsafe.

Line 105 starts a new goroutine every call, and Line 122 panics if Stop() is called twice (close on closed channel). Add sync.Once guards.

Proposed fix
 type Exchanger struct {
 	tokenURL     string
 	publicKey    *rsa.PublicKey
 	sessionID    string
 	httpClient   *http.Client
 	mu           sync.RWMutex
 	currentToken string
 	onRefresh    func(string)
 	stopCh       chan struct{}
+	startOnce    sync.Once
+	stopOnce     sync.Once
 }
@@
 func (e *Exchanger) StartBackgroundRefresh() {
-	go func() {
+	e.startOnce.Do(func() {
+		go func() {
 		ticker := time.NewTicker(refreshPeriod)
 		defer ticker.Stop()
 		for {
 			select {
 			case <-ticker.C:
 				if _, err := e.FetchToken(); err != nil {
 					fmt.Fprintf(os.Stderr, "background token refresh failed: %v\n", err)
 				}
 			case <-e.stopCh:
 				return
 			}
 		}
-	}()
+		}()
+	})
 }
 
 func (e *Exchanger) Stop() {
-	close(e.stopCh)
+	e.stopOnce.Do(func() {
+		close(e.stopCh)
+	})
 }

As per coding guidelines, "Flag only errors, security risks, or functionality-breaking problems."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-mcp/tokenexchange/tokenexchange.go` around lines 104 -
123, The StartBackgroundRefresh and Stop methods are not guarded and can spawn
multiple goroutines or panic on a double close of stopCh; add sync.Once guards:
introduce startOnce and stopOnce fields on Exchanger, wrap the goroutine startup
in startOnce.Do(...) inside StartBackgroundRefresh to ensure only one background
goroutine is started, and wrap close(e.stopCh) in stopOnce.Do(func(){
close(e.stopCh) }) inside Stop to make closing idempotent; ensure stopCh is
initialized before startOnce.Do is called (e.g., in constructor) so the
goroutine/select uses a valid channel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/ambient-mcp/client/client_test.go`:
- Around line 14-16: TestNew expects trailing slash to be stripped but
client.New currently stores baseURL as-is; update the constructor New(...) in
the client package to normalize the provided baseURL by trimming any trailing
slash before assigning to the baseURL field so BaseURL() returns the normalized
value, and keep BaseURL() behavior unchanged (or alternatively change the test
to expect the raw value) — locate the constructor New, the baseURL field, and
the BaseURL() method to implement the trim logic.

In `@components/ambient-mcp/tokenexchange/tokenexchange.go`:
- Around line 190-193: The token URL validation in tokenexchange.go currently
allows plaintext HTTP by checking parsed.Scheme and permitting "http" or
"https"; change the check to accept only "https" (i.e., require parsed.Scheme ==
"https"), update the error message to reflect that only HTTPS is allowed, and
ensure this logic is applied in the function where parsed.Scheme / scheme is
used for token endpoint validation so non‑TLS endpoints are rejected.

---

Duplicate comments:
In `@components/ambient-mcp/tokenexchange/tokenexchange.go`:
- Around line 62-64: The OnRefresh setter races with concurrent readers; protect
writes to e.onRefresh by acquiring the same Exchanger mutex used when invoking
it during refresh (i.e., use the Exchanger's mutex around OnRefresh assignment)
and ensure the refresh path (where e.onRefresh is read/invoked in the refresh
routine at lines ~84-90) also holds that same mutex while accessing e.onRefresh;
update the OnRefresh method to lock/unlock the mutex and keep the existing
read/invoke sites locked with the identical mutex to eliminate the data race.
- Around line 104-123: The StartBackgroundRefresh and Stop methods are not
guarded and can spawn multiple goroutines or panic on a double close of stopCh;
add sync.Once guards: introduce startOnce and stopOnce fields on Exchanger, wrap
the goroutine startup in startOnce.Do(...) inside StartBackgroundRefresh to
ensure only one background goroutine is started, and wrap close(e.stopCh) in
stopOnce.Do(func(){ close(e.stopCh) }) inside Stop to make closing idempotent;
ensure stopCh is initialized before startOnce.Do is called (e.g., in
constructor) so the goroutine/select uses a valid channel.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3bd4301f-916f-4e8e-8894-aeaed2be2063

📥 Commits

Reviewing files that changed from the base of the PR and between 405beea and 3b8600a.

📒 Files selected for processing (11)
  • components/ambient-control-plane/internal/config/config.go
  • components/ambient-control-plane/internal/reconciler/kube_reconciler.go
  • components/ambient-mcp/client/client.go
  • components/ambient-mcp/client/client_test.go
  • components/ambient-mcp/main.go
  • components/ambient-mcp/mention/resolve.go
  • components/ambient-mcp/mention/resolve_test.go
  • components/ambient-mcp/tokenexchange/tokenexchange.go
  • components/ambient-mcp/tokenexchange/tokenexchange_test.go
  • components/ambient-mcp/tools/sessions.go
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
💤 Files with no reviewable changes (1)
  • components/manifests/overlays/mpp-openshift/ambient-control-plane.yaml
✅ Files skipped from review due to trivial changes (1)
  • components/ambient-mcp/tokenexchange/tokenexchange_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • components/ambient-mcp/tools/sessions.go
  • components/ambient-mcp/client/client.go
  • components/ambient-mcp/main.go
  • components/ambient-control-plane/internal/reconciler/kube_reconciler.go
  • components/ambient-mcp/mention/resolve.go

Comment on lines +190 to +193
scheme := strings.ToLower(parsed.Scheme)
if scheme != "http" && scheme != "https" {
return fmt.Errorf("invalid token URL scheme %q (must be http or https)", scheme)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Token endpoint validation should not accept plaintext http.

Line 191 currently permits http. Since this endpoint returns the API token, accepting non-TLS transport exposes token theft risk in transit.

Proposed fix
 	scheme := strings.ToLower(parsed.Scheme)
-	if scheme != "http" && scheme != "https" {
-		return fmt.Errorf("invalid token URL scheme %q (must be http or https)", scheme)
+	if scheme != "https" {
+		return fmt.Errorf("invalid token URL scheme %q (must be https)", scheme)
 	}

As per coding guidelines, "Flag only errors, security risks, or functionality-breaking problems."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-mcp/tokenexchange/tokenexchange.go` around lines 190 -
193, The token URL validation in tokenexchange.go currently allows plaintext
HTTP by checking parsed.Scheme and permitting "http" or "https"; change the
check to accept only "https" (i.e., require parsed.Scheme == "https"), update
the error message to reflect that only HTTPS is allowed, and ensure this logic
is applied in the function where parsed.Scheme / scheme is used for token
endpoint validation so non‑TLS endpoints are rejected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — this is in-cluster pod-to-pod traffic over Kubernetes service DNS (e.g. http://ambient-control-plane.ns.svc:8080/token). The runner's Python implementation also permits HTTP for the same reason (_grpc_client.py:52-63). Requiring HTTPS would break all in-cluster deployments where TLS termination is handled at the service mesh / ingress layer.

@markturansky markturansky merged commit 23002c1 into alpha Apr 10, 2026
38 checks passed
@markturansky markturansky deleted the fix/mcp-sidecar-ambient-token branch April 10, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant