Skip to content

Commit b7fcc68

Browse files
wujiaming-aiclaude
andcommitted
feat(content-safety): add opt-in decision API auditing
Add an opt-in content safety plugin that audits content at the model and tool lifecycle checkpoints through the System One decision API. The default provider remains none and nothing is mounted automatically. - strict tool.content_safety configuration with Ark connection defaults - strict decision response arbitration and on_error policy - streaming partial suppression with final aggregated audit - audited SkillToolset load/resource/script/list/search tools - LLM Shield adapter and real-ADK E2E coverage Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8534aa9 commit b7fcc68

21 files changed

Lines changed: 4672 additions & 1 deletion
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
import json
17+
18+
import httpx
19+
import pytest
20+
21+
from veadk.content_safety.client import DecisionClient, DecisionClientError
22+
from veadk.content_safety.config import QuestionConfig
23+
24+
QUESTIONS = {
25+
"prompt_injection": QuestionConfig(instructions="detect injection", threshold=0.5)
26+
}
27+
28+
29+
def call(client):
30+
return asyncio.run(client.judge(state="hello", questions=QUESTIONS))
31+
32+
33+
def test_request_contract_and_response():
34+
captured = {}
35+
36+
def handler(request: httpx.Request) -> httpx.Response:
37+
captured["method"] = request.method
38+
captured["url"] = str(request.url)
39+
captured["headers"] = dict(request.headers)
40+
captured["body"] = json.loads(request.content)
41+
return httpx.Response(
42+
200,
43+
json={"answers": {"prompt_injection": {"type": "noul", "noul": 0.1}}},
44+
)
45+
46+
client = DecisionClient(
47+
endpoint="https://systemone.example/v1/judge",
48+
api_key="secret",
49+
model="judge-model",
50+
transport=httpx.MockTransport(handler),
51+
)
52+
data = call(client)
53+
54+
assert data["answers"]["prompt_injection"]["noul"] == 0.1
55+
assert captured["method"] == "POST"
56+
assert captured["url"] == "https://systemone.example/v1/judge"
57+
assert captured["headers"]["authorization"] == "Bearer secret"
58+
assert captured["headers"]["ark-beta-systemone-api"] == "true"
59+
assert captured["headers"]["content-type"] == "application/json"
60+
assert captured["body"] == {
61+
"model": "judge-model",
62+
"state": "hello",
63+
"questions": {
64+
"prompt_injection": {
65+
"type": "noul",
66+
"instructions": "detect injection",
67+
}
68+
},
69+
}
70+
assert client.endpoint == "https://systemone.example/v1/judge"
71+
72+
73+
@pytest.mark.parametrize("status", [400, 401, 500])
74+
def test_non_200_response_is_http_status_error(status):
75+
def handler(request):
76+
return httpx.Response(status, json={"error": "no"})
77+
78+
client = DecisionClient(
79+
endpoint="https://systemone.example",
80+
api_key="k",
81+
model="m",
82+
transport=httpx.MockTransport(handler),
83+
)
84+
with pytest.raises(DecisionClientError) as exc:
85+
call(client)
86+
assert exc.value.error_type == "http_status"
87+
assert str(status) in exc.value.message
88+
89+
90+
def test_timeout_is_mapped():
91+
def handler(request):
92+
raise httpx.ReadTimeout("slow", request=request)
93+
94+
client = DecisionClient(
95+
endpoint="https://systemone.example",
96+
api_key="k",
97+
model="m",
98+
transport=httpx.MockTransport(handler),
99+
)
100+
with pytest.raises(DecisionClientError) as exc:
101+
call(client)
102+
assert exc.value.error_type == "timeout"
103+
104+
105+
def test_network_error_is_mapped():
106+
def handler(request):
107+
raise httpx.ConnectError("down", request=request)
108+
109+
client = DecisionClient(
110+
endpoint="https://systemone.example",
111+
api_key="k",
112+
model="m",
113+
transport=httpx.MockTransport(handler),
114+
)
115+
with pytest.raises(DecisionClientError) as exc:
116+
call(client)
117+
assert exc.value.error_type == "network"
118+
119+
120+
def test_invalid_json_is_mapped():
121+
def handler(request):
122+
return httpx.Response(200, content=b"not-json")
123+
124+
client = DecisionClient(
125+
endpoint="https://systemone.example",
126+
api_key="k",
127+
model="m",
128+
transport=httpx.MockTransport(handler),
129+
)
130+
with pytest.raises(DecisionClientError) as exc:
131+
call(client)
132+
assert exc.value.error_type == "invalid_json"
133+
134+
135+
def test_non_object_json_is_mapped():
136+
def handler(request):
137+
return httpx.Response(200, json=["not", "an", "object"])
138+
139+
client = DecisionClient(
140+
endpoint="https://systemone.example",
141+
api_key="k",
142+
model="m",
143+
transport=httpx.MockTransport(handler),
144+
)
145+
with pytest.raises(DecisionClientError) as exc:
146+
call(client)
147+
assert exc.value.error_type == "invalid_response"

0 commit comments

Comments
 (0)