Skip to content

Commit de5f42a

Browse files
Jazzcortowtaylor
andauthored
Mitigate tool call error due to missing "detail" field in the gatekeeper response (#448)
fix: handle null or missing detail in gatekeeper response Add default empty string for GatekeeperResult.detail so responses that omit the field (common when status is OK) no longer cause errors. Introduce GatekeeperResultStrict for structured LLM output and unify JSON parsing through model_validate_json, removing the manual fallback. Co-authored-by: Owen Taylor <otaylor@fishsoup.net>
1 parent 710d271 commit de5f42a

3 files changed

Lines changed: 29 additions & 30 deletions

File tree

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from linux_mcp_server.gatekeeper.check_run_script import check_run_script
22
from linux_mcp_server.gatekeeper.check_run_script import GatekeeperResult
3+
from linux_mcp_server.gatekeeper.check_run_script import GatekeeperResultStrict
34
from linux_mcp_server.gatekeeper.check_run_script import GatekeeperStatus
45

56

6-
__all__ = ["check_run_script", "GatekeeperStatus", "GatekeeperResult"]
7+
__all__ = ["check_run_script", "GatekeeperStatus", "GatekeeperResult", "GatekeeperResultStrict"]

src/linux_mcp_server/gatekeeper/check_run_script.py

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import logging
32

43
from litellm import Choices
@@ -96,6 +95,8 @@ def get_model() -> str:
9695
what is wrong with the script. Be specific to allow the language model to correct
9796
the problem.
9897
98+
If status is OK, the detail should be an empty string.
99+
99100
If the script seems buggy but does not fall into any of the categories above, return
100101
a status of `OK`.
101102
@@ -116,7 +117,7 @@ class GatekeeperStatus(StrEnum):
116117

117118
class GatekeeperResult(BaseModel):
118119
status: GatekeeperStatus
119-
detail: str
120+
detail: str = ""
120121

121122
@property
122123
def description(self) -> str:
@@ -175,6 +176,10 @@ def parse_from_description(cls, description: str) -> "GatekeeperResult":
175176
return cls(status=status, detail=detail)
176177

177178

179+
class GatekeeperResultStrict(GatekeeperResult):
180+
detail: str # type:ignore
181+
182+
178183
def check_run_script(description: str, script_type: str, script: str, *, readonly: bool) -> GatekeeperResult:
179184
# Check that the script does what is described
180185
if "start_of_script" in script.lower() or "end_of_script" in script.lower():
@@ -200,7 +205,7 @@ def check_run_script(description: str, script_type: str, script: str, *, readonl
200205

201206
params = get_supported_openai_params(model=get_model())
202207
if params is not None and "response_format" in params:
203-
response_format = GatekeeperResult
208+
response_format = GatekeeperResultStrict
204209
else:
205210
response_format = None
206211

@@ -211,22 +216,4 @@ def check_run_script(description: str, script_type: str, script: str, *, readonl
211216

212217
logger.info(f"Gatekeeper response: {response_text}")
213218

214-
if response_format is not None:
215-
return GatekeeperResult.model_validate_json(response_text)
216-
217-
try:
218-
response_data = json.loads(response_text)
219-
except json.JSONDecodeError:
220-
raise RuntimeError("Failed to parse response from gatekeeper model")
221-
222-
if not isinstance(response_data, dict):
223-
raise RuntimeError("Invalid response format from gatekeeper model")
224-
225-
try:
226-
status = GatekeeperStatus(response_data.get("status", ""))
227-
except ValueError:
228-
raise RuntimeError("Bad status in gatekeeper model response")
229-
230-
detail = response_data.get("detail", "")
231-
232-
return GatekeeperResult(status=status, detail=detail)
219+
return GatekeeperResult.model_validate_json(response_text)

tests/gatekeeper/test_check_run_script.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
from litellm import Choices
88
from litellm import ModelResponse
9+
from pydantic import ValidationError
910

1011
from linux_mcp_server.gatekeeper import GatekeeperResult
12+
from linux_mcp_server.gatekeeper import GatekeeperResultStrict
1113
from linux_mcp_server.gatekeeper import GatekeeperStatus
1214
from linux_mcp_server.gatekeeper.check_run_script import check_run_script
1315
from linux_mcp_server.gatekeeper.check_run_script import get_model
@@ -123,22 +125,31 @@ def test_response_format_handling(self, mock_litellm, supported_params, expect_r
123125

124126
call_kwargs = mock_completion.call_args.kwargs
125127
if expect_response_format:
126-
assert call_kwargs["response_format"] is GatekeeperResult
128+
assert call_kwargs["response_format"] is GatekeeperResultStrict
127129
else:
128130
assert call_kwargs["response_format"] is None
129131

132+
def test_missing_detail_defaults_to_empty(self, mock_litellm):
133+
mock_completion, mock_get_params = mock_litellm
134+
mock_get_params.return_value = ["response_format"]
135+
mock_completion.return_value = self._make_response('{"status": "OK"}')
136+
137+
result = check_run_script(description="test", script_type="bash", script="echo hi", readonly=True)
138+
assert result.status == GatekeeperStatus.OK
139+
assert result.detail == ""
140+
130141
@pytest.mark.parametrize(
131-
"response_text,error_match",
142+
"response_text,error_type,error_match",
132143
[
133-
("not valid json", "Failed to parse response"),
134-
('"just a string"', "Invalid response format"),
135-
('{"status": "INVALID_STATUS"}', "Bad status"),
144+
("not valid json", ValidationError, "input_value='not valid json'"),
145+
('"just a string"', ValidationError, "input_value='just a string'"),
146+
('{"status": "INVALID_STATUS"}', ValidationError, "input_value='INVALID_STATUS'"),
136147
],
137148
)
138-
def test_parse_errors(self, mock_litellm, response_text, error_match):
149+
def test_parse_errors(self, mock_litellm, response_text, error_type, error_match):
139150
mock_completion, mock_get_params = mock_litellm
140151
mock_get_params.return_value = None # No response_format support
141152
mock_completion.return_value = self._make_response(response_text)
142153

143-
with pytest.raises(RuntimeError, match=error_match):
154+
with pytest.raises(error_type, match=error_match):
144155
check_run_script(description="test", script_type="bash", script="echo hi", readonly=True)

0 commit comments

Comments
 (0)