@@ -182,7 +182,7 @@ def object_schema(properties: dict[str, Union[str, dict[str, Any]]], *, required
182182
183183def read_file (config : WorkspaceToolConfig , args : dict [str , Any ]) -> dict [str , Any ]:
184184 path = resolve_workspace_path (config , args ["path" ])
185- max_bytes = args . get ( "max_bytes" , config .max_read_bytes )
185+ max_bytes = non_negative_int_arg ( args , "max_bytes" , default = config .max_read_bytes )
186186 data = path .read_bytes ()
187187 truncated = len (data ) > max_bytes
188188 return {
@@ -241,7 +241,7 @@ def edit_at_hash(config: WorkspaceToolConfig, args: dict[str, Any]) -> dict[str,
241241
242242def list_files (config : WorkspaceToolConfig , args : dict [str , Any ]) -> dict [str , Any ]:
243243 pattern = args ["pattern" ]
244- limit = args . get ( "limit" , 200 )
244+ limit = positive_int_arg ( args , "limit" , default = 200 )
245245 files = []
246246 for path in sorted (config .root .rglob ("*" )):
247247 if path .is_file () and ".git" not in path .parts and fnmatch (relative_path (config , path ), pattern ):
@@ -254,7 +254,7 @@ def list_files(config: WorkspaceToolConfig, args: dict[str, Any]) -> dict[str, A
254254def search_text (config : WorkspaceToolConfig , args : dict [str , Any ]) -> dict [str , Any ]:
255255 regex = re .compile (args ["pattern" ])
256256 include = args .get ("include" , "*" )
257- limit = args . get ( "limit" , 200 )
257+ limit = positive_int_arg ( args , "limit" , default = 200 )
258258 matches = []
259259 for path in sorted (config .root .rglob ("*" )):
260260 if not path .is_file () or ".git" in path .parts or not fnmatch (relative_path (config , path ), include ):
@@ -283,7 +283,7 @@ def git_status(config: WorkspaceToolConfig) -> dict[str, Any]:
283283
284284
285285def run_shell (config : WorkspaceToolConfig , args : dict [str , Any ]) -> dict [str , Any ]:
286- timeout = args . get ( "timeout_seconds" , config .command_timeout_seconds )
286+ timeout = positive_int_arg ( args , "timeout_seconds" , default = config .command_timeout_seconds )
287287 result = subprocess .run (
288288 args ["command" ],
289289 cwd = str (config .root ),
@@ -319,10 +319,24 @@ def run_shell_inspect(config: WorkspaceToolConfig, args: dict[str, Any]) -> dict
319319 policy = classify_shell_command_policy (args ["command" ])
320320 if policy != "inspect" :
321321 raise ValueError ("command is not inspect-safe; retry with workspace_run_shell_mutate" )
322- timeout = args . get ( "timeout_seconds" , config .command_timeout_seconds )
322+ timeout = positive_int_arg ( args , "timeout_seconds" , default = config .command_timeout_seconds )
323323 return run_shell_argv (config , shlex .split (args ["command" ]), timeout_seconds = timeout )
324324
325325
326+ def positive_int_arg (args : dict [str , Any ], name : str , * , default : int ) -> int :
327+ value = args .get (name , default )
328+ if value < 1 :
329+ raise ValueError (f"{ name } must be >= 1" )
330+ return value
331+
332+
333+ def non_negative_int_arg (args : dict [str , Any ], name : str , * , default : int ) -> int :
334+ value = args .get (name , default )
335+ if value < 0 :
336+ raise ValueError (f"{ name } must be >= 0" )
337+ return value
338+
339+
326340def classify_shell_command_policy (command : str ) -> str :
327341 blocked_tokens = (">" , "<" , "|" , "&&" , ";" , "`" , "$(" )
328342 if any (token in command for token in blocked_tokens ):
0 commit comments