Skip to content

Commit be276db

Browse files
Merge branch 'main' into app-level-instrumentation
# Conflicts: # pyproject.toml
2 parents 73fe7de + cf61b6b commit be276db

20 files changed

Lines changed: 329 additions & 109 deletions

File tree

.claude/skills/review-pr/SKILL.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ compatibility: gh CLI ≥ 2.0, git, GitHub access to SAP/cloud-sdk-python
77

88
# PR Review: SAP Cloud SDK for Python
99

10-
Reviews a PR against 23 criteria across 6 sections. Run from the root of the `cloud-sdk-python` repository.
10+
Reviews a PR against 25 criteria across 6 sections. Run from the root of the `cloud-sdk-python` repository.
1111

1212
---
1313

@@ -172,6 +172,16 @@ New list/query operations: encapsulate pagination params like existing modules (
172172
**D5: Telemetry instrumentation**
173173
New client methods: `@record_metrics(Module.X, Operation.Y)` from `core/telemetry`. New module: constant added to `core/telemetry/module.py` and operations to `core/telemetry/operation.py`. If module is called by other SDK modules: `_telemetry_source: Optional[Module] = None` param present.
174174

175+
**D6: Multi-tenancy support**
176+
Multi-tenancy is a cross-cutting concern for most BTP applications. For new service modules, consider the full scope of multi-tenancy — not just token routing, but also provisioning and tenant isolation:
177+
178+
- **Provisioning:** Does the service require a subscription or onboarding step per tenant? Does the module need to support tenant lifecycle callbacks (subscribe/unsubscribe)?
179+
- **Tenant isolation:** Is data or configuration isolated per tenant at the service level? Does the module enforce or expose tenant boundaries correctly?
180+
- **Auth/routing:** Does the module need to route requests to a subscriber tenant context (e.g., XSUAA subdomain replacement, IAS `app_tid`, or Destination Service routing)?
181+
- **Infrastructure:** Is there any infrastructure work required (e.g., new service binding fields, SPII fragments, Subscription Manager integration)?
182+
183+
This is a **nice-to-have** (⚠️ if absent, not ❌), but must be a conscious decision either way — the `user-guide.md` must document whether support is present, planned, or intentionally out of scope (see E2a).
184+
175185
---
176186

177187
### Section E: Tests & Documentation
@@ -182,6 +192,15 @@ Every changed `src/` file → corresponding change in `tests/`. Unit: `tests/[mo
182192
**E2: Documentation quality**
183193
New modules: `user-guide.md` with overview, quick start, config examples, API examples, troubleshooting. Changed public APIs: docstrings updated (Google/NumPy style: `Args:`, `Returns:`, `Raises:`). Sub-audience features not mixed into the general user guide.
184194

195+
**E2a: Multi-tenancy documentation**
196+
Every new or modified `user-guide.md` must contain a `## Multi-tenancy` section with all four fields:
197+
- **Supported:** Yes / No / Not yet / N/A
198+
- **Authentication:** XSUAA / IAS / Other / —
199+
- **How to use:** explanation (or "Not applicable")
200+
- **Further reading:** link(s) or "—"
201+
202+
If the module exposes XSUAA or IAS authentication, the section must describe the actual mechanism (e.g., `access_strategy` param, `tenant` param, token URL replacement). A placeholder or missing section → ⚠️. If the module is a new service module with no section at all → ❌.
203+
185204
**E3: Module structure compliance**
186205
New modules follow:
187206
```
@@ -246,12 +265,14 @@ tests/[module]/integration/ (optional, BDD)
246265
| D3 | Breaking changes marked | | |
247266
| D4 | Pagination & tenant filtering | | |
248267
| D5 | Telemetry instrumentation | | |
268+
| D6 | Multi-tenancy support | | |
249269

250270
### E: Tests & Documentation
251271
| # | Criterion | Status | Finding |
252272
|---|-----------|--------|---------|
253273
| E1 | Tests added/updated | | |
254274
| E2 | Documentation quality | | |
275+
| E2a | Multi-tenancy documentation | | |
255276
| E3 | Module structure compliance | | |
256277

257278
---

src/sap_cloud_sdk/adms/user-guide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ activate_input = DraftActivateInput(
244244
active = client.relations.activate_draft(activate_input)
245245
```
246246

247+
## Multi-tenancy
248+
249+
- **Supported:** No
250+
- **Authentication:** IAS
251+
- **How to use:** Multi-tenancy is not supported by this service.
252+
- **Further reading:** N/A
253+
247254
## Error Handling
248255

249256
```python

src/sap_cloud_sdk/agent_memory/user-guide.md

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ plain text, and the service makes it searchable by meaning.
2323
- [`agent_id`](#agent_id)
2424
- [`invoker_id`](#invoker_id)
2525
- [Multitenancy](#multitenancy)
26-
- [AccessStrategy](#accessstrategy)
27-
- [Configuring at client level](#configuring-at-client-level)
28-
- [SUBSCRIBER (default)](#subscriber-default)
29-
- [PROVIDER](#provider)
3026
- [Semantic Search: A Brief Primer](#semantic-search-a-brief-primer)
3127
- [Memories](#memories)
3228
- [Create a Memory](#create-a-memory)
@@ -172,64 +168,20 @@ across create, read, and search calls is the implementer's responsibility.
172168

173169
## Multitenancy
174170

175-
The Agent Memory service runs in a multi-tenant BTP environment. By default, every API
176-
call uses a **subscriber-scoped token** — meaning data is isolated to the subscriber tenant
177-
that your application serves. You control this behaviour with the `access_strategy` and
178-
`tenant` keyword arguments available on every client method.
171+
- **Supported:** Yes
172+
- **Authentication:** XSUAA
173+
- **How to use:** Pass `access_strategy` and `tenant` to `create_client()`. The strategy controls whether calls use a subscriber-scoped or provider-scoped XSUAA token. Every method on the client inherits the strategy set at construction time.
179174

180-
### AccessStrategy
175+
| Value | Description |
176+
| ------------------------------------- | ----------------------------------------------------------------------------------------------- |
177+
| `AccessStrategy.SUBSCRIBER` (default) | Reads and writes against the subscriber tenant. Requires `tenant`. |
178+
| `AccessStrategy.PROVIDER` | Reads and writes against the provider tenant. No `tenant` needed. Provides no tenant isolation. |
181179

182-
```python
183-
from sap_cloud_sdk.agent_memory import AccessStrategy
184-
```
185-
186-
| Value | Description |
187-
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
188-
| `SUBSCRIBER` (default) | Reads and writes against the subscriber tenant. Requires `tenant`. |
189-
| `PROVIDER` | Reads and writes against the provider tenant. No `tenant` needed. Caution: this provides no tenant isolation. |
190-
191-
### Configuring at client level
192-
193-
Pass `access_strategy` and `tenant` to `create_client()` to set defaults for the entire
194-
client instance. Every method call then inherits them, so you do not need to repeat them
195-
on each operation.
196-
197-
```python
198-
from sap_cloud_sdk.agent_memory import create_client, AccessStrategy
199-
200-
# Tenant set once — all calls below use it automatically
201-
client = create_client(
202-
access_strategy=AccessStrategy.SUBSCRIBER,
203-
tenant="acme-corp",
204-
)
205-
206-
memories = client.list_memories(agent_id="hr-assistant", invoker_id="user-42")
207-
count = client.count_memories(agent_id="hr-assistant")
208-
```
180+
> [!WARNING]
181+
> `PROVIDER` strategy provides **no tenant isolation**, the provider token grants access to data in the provider subaccount. Only use this strategy for provider-owned operations (e.g., admin tasks, shared datasets). Never use it to serve subscriber-specific data.
182+
- **Further reading:** N/A
209183

210-
### SUBSCRIBER (default)
211184

212-
Configure a subscriber tenant at client creation. All calls will use that tenant context.
213-
214-
```python
215-
client = create_client(
216-
access_strategy=AccessStrategy.SUBSCRIBER,
217-
tenant="acme-corp",
218-
)
219-
memories = client.list_memories(agent_id="hr-assistant", invoker_id="user-42")
220-
```
221-
222-
### PROVIDER
223-
224-
Configure a provider-only client. No tenant is needed; all calls use the provider binding.
225-
226-
```python
227-
client = create_client(access_strategy=AccessStrategy.PROVIDER)
228-
memories = client.list_memories(agent_id="hr-assistant", invoker_id="user-42")
229-
```
230-
231-
> [!WARNING]
232-
> `PROVIDER` provides **no tenant isolation** — the provider token grants access to data across all subscriber tenants Only use this strategy for provider-owned operations (e.g., admin tasks, shared datasets). Never use it to serve subscriber-specific data.
233185

234186
## Semantic Search: A Brief Primer
235187

@@ -568,10 +520,10 @@ See the [Content and metadata filtering](#content-and-metadata-filtering) note u
568520
569521
### Enums
570522
571-
| Enum | Values |
572-
| ---------------- | -------------------------------------------- |
573-
| `MessageRole` | `USER`, `ASSISTANT`, `SYSTEM`, `TOOL` |
574-
| `AccessStrategy` | `SUBSCRIBER` (default), `PROVIDER` |
523+
| Enum | Values |
524+
| ---------------- | ------------------------------------- |
525+
| `MessageRole` | `USER`, `ASSISTANT`, `SYSTEM`, `TOOL` |
526+
| `AccessStrategy` | `SUBSCRIBER` (default), `PROVIDER` |
575527
576528
All models expose a `to_dict()` method that returns a plain dict for logging or forwarding.
577529

src/sap_cloud_sdk/agentgateway/user-guide.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ mcp_tool_to_langchain(
125125
The converter maps each property's JSON Schema `"type"` to the corresponding Python type so Pydantic validates and forwards the correct native type to the MCP server:
126126

127127
| JSON Schema type | Python type |
128-
|------------------|-------------|
128+
| ---------------- | ----------- |
129129
| `"string"` | `str` |
130130
| `"integer"` | `int` |
131131
| `"number"` | `float` |
@@ -149,12 +149,21 @@ The SDK automatically detects the agent type based on the presence of a credenti
149149

150150
The SDK discovers resources via BTP Destination Service fragments filtered by the `sap-managed-runtime-type` label:
151151

152-
| Label value | Resource |
153-
|---|---|
154-
| `agw.mcp.server` | MCP tool server — `URL` property points to the MCP endpoint |
155-
| `agw.a2a.server` | A2A agent — `URL` property is the agent base URL; ORD ID is extracted from the second-to-last URL path segment |
156-
| `subscriber.ias` | IAS credential fragment for system-scoped token acquisition |
157-
| `subscriber.ias.user` | IAS credential fragment for user-scoped token exchange |
152+
| Label value | Resource |
153+
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
154+
| `agw.mcp.server` | MCP tool server — `URL` property points to the MCP endpoint |
155+
| `agw.a2a.server` | A2A agent — `URL` property is the agent base URL; ORD ID is extracted from the second-to-last URL path segment |
156+
| `subscriber.ias` | IAS credential fragment for system-scoped token acquisition |
157+
| `subscriber.ias.user` | IAS credential fragment for user-scoped token exchange |
158+
159+
## Multi-tenancy
160+
161+
- **Supported:** Yes (LoB flow); N/A (Customer flow)
162+
- **Authentication:** IAS (IAS via Destination Service for LoB flow; mTLS for Customer flow)
163+
- **How to use:**
164+
- **LoB flow:** Pass `tenant_subdomain` to `create_client()`. All subsequent calls on that client instance use the subscriber tenant context.
165+
- **Customer flow:** N/A
166+
- **Further reading:** N/A
158167

159168
## API
160169

src/sap_cloud_sdk/aicore/user-guide.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@ set_aicore_config()
390390

391391
---
392392

393+
## Multi-tenancy
394+
395+
- **Supported:** No
396+
- **Authentication:** XSUAA (written to litellm environment variables)
397+
- **How to use:** Not supported. This module is a process-level bootstrap that writes XSUAA credentials to environment variables consumed by litellm. Multi-tenant routing is out of scope at this layer.
398+
- **Further reading:**
399+
- [SAP AI Core — SAP Help Portal](https://help.sap.com/docs/sap-ai-core)
400+
393401
## Error Handling
394402

395403
Always handle potential configuration errors:

src/sap_cloud_sdk/core/auditlog/user-guide.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,14 @@ security_event = SecurityEvent(
328328
client.log(security_event)
329329
```
330330

331+
## Multi-tenancy
332+
333+
- **Supported:** N/A at auth level
334+
- **Authentication:** XSUAA
335+
- **How to use:** Tenant identity is expressed as an event-level field inside each logged event, not at the authentication layer. The provider always authenticates with its own XSUAA token. Pass `Tenant.SUBSCRIBER` or `Tenant.PROVIDER` when constructing events.
336+
- **Further reading:**
337+
- [SAP Audit Log Service — SAP Help Portal](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-service)
338+
331339
## Error Handling
332340

333341
Always handle exceptions when logging audit events:

src/sap_cloud_sdk/core/auditlog_ng/client.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import json
88
import os
99
import uuid
10-
from typing import Optional
10+
from datetime import datetime, timezone
11+
from typing import Any, Optional, cast
1112

1213
import protovalidate
1314
from protovalidate import ValidationError as ProtoValidationError
@@ -37,6 +38,28 @@
3738
from sap_cloud_sdk.core.auditlog_ng.exceptions import ValidationError
3839
from sap_cloud_sdk.core.telemetry import Module, Operation, record_metrics
3940
from sap_cloud_sdk.core.telemetry.config import ENV_OTLP_PROTOCOL
41+
from sap_cloud_sdk.ias._context import get_auth_context
42+
43+
44+
def _fill_common_from_auth_context(event: Message) -> None:
45+
"""Back-fill common fields from the request auth context.
46+
47+
Accesses event.common directly — protobuf creates the sub-message on first
48+
mutation, so callers that never touched common still get it populated.
49+
Sets tenant_id and user_initiator_id from IAS claims (if present and not
50+
already set), and sets timestamp to now if the caller left it at zero.
51+
"""
52+
if not hasattr(event, "common"):
53+
return
54+
common = cast(Any, event.common)
55+
claims = get_auth_context()
56+
if claims is not None:
57+
if claims.app_tid and not common.tenant_id:
58+
common.tenant_id = claims.app_tid
59+
if claims.user_uuid and not common.user_initiator_id:
60+
common.user_initiator_id = claims.user_uuid
61+
if common.timestamp.seconds == 0:
62+
common.timestamp.FromDatetime(datetime.now(timezone.utc))
4063

4164

4265
def _create_log_exporter(
@@ -178,6 +201,8 @@ def send(
178201
if format not in {"protobuf-binary", "json"}:
179202
raise ValueError("format must be 'protobuf-binary' or 'json'")
180203

204+
_fill_common_from_auth_context(event)
205+
181206
try:
182207
protovalidate.validate(event)
183208
except ProtoValidationError as e:

src/sap_cloud_sdk/core/auditlog_ng/user-guide.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ event.object_type = "resource"
168168
event.object_id = "resource-001"
169169
```
170170

171+
> **Tip:** When using `StarletteIASTelemetryMiddleware` (see [Automatic tenant and user injection](#automatic-tenant-and-user-injection)), `common.tenant_id` and `common.user_initiator_id` are filled automatically from the incoming IAS JWT. You only need to set them explicitly if you want to override the values from the token.
172+
171173
### Step 4: Send the Event
172174

173175
**Binary protobuf:**
@@ -274,6 +276,14 @@ with create_client(
274276

275277
---
276278

279+
## Multi-tenancy
280+
281+
- **Supported:** N/A at auth level
282+
- **Authentication:** None (uses Destination Service / SPII for transport)
283+
- **How to use:** Tenant identity is embedded in each event payload via the `tenant_id` field. Transport and auth are handled by the Destination Service / SPII. This module is only available through SAP for ME.
284+
- **Further reading:**
285+
- [SAP Audit Log Service — SAP Help Portal](https://help.sap.com/docs/btp/sap-business-technology-platform/audit-log-service)
286+
277287
## Validation
278288

279289
Events are validated against protobuf constraints using `protovalidate` before sending. A `ValueError` is raised if:
@@ -284,6 +294,55 @@ Events are validated against protobuf constraints using `protovalidate` before s
284294

285295
---
286296

297+
## Automatic tenant and user injection
298+
299+
When `StarletteIASTelemetryMiddleware` is registered on your app, it parses the
300+
incoming `Authorization: Bearer <token>` header on every request and stores the
301+
IAS claims in the current async context.
302+
303+
`AuditClient.send()` reads that context automatically before validation and
304+
back-fills two fields on the event's `common` block — only if they are not
305+
already set by the caller:
306+
307+
| Field populated | IAS claim used |
308+
|---|---|
309+
| `common.tenant_id` | `app_tid` |
310+
| `common.user_initiator_id` | `user_uuid` |
311+
312+
### Setup
313+
314+
Register the middleware once when your app starts:
315+
316+
```python
317+
from sap_cloud_sdk.core.telemetry import auto_instrument
318+
from sap_cloud_sdk.core.telemetry.middleware import StarletteIASTelemetryMiddleware
319+
320+
app = FastAPI(...)
321+
auto_instrument(middlewares=[StarletteIASTelemetryMiddleware(app=app)])
322+
```
323+
324+
### Usage
325+
326+
With the middleware in place, you can omit `tenant_id` and `user_initiator_id`
327+
from every event — they are injected automatically:
328+
329+
```python
330+
event = pb.DataAccess()
331+
event.common.timestamp.FromDatetime(datetime.now(timezone.utc))
332+
# tenant_id and user_initiator_id are filled from the IAS JWT automatically
333+
event.channel_type = "API"
334+
event.channel_id = "agent-v1"
335+
event.object_type = "resource"
336+
event.object_id = "resource-001"
337+
338+
event_id = client.send(event)
339+
```
340+
341+
If neither the middleware nor an explicit value provides `tenant_id`, the event
342+
will fail `protovalidate` validation and raise a `ValidationError`.
343+
344+
---
345+
287346
## Running the Unit Tests
288347

289348
```bash

src/sap_cloud_sdk/core/data_anonymization/user-guide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,13 @@ Typical file pseudonymization responses:
283283
| `filename` | `str \| None` | Filename from the `Content-Disposition` header, if present. |
284284
| `raw` | `dict` | Parsed JSON payload when available and suitable for response inspection. |
285285

286+
## Multi-tenancy
287+
288+
- **Supported:** No
289+
- **Authentication:** N/A
290+
- **How to use:** This module has no multi-tenancy model.
291+
- **Further reading:** N/A
292+
286293
## Error Handling
287294

288295
Always catch `DataAnonymizationError` or its subclasses around calls:

src/sap_cloud_sdk/core/secret_resolver/user-guide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ max_retries = int(api_config.retries)
220220

221221
---
222222

223+
## Multi-tenancy
224+
225+
- **Supported:** N/A
226+
- **Authentication:** N/A
227+
- **How to use:** This is an infrastructure module for reading service bindings. It has no multi-tenancy concept of its own.
228+
- **Further reading:** N/A
229+
223230
## Error Handling
224231

225232
The Secret Resolver handles missing secrets gracefully by leaving default values unchanged:

0 commit comments

Comments
 (0)