Skip to content

Commit 6b328db

Browse files
feat: use dedicated fragments for AGW token generation (#137)
Co-authored-by: Max Heidinger <max.heidinger@sap.com>
1 parent 61b7e11 commit 6b328db

21 files changed

Lines changed: 1194 additions & 243 deletions

.env_integration_tests.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ CLOUD_SDK_CFG_SDM_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-c
2525

2626
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL=https://your-agent-memory-api-url-here
2727
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret"}'
28+
29+
APPFND_CONHOS_LANDSCAPE=your-landscape-here
30+
TENANT_SUBDOMAIN=your-tenant-subdomain-here
31+
AGW_USER_TOKEN=your-user-jwt-here

docs/INTEGRATION_TESTS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ CLOUD_SDK_CFG_DATA_ANONYMIZATION_DEFAULT_DESTINATION_NAME=your-client-certificat
9696

9797
The destination must be configured with `ClientCertificateAuthentication` and reference a certificate bundle containing the client certificate and private key.
9898

99+
### Agent Gateway Integration Tests
100+
101+
Agent Gateway integration tests use the LoB agent flow via the Destination Service. Configure the following variables in `.env_integration_tests`:
102+
103+
```bash
104+
# Destination Service (required by the LoB agent flow)
105+
CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTID=your-destination-client-id-here
106+
CLOUD_SDK_CFG_DESTINATION_DEFAULT_CLIENTSECRET=your-destination-client-secret-here
107+
CLOUD_SDK_CFG_DESTINATION_DEFAULT_URL=https://your-destination-auth-url-here
108+
CLOUD_SDK_CFG_DESTINATION_DEFAULT_URI=https://your-destination-configuration-uri-here
109+
CLOUD_SDK_CFG_DESTINATION_DEFAULT_IDENTITYZONE=your-identity-zone-here
110+
111+
# Landscape suffix used to resolve the IAS destination name
112+
APPFND_CONHOS_LANDSCAPE=your-landscape-here
113+
114+
# Tenant subdomain for multi-tenant lookup
115+
TENANT_SUBDOMAIN=your-tenant-subdomain-here
116+
117+
# User JWT for token exchange scenarios (get_user_auth)
118+
# If not set, user auth scenarios are automatically skipped
119+
AGW_USER_TOKEN=your-user-jwt-here
120+
```
121+
99122
## Running Integration Tests
100123

101124
```bash
@@ -108,6 +131,7 @@ uv run pytest tests/core/integration/data_anonymization -v
108131
uv run pytest tests/objectstore/integration/ -v
109132
uv run pytest tests/destination/integration/ -v
110133
uv run pytest tests/agent_memory/integration/ -v
134+
uv run pytest tests/agentgateway/integration/ -v
111135
```
112136

113137
### BDD Scenarios

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.21.1"
3+
version = "0.22.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/agentgateway/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
]
5353
"""
5454

55-
from sap_cloud_sdk.agentgateway._models import MCPTool
55+
from sap_cloud_sdk.agentgateway._models import AuthResult, MCPTool
5656
from sap_cloud_sdk.agentgateway.config import ClientConfig
5757
from sap_cloud_sdk.agentgateway.agw_client import create_client, AgentGatewayClient
5858
from sap_cloud_sdk.agentgateway.exceptions import (
@@ -69,6 +69,7 @@
6969
# Configuration
7070
"ClientConfig",
7171
# Data models
72+
"AuthResult",
7273
"MCPTool",
7374
# Exceptions
7475
"AgentGatewaySDKError",

src/sap_cloud_sdk/agentgateway/_customer.py

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
- Tool invocation: mTLS + jwt-bearer grant → user-scoped token (principal propagation)
99
"""
1010

11-
import asyncio
1211
import json
1312
import logging
1413
import os
@@ -432,17 +431,18 @@ async def _list_server_tools(
432431

433432
async def get_mcp_tools_customer(
434433
credentials: CustomerCredentials,
434+
system_token: str,
435435
timeout: float,
436-
app_tid: str | None = None,
437436
) -> list[MCPTool]:
438437
"""List all MCP tools from servers defined in credentials.
439438
440439
Iterates over all integrationDependencies in the credentials file and
441-
discovers tools from each MCP server using mTLS client credentials.
440+
discovers tools from each MCP server using a pre-fetched system token.
442441
443442
Args:
444443
credentials: Customer credentials with integrationDependencies.
445-
app_tid: BTP Application Tenant ID of subscriber (optional).
444+
system_token: Pre-fetched raw system token for authentication.
445+
timeout: HTTP timeout in seconds for MCP server calls.
446446
447447
Returns:
448448
List of MCPTool objects from all servers.
@@ -459,12 +459,6 @@ async def get_mcp_tools_customer(
459459

460460
logger.info("Discovering tools from %d MCP server(s)", len(dependencies))
461461

462-
# Get system token for discovery
463-
loop = asyncio.get_running_loop()
464-
system_token = await loop.run_in_executor(
465-
None, get_system_token_mtls, credentials, timeout, app_tid
466-
)
467-
468462
tools: list[MCPTool] = []
469463

470464
for dep in dependencies:
@@ -490,53 +484,30 @@ async def get_mcp_tools_customer(
490484

491485

492486
async def call_mcp_tool_customer(
493-
credentials: CustomerCredentials,
494487
tool: MCPTool,
495-
user_token: str | None,
488+
auth_token: str,
496489
timeout: float,
497-
app_tid: str | None = None,
498490
**kwargs,
499491
) -> str:
500492
"""Invoke an MCP tool using customer flow.
501493
502-
If user_token is provided, exchanges it for an AGW-scoped token to preserve
503-
user identity for principal propagation. Otherwise, falls back to system token.
494+
Uses a pre-fetched token (either user-scoped or system-scoped) for
495+
authentication against the MCP server.
504496
505497
Args:
506-
credentials: Customer credentials.
507498
tool: MCPTool to invoke.
508-
user_token: User's JWT token for principal propagation (optional).
509-
If None, system token is used instead (no principal propagation).
510-
app_tid: BTP Application Tenant ID of subscriber (optional).
499+
auth_token: Pre-fetched raw access token for authentication.
500+
timeout: HTTP timeout in seconds for the MCP server call.
511501
**kwargs: Tool input parameters.
512502
513503
Returns:
514504
Tool execution result as string.
515505
"""
516506
logger.info("Calling tool '%s' on server '%s'", tool.name, tool.server_name)
517507

518-
loop = asyncio.get_running_loop()
519-
520-
if user_token:
521-
# Exchange user token for AGW-scoped token (with principal propagation)
522-
agw_token = await loop.run_in_executor(
523-
None, exchange_user_token, credentials, user_token, timeout, app_tid
524-
)
525-
else:
526-
# TODO: IBD workaround - use system token when user_token is not available.
527-
# This bypasses principal propagation. Remove this fallback once IBD
528-
# supports proper user token flow.
529-
logger.warning(
530-
"No user_token provided - using system token for tool invocation. "
531-
"Principal propagation will NOT work."
532-
)
533-
agw_token = await loop.run_in_executor(
534-
None, get_system_token_mtls, credentials, timeout, app_tid
535-
)
536-
537508
async with httpx.AsyncClient(
538509
headers={
539-
"Authorization": f"Bearer {agw_token}",
510+
"Authorization": f"Bearer {auth_token}",
540511
"x-correlation-id": str(uuid.uuid4()),
541512
},
542513
timeout=timeout,

0 commit comments

Comments
 (0)