Skip to content

Commit 478ebef

Browse files
Altruistusclaude
andcommitted
CM-73403: Report the device a hook ran on with each AI hook event
The platform opens the unauthorized MCP server violation on this device instead of guessing among the user's devices. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 362469f commit 478ebef

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

‎cycode/cyclient/ai_security_manager_client.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING, Optional
44

55
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError
6+
from cycode.cli.utils.host_info import get_serial_number
67
from cycode.cyclient.cycode_client_base import CycodeClientBase
78
from cycode.cyclient.logger import logger
89

@@ -87,6 +88,9 @@ def create_event(
8788
'mcp_tool_name': payload.mcp_tool_name,
8889
'error_message': error_message,
8990
'file_path': file_path,
91+
# The device the hook ran on, so a guardrail violation lands on it rather than on any of
92+
# the user's devices. None where no serial can be resolved (e.g. Linux).
93+
'device_id': get_serial_number(),
9094
}
9195

9296
try:

‎tests/cyclient/test_ai_security_manager_client.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from unittest.mock import MagicMock
22

3+
import pytest
4+
from pytest_mock import MockerFixture
5+
36
from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
47
from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType, AIHookOutcome
58
from cycode.cyclient.ai_security_manager_client import AISecurityManagerClient
@@ -13,6 +16,12 @@ def _build_client() -> tuple[AISecurityManagerClient, MagicMock]:
1316
return AISecurityManagerClient(http_client, service_config), http_client
1417

1518

19+
@pytest.fixture(autouse=True)
20+
def serial_number(mocker: MockerFixture) -> MagicMock:
21+
"""Resolving the real serial shells out and caches under the home directory."""
22+
return mocker.patch('cycode.cyclient.ai_security_manager_client.get_serial_number', return_value='SERIAL-1')
23+
24+
1625
def _posted_body(http_client: MagicMock) -> dict:
1726
return http_client.post.call_args.kwargs['body']
1827

@@ -27,6 +36,26 @@ def test_create_event_reports_the_payload_hook_event_id_as_the_event_id() -> Non
2736
assert _posted_body(http_client)['id'] == payload.hook_event_id
2837

2938

39+
def test_create_event_reports_the_device_it_ran_on() -> None:
40+
"""The guardrail violation is attributed to this device, not to any of the user's devices."""
41+
client, http_client = _build_client()
42+
payload = AIHookPayload(event_name='Prompt', conversation_id='conv-1', generation_id='gen-1')
43+
44+
client.create_event(payload, AiHookEventType.PROMPT, AIHookOutcome.ALLOWED)
45+
46+
assert _posted_body(http_client)['device_id'] == 'SERIAL-1'
47+
48+
49+
def test_create_event_without_a_resolvable_serial_reports_none(serial_number: MagicMock) -> None:
50+
serial_number.return_value = None
51+
client, http_client = _build_client()
52+
payload = AIHookPayload(event_name='Prompt', conversation_id='conv-1', generation_id='gen-1')
53+
54+
client.create_event(payload, AiHookEventType.PROMPT, AIHookOutcome.ALLOWED)
55+
56+
assert _posted_body(http_client)['device_id'] is None
57+
58+
3059
def test_create_event_reports_a_distinct_id_per_hook_event() -> None:
3160
client, http_client = _build_client()
3261
conversation_id = 'conv-1'

0 commit comments

Comments
 (0)