fleet-python 0.2.64__tar.gz → 0.2.66b2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

Files changed (86) hide show
  1. {fleet_python-0.2.64/fleet_python.egg-info → fleet_python-0.2.66b2}/PKG-INFO +1 -1
  2. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/models.py +2 -0
  3. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/tasks.py +7 -9
  4. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/models.py +2 -0
  5. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/tasks.py +7 -9
  6. {fleet_python-0.2.64 → fleet_python-0.2.66b2/fleet_python.egg-info}/PKG-INFO +1 -1
  7. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet_python.egg-info/SOURCES.txt +2 -1
  8. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/pyproject.toml +1 -1
  9. fleet_python-0.2.66b2/tests/test_verifier_security.py +427 -0
  10. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/LICENSE +0 -0
  11. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/README.md +0 -0
  12. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/diff_example.py +0 -0
  13. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/dsl_example.py +0 -0
  14. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example.py +0 -0
  15. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/exampleResume.py +0 -0
  16. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_account.py +0 -0
  17. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_action_log.py +0 -0
  18. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_client.py +0 -0
  19. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_mcp_anthropic.py +0 -0
  20. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_mcp_openai.py +0 -0
  21. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_sync.py +0 -0
  22. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_task.py +0 -0
  23. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_tasks.py +0 -0
  24. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/example_verifier.py +0 -0
  25. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/export_tasks.py +0 -0
  26. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/gemini_example.py +0 -0
  27. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/import_tasks.py +0 -0
  28. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/json_tasks_example.py +0 -0
  29. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/nova_act_example.py +0 -0
  30. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/openai_example.py +0 -0
  31. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/openai_simple_example.py +0 -0
  32. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/query_builder_example.py +0 -0
  33. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/quickstart.py +0 -0
  34. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/examples/test_cdp_logging.py +0 -0
  35. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/__init__.py +0 -0
  36. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/__init__.py +0 -0
  37. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/base.py +0 -0
  38. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/client.py +0 -0
  39. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/env/__init__.py +0 -0
  40. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/env/client.py +0 -0
  41. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/exceptions.py +0 -0
  42. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/global_client.py +0 -0
  43. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/instance/__init__.py +0 -0
  44. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/instance/base.py +0 -0
  45. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/instance/client.py +0 -0
  46. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/resources/__init__.py +0 -0
  47. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/resources/base.py +0 -0
  48. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/resources/browser.py +0 -0
  49. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/resources/mcp.py +0 -0
  50. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/resources/sqlite.py +0 -0
  51. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/verifiers/__init__.py +0 -0
  52. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/verifiers/bundler.py +0 -0
  53. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/_async/verifiers/verifier.py +0 -0
  54. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/base.py +0 -0
  55. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/client.py +0 -0
  56. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/config.py +0 -0
  57. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/env/__init__.py +0 -0
  58. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/env/client.py +0 -0
  59. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/exceptions.py +0 -0
  60. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/global_client.py +0 -0
  61. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/instance/__init__.py +0 -0
  62. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/instance/base.py +0 -0
  63. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/instance/client.py +0 -0
  64. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/instance/models.py +0 -0
  65. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/resources/__init__.py +0 -0
  66. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/resources/base.py +0 -0
  67. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/resources/browser.py +0 -0
  68. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/resources/mcp.py +0 -0
  69. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/resources/sqlite.py +0 -0
  70. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/types.py +0 -0
  71. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/__init__.py +0 -0
  72. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/bundler.py +0 -0
  73. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/code.py +0 -0
  74. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/db.py +0 -0
  75. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/decorator.py +0 -0
  76. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/parse.py +0 -0
  77. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/sql_differ.py +0 -0
  78. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet/verifiers/verifier.py +0 -0
  79. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet_python.egg-info/dependency_links.txt +0 -0
  80. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet_python.egg-info/requires.txt +0 -0
  81. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/fleet_python.egg-info/top_level.txt +0 -0
  82. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/scripts/fix_sync_imports.py +0 -0
  83. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/scripts/unasync.py +0 -0
  84. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/setup.cfg +0 -0
  85. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/tests/__init__.py +0 -0
  86. {fleet_python-0.2.64 → fleet_python-0.2.66b2}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.64
