Skip to content

Commit a51145e

Browse files
fix(auditlog-ng): strip prompt/response text from AiMessageContext
1 parent 5b56ed3 commit a51145e

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

‎src/sap_cloud_sdk/core/auditlog_ng/client.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,29 @@ def _fill_common_from_auth_context(event: Message) -> None:
7676
common.timestamp.FromDatetime(datetime.now(timezone.utc))
7777

7878

79+
def _strip_message_context_text(event: Message) -> None:
80+
"""Remove free-text fields from an event's ``message_context``, if present.
81+
82+
Several AI audit events embed an ``AiMessageContext`` (proto field
83+
``message_context``) whose ``prompt_text`` and ``response_text`` fields hold
84+
the verbatim model input/output and may contain sensitive data. We never
85+
forward that content to the audit log; correlation fields (uid, name,
86+
ai_role_id) and token counts are left intact.
87+
88+
Safe to call on any event: events without a ``message_context`` field, or
89+
with one that was never set, are left untouched (we only act when the field
90+
is actually present, so we never auto-create an empty sub-message).
91+
"""
92+
if not hasattr(event, "message_context"):
93+
return
94+
event_any = cast(Any, event)
95+
if not event_any.HasField("message_context"):
96+
return
97+
message_context = event_any.message_context
98+
message_context.ClearField("prompt_text")
99+
message_context.ClearField("response_text")
100+
101+
79102
def _create_log_exporter(
80103
config: AuditLogNGConfig,
81104
credentials: Optional[grpc.ChannelCredentials],
@@ -216,6 +239,7 @@ def send(
216239
raise ValueError("format must be 'protobuf-binary' or 'json'")
217240

218241
_fill_common_from_auth_context(event)
242+
_strip_message_context_text(event)
219243

220244
try:
221245
protovalidate.validate(event)

‎tests/core/unit/auditlog_ng/unit/test_client.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sap_cloud_sdk.core.auditlog_ng.client import (
1212
AuditClient,
1313
_fill_common_from_auth_context,
14+
_strip_message_context_text,
1415
)
1516
from sap_cloud_sdk.core.auditlog_ng.config import AuditLogNGConfig, SCHEMA_URL
1617
from sap_cloud_sdk.core.auditlog_ng.exceptions import ValidationError
@@ -401,3 +402,56 @@ def test_does_not_overwrite_explicit_timestamp(self):
401402
def test_event_without_common_field_is_skipped(self):
402403
event = MagicMock(spec=[]) # no 'common' attribute
403404
_fill_common_from_auth_context(event) # must not raise
405+
406+
407+
class TestStripMessageContextText:
408+
def test_strips_prompt_and_response_text(self):
409+
event = pb.AiPromptReceived()
410+
event.message_context.prompt_text = "verbatim user prompt"
411+
event.message_context.response_text = "verbatim model response"
412+
_strip_message_context_text(event)
413+
assert event.message_context.prompt_text == ""
414+
assert event.message_context.response_text == ""
415+
416+
def test_preserves_correlation_and_token_fields(self):
417+
event = pb.AiPromptReceived()
418+
event.message_context.prompt_text = "secret"
419+
event.message_context.response_text = "secret"
420+
event.message_context.uid = "conversation-123"
421+
event.message_context.name = "thread-name"
422+
event.message_context.prompt_tokens = 42
423+
event.message_context.completion_tokens = 7
424+
event.message_context.total_tokens = 49
425+
_strip_message_context_text(event)
426+
assert event.message_context.uid == "conversation-123"
427+
assert event.message_context.name == "thread-name"
428+
assert event.message_context.prompt_tokens == 42
429+
assert event.message_context.completion_tokens == 7
430+
assert event.message_context.total_tokens == 49
431+
432+
def test_unset_message_context_is_not_created(self):
433+
event = pb.AiPromptReceived()
434+
_strip_message_context_text(event)
435+
assert not event.HasField("message_context")
436+
437+
def test_event_without_message_context_field_is_skipped(self):
438+
# AiSkillSelected has no message_context field at all.
439+
event = pb.AiSkillSelected()
440+
_strip_message_context_text(event) # must not raise
441+
442+
def test_send_strips_message_context_text_before_emit(self):
443+
client, mock_logger, _, _, patcher, _ = _make_mocked_client()
444+
try:
445+
event = pb.AiPromptReceived()
446+
event.common.tenant_id = "tenant-123"
447+
event.message_context.prompt_text = "sensitive prompt"
448+
event.message_context.response_text = "sensitive response"
449+
450+
client.send(event)
451+
452+
# The event object itself is scrubbed in place before serialization.
453+
assert event.message_context.prompt_text == ""
454+
assert event.message_context.response_text == ""
455+
mock_logger.emit.assert_called_once()
456+
finally:
457+
patcher.stop()

0 commit comments

Comments
 (0)