3
+ Version: 0.2.66b2
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -349,6 +349,8 @@ class InstanceResponse(BaseModel):
349
349
  team_id: str = Field(..., title="Team Id")
350
350
  region: str = Field(..., title="Region")
351
351
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
352
+ data_key: Optional[str] = Field(None, title="Data Key")
353
+ data_version: Optional[str] = Field(None, title="Data Version")
352
354
  urls: Optional[InstanceURLs] = Field(None, title="Urls")
353
355
  health: Optional[bool] = Field(None, title="Health")
354
356
 
@@ -279,17 +279,14 @@ def verifier_from_string(
279
279
  """
280
280
  try:
281
281
  import inspect
282
- import re
283
282
  from .verifiers.verifier import AsyncVerifierFunction
284
283
  from fleet.verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
285
284
  from fleet.verifiers.db import IgnoreConfig
285
+ from fleet.verifiers.parsing import parse_and_validate_verifier
286
286
 
287
- # Strip @verifier decorator if present to avoid double-wrapping
288
- # Remove lines like: @verifier(key="...")
289
- cleaned_code = re.sub(r'@verifier\([^)]*\)\s*\n', '', verifier_func)
290
- # Also remove the verifier import if present
291
- cleaned_code = re.sub(r'from fleet import.*verifier.*\n', '', cleaned_code)
292
- cleaned_code = re.sub(r'import.*verifier.*\n', '', cleaned_code)
287
+ # Validate the code and extract function name
288
+ # This ensures no arbitrary code execution during import
289
+ func_name = parse_and_validate_verifier(verifier_func)
293
290
 
294
291
  # Create a local namespace for executing the code
295
292
  local_namespace = {
@@ -299,8 +296,9 @@ def verifier_from_string(
299
296
  "Environment": object, # Add Environment type if needed
300
297
  }
301
298
 
302
- # Execute the cleaned verifier code in the namespace
303
- exec(cleaned_code, globals(), local_namespace)
299
+ # Execute the verifier code in the namespace
300
+ # This is now safe because we validated it contains only declarative code
301
+ exec(verifier_func, globals(), local_namespace)
304
302
 
305
303
  # Find the function that was defined (not imported)
306
304
  # Functions defined via exec have co_filename == '<string>'
@@ -356,6 +356,8 @@ class InstanceResponse(BaseModel):
356
356
  team_id: str = Field(..., title="Team Id")
357
357
  region: str = Field(..., title="Region")
358
358
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
359
+ data_key: Optional[str] = Field(None, title="Data Key")
360
+ data_version: Optional[str] = Field(None, title="Data Version")
359
361
  urls: Optional[InstanceURLs] = Field(None, title="Urls")
360
362
  health: Optional[bool] = Field(None, title="Health")
361
363
 
@@ -272,17 +272,14 @@ def verifier_from_string(
272
272
  """
273
273
  try:
274
274
  import inspect
275
- import re
276
275
  from .verifiers import SyncVerifierFunction
277
276
  from .verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
278
277
  from .verifiers.db import IgnoreConfig
278
+ from .verifiers.parsing import parse_and_validate_verifier
279
279
 
280
- # Strip @verifier decorator if present to avoid double-wrapping
281
- # Remove lines like: @verifier(key="...")
282
- cleaned_code = re.sub(r'@verifier\([^)]*\)\s*\n', '', verifier_func)
283
- # Also remove the verifier import if present
284
- cleaned_code = re.sub(r'from fleet import.*verifier.*\n', '', cleaned_code)
285
- cleaned_code = re.sub(r'import.*verifier.*\n', '', cleaned_code)
280
+ # Validate the code and extract function name
281
+ # This ensures no arbitrary code execution during import
282
+ func_name = parse_and_validate_verifier(verifier_func)
286
283
 
287
284
  # Create a globals namespace with all required imports
288
285
  exec_globals = globals().copy()
@@ -298,8 +295,9 @@ def verifier_from_string(
298
295
  # Create a local namespace for executing the code
299
296
  local_namespace = {}
300
297
 
301
- # Execute the cleaned verifier code in the namespace
302
- exec(cleaned_code, exec_globals, local_namespace)
298
+ # Execute the verifier code in the namespace
299
+ # This is now safe because we validated it contains only declarative code
300
+ exec(verifier_func, exec_globals, local_namespace)
303
301
 
304
302
  # Find the function that was defined (not imported)
305
303
  # Functions defined via exec have co_filename == '<string>'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.64
3
+ Version: 0.2.66b2
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -80,4 +80,5 @@ fleet_python.egg-info/top_level.txt
80
80
  scripts/fix_sync_imports.py
81
81
  scripts/unasync.py
82
82
  tests/__init__.py
83
- tests/test_verifier_from_string.py
83
+ tests/test_verifier_from_string.py
84
+ tests/test_verifier_security.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fleet-python"
7
- version = "0.2.64"
7
+ version = "0.2.66b2"
8
8
  description = "Python SDK for Fleet environments"
9
9
  authors = [
10
10
  {name = "Fleet AI", email = "nic@fleet.so"},
@@ -0,0 +1,427 @@
1
+ """Security tests for verifier_from_string function.
2
+
3
+ Tests that the verifier parsing and validation properly blocks
4
+ arbitrary code execution during import.
5
+ """
6
+
7
+ import pytest
8
+ from fleet.tasks import verifier_from_string as sync_verifier_from_string
9
+ from fleet._async.tasks import verifier_from_string as async_verifier_from_string
10
+
11
+
12
+ class TestSyncVerifierSecurity:
13
+ """Security tests for sync version of verifier_from_string."""
14
+
15
+ def test_blocks_module_level_subprocess_run(self):
16
+ """Test that module-level subprocess.run() is blocked."""
17
+ code = """
18
+ import subprocess
19
+ subprocess.run(['echo', 'malicious'])
20
+
21
+ def my_verifier(env):
22
+ return 1.0
23
+ """
24
+ with pytest.raises(ValueError, match="Expression statements that are not constants"):
25
+ sync_verifier_from_string(
26
+ verifier_func=code,
27
+ verifier_id="test-verifier",
28
+ verifier_key="test-key",
29
+ sha256="test-sha",
30
+ )
31
+
32
+ def test_blocks_module_level_open(self):
33
+ """Test that module-level open() is blocked."""
34
+ code = """
35
+ open('/etc/passwd', 'r')
36
+
37
+ def my_verifier(env):
38
+ return 1.0
39
+ """
40
+ with pytest.raises(ValueError, match="Expression statements that are not constants"):
41
+ sync_verifier_from_string(
42
+ verifier_func=code,
43
+ verifier_id="test-verifier",
44
+ verifier_key="test-key",
45
+ sha256="test-sha",
46
+ )
47
+
48
+ def test_blocks_assignment_with_subprocess_call(self):
49
+ """Test that variable assignment with subprocess call is blocked."""
50
+ code = """
51
+ import subprocess
52
+ result = subprocess.run(['echo', 'malicious'])
53
+
54
+ def my_verifier(env):
55
+ return 1.0
56
+ """
57
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
58
+ sync_verifier_from_string(
59
+ verifier_func=code,
60
+ verifier_id="test-verifier",
61
+ verifier_key="test-key",
62
+ sha256="test-sha",
63
+ )
64
+
65
+ def test_blocks_assignment_with_open_call(self):
66
+ """Test that variable assignment with open() is blocked."""
67
+ code = """
68
+ file_handle = open('/etc/passwd', 'r')
69
+
70
+ def my_verifier(env):
71
+ return 1.0
72
+ """
73
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
74
+ sync_verifier_from_string(
75
+ verifier_func=code,
76
+ verifier_id="test-verifier",
77
+ verifier_key="test-key",
78
+ sha256="test-sha",
79
+ )
80
+
81
+ def test_blocks_assignment_with_any_function_call(self):
82
+ """Test that variable assignment with any function call is blocked."""
83
+ code = """
84
+ import os
85
+ path = os.getcwd()
86
+
87
+ def my_verifier(env):
88
+ return 1.0
89
+ """
90
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
91
+ sync_verifier_from_string(
92
+ verifier_func=code,
93
+ verifier_id="test-verifier",
94
+ verifier_key="test-key",
95
+ sha256="test-sha",
96
+ )
97
+
98
+ def test_allows_constant_assignment(self):
99
+ """Test that constant variable assignments are allowed."""
100
+ code = """
101
+ CONSTANT_VALUE = 42
102
+ ANOTHER_CONSTANT = "test"
103
+ PI = 3.14159
104
+
105
+ def my_verifier(env):
106
+ return CONSTANT_VALUE
107
+ """
108
+ # Should not raise
109
+ verifier = sync_verifier_from_string(
110
+ verifier_func=code,
111
+ verifier_id="test-verifier",
112
+ verifier_key="test-key",
113
+ sha256="test-sha",
114
+ )
115
+ assert verifier is not None
116
+
117
+ def test_allows_list_dict_constant_assignment(self):
118
+ """Test that list/dict constant assignments are allowed."""
119
+ code = """
120
+ MY_LIST = [1, 2, 3]
121
+ MY_DICT = {"key": "value"}
122
+ MY_TUPLE = (1, 2, 3)
123
+
124
+ def my_verifier(env):
125
+ return 1.0
126
+ """
127
+ # Should not raise
128
+ verifier = sync_verifier_from_string(
129
+ verifier_func=code,
130
+ verifier_id="test-verifier",
131
+ verifier_key="test-key",
132
+ sha256="test-sha",
133
+ )
134
+ assert verifier is not None
135
+
136
+ def test_allows_valid_imports(self):
137
+ """Test that imports are allowed."""
138
+ code = """
139
+ import json
140
+ import os
141
+ from typing import Dict
142
+
143
+ def my_verifier(env):
144
+ return 1.0
145
+ """
146
+ # Should not raise
147
+ verifier = sync_verifier_from_string(
148
+ verifier_func=code,
149
+ verifier_id="test-verifier",
150
+ verifier_key="test-key",
151
+ sha256="test-sha",
152
+ )
153
+ assert verifier is not None
154
+
155
+ def test_allows_class_definitions(self):
156
+ """Test that class definitions are allowed."""
157
+ code = """
158
+ class MyHelper:
159
+ def __init__(self):
160
+ self.value = 42
161
+
162
+ def get_value(self):
163
+ return self.value
164
+
165
+ def my_verifier(env):
166
+ helper = MyHelper()
167
+ return helper.get_value()
168
+ """
169
+ # Should not raise
170
+ verifier = sync_verifier_from_string(
171
+ verifier_func=code,
172
+ verifier_id="test-verifier",
173
+ verifier_key="test-key",
174
+ sha256="test-sha",
175
+ )
176
+ assert verifier is not None
177
+
178
+ def test_allows_multiple_functions(self):
179
+ """Test that multiple function definitions are allowed."""
180
+ code = """
181
+ def helper_function(x):
182
+ return x * 2
183
+
184
+ def my_verifier(env):
185
+ return helper_function(0.5)
186
+ """
187
+ # Should not raise
188
+ verifier = sync_verifier_from_string(
189
+ verifier_func=code,
190
+ verifier_id="test-verifier",
191
+ verifier_key="test-key",
192
+ sha256="test-sha",
193
+ )
194
+ assert verifier is not None
195
+
196
+ def test_extracts_first_function_name(self):
197
+ """Test that the first function name is correctly extracted."""
198
+ code = """
199
+ def first_function(env):
200
+ return 1.0
201
+
202
+ def second_function(env):
203
+ return 0.5
204
+ """
205
+ verifier = sync_verifier_from_string(
206
+ verifier_func=code,
207
+ verifier_id="test-verifier",
208
+ verifier_key="test-key",
209
+ sha256="test-sha",
210
+ )
211
+ # The first function should be used
212
+ assert verifier.func.__name__ == "first_function"
213
+
214
+ def test_error_message_includes_line_number(self):
215
+ """Test that error messages include helpful line numbers."""
216
+ code = """
217
+ import subprocess
218
+
219
+ subprocess.run(['echo', 'test'])
220
+
221
+ def my_verifier(env):
222
+ return 1.0
223
+ """
224
+ with pytest.raises(ValueError, match=r"Line \d+"):
225
+ sync_verifier_from_string(
226
+ verifier_func=code,
227
+ verifier_id="test-verifier",
228
+ verifier_key="test-key",
229
+ sha256="test-sha",
230
+ )
231
+
232
+ def test_blocks_nested_function_call_in_list(self):
233
+ """Test that function calls nested in list assignments are blocked."""
234
+ code = """
235
+ import os
236
+ MY_LIST = [1, 2, os.getcwd()]
237
+
238
+ def my_verifier(env):
239
+ return 1.0
240
+ """
241
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
242
+ sync_verifier_from_string(
243
+ verifier_func=code,
244
+ verifier_id="test-verifier",
245
+ verifier_key="test-key",
246
+ sha256="test-sha",
247
+ )
248
+
249
+ def test_blocks_nested_function_call_in_dict(self):
250
+ """Test that function calls nested in dict assignments are blocked."""
251
+ code = """
252
+ import os
253
+ MY_DICT = {"cwd": os.getcwd()}
254
+
255
+ def my_verifier(env):
256
+ return 1.0
257
+ """
258
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
259
+ sync_verifier_from_string(
260
+ verifier_func=code,
261
+ verifier_id="test-verifier",
262
+ verifier_key="test-key",
263
+ sha256="test-sha",
264
+ )
265
+
266
+ def test_allows_docstrings(self):
267
+ """Test that module-level docstrings are allowed."""
268
+ code = '''
269
+ """This is a module docstring."""
270
+
271
+ def my_verifier(env):
272
+ """This is a function docstring."""
273
+ return 1.0
274
+ '''
275
+ # Should not raise
276
+ verifier = sync_verifier_from_string(
277
+ verifier_func=code,
278
+ verifier_id="test-verifier",
279
+ verifier_key="test-key",
280
+ sha256="test-sha",
281
+ )
282
+ assert verifier is not None
283
+
284
+ def test_function_with_decorator_extracts_correct_name(self):
285
+ """Test that decorators don't affect function name extraction."""
286
+ code = """
287
+ def some_decorator(func):
288
+ return func
289
+
290
+ @some_decorator
291
+ def my_actual_function(env):
292
+ return 1.0
293
+ """
294
+ verifier = sync_verifier_from_string(
295
+ verifier_func=code,
296
+ verifier_id="test-verifier",
297
+ verifier_key="test-key",
298
+ sha256="test-sha",
299
+ )
300
+ # Should extract 'some_decorator' (first function) or 'my_actual_function'
301
+ # depending on order, but NOT the decorator name itself
302
+ assert verifier.func.__name__ in ["some_decorator", "my_actual_function"]
303
+
304
+ def test_blocks_decorator_with_function_call(self):
305
+ """Test that decorators with function calls are blocked."""
306
+ code = """
307
+ import subprocess
308
+
309
+ @subprocess.run(['echo', 'bad'])
310
+ def my_verifier(env):
311
+ return 1.0
312
+ """
313
+ # Decorators execute during import, so calls in decorators are dangerous
314
+ with pytest.raises(ValueError, match="Function decorators with function calls"):
315
+ sync_verifier_from_string(
316
+ verifier_func=code,
317
+ verifier_id="test-verifier",
318
+ verifier_key="test-key",
319
+ sha256="test-sha",
320
+ )
321
+
322
+ def test_allows_simple_decorator_reference(self):
323
+ """Test that simple decorator references (no calls) are allowed."""
324
+ code = """
325
+ def my_decorator(func):
326
+ return func
327
+
328
+ @my_decorator
329
+ def my_verifier(env):
330
+ return 1.0
331
+ """
332
+ # Simple decorator reference (no call) should be allowed
333
+ verifier = sync_verifier_from_string(
334
+ verifier_func=code,
335
+ verifier_id="test-verifier",
336
+ verifier_key="test-key",
337
+ sha256="test-sha",
338
+ )
339
+ assert verifier is not None
340
+
341
+
342
+ class TestAsyncVerifierSecurity:
343
+ """Security tests for async version of verifier_from_string."""
344
+
345
+ def test_blocks_module_level_subprocess_run(self):
346
+ """Test that module-level subprocess.run() is blocked."""
347
+ code = """
348
+ import subprocess
349
+ subprocess.run(['echo', 'malicious'])
350
+
351
+ async def my_async_verifier(env):
352
+ return 1.0
353
+ """
354
+ with pytest.raises(ValueError, match="Expression statements that are not constants"):
355
+ async_verifier_from_string(
356
+ verifier_func=code,
357
+ verifier_id="test-verifier",
358
+ verifier_key="test-key",
359
+ sha256="test-sha",
360
+ )
361
+
362
+ def test_blocks_assignment_with_function_call(self):
363
+ """Test that variable assignment with function call is blocked."""
364
+ code = """
365
+ import subprocess
366
+ result = subprocess.run(['echo', 'malicious'])
367
+
368
+ async def my_async_verifier(env):
369
+ return 1.0
370
+ """
371
+ with pytest.raises(ValueError, match="Variable assignments with function calls"):
372
+ async_verifier_from_string(
373
+ verifier_func=code,
374
+ verifier_id="test-verifier",
375
+ verifier_key="test-key",
376
+ sha256="test-sha",
377
+ )
378
+
379
+ def test_allows_constant_assignment(self):
380
+ """Test that constant variable assignments are allowed."""
381
+ code = """
382
+ CONSTANT_VALUE = 42
383
+
384
+ async def my_async_verifier(env):
385
+ return CONSTANT_VALUE
386
+ """
387
+ # Should not raise
388
+ verifier = async_verifier_from_string(
389
+ verifier_func=code,
390
+ verifier_id="test-verifier",
391
+ verifier_key="test-key",
392
+ sha256="test-sha",
393
+ )
394
+ assert verifier is not None
395
+
396
+ def test_allows_async_function_definitions(self):
397
+ """Test that async function definitions are recognized."""
398
+ code = """
399
+ async def my_async_verifier(env):
400
+ return 1.0
401
+ """
402
+ # Should not raise
403
+ verifier = async_verifier_from_string(
404
+ verifier_func=code,
405
+ verifier_id="test-verifier",
406
+ verifier_key="test-key",
407
+ sha256="test-sha",
408
+ )
409
+ assert verifier is not None
410
+
411
+ def test_extracts_first_async_function_name(self):
412
+ """Test that the first async function name is correctly extracted."""
413
+ code = """
414
+ async def first_async_function(env):
415
+ return 1.0
416
+
417
+ async def second_async_function(env):
418
+ return 0.5
419
+ """
420
+ verifier = async_verifier_from_string(
421
+ verifier_func=code,
422
+ verifier_id="test-verifier",
423
+ verifier_key="test-key",
424
+ sha256="test-sha",
425
+ )
426
+ # The first function should be used
427
+ assert verifier.func.__name__ == "first_async_function"
File without changes
File without changes
File without changes