fleet-python 0.2.65__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.
- {fleet_python-0.2.65/fleet_python.egg-info → fleet_python-0.2.66b2}/PKG-INFO +1 -1
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/tasks.py +7 -9
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/tasks.py +7 -9
- {fleet_python-0.2.65 → fleet_python-0.2.66b2/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet_python.egg-info/SOURCES.txt +2 -1
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/pyproject.toml +1 -1
- fleet_python-0.2.66b2/tests/test_verifier_security.py +427 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/LICENSE +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/README.md +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/diff_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_account.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_sync.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_task.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/openai_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/quickstart.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/config.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/env/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/global_client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/models.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/types.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/scripts/unasync.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/setup.cfg +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/tests/__init__.py +0 -0
- {fleet_python-0.2.65 → fleet_python-0.2.66b2}/tests/test_verifier_from_string.py +0 -0
|
@@ -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
|
-
#
|
|
288
|
-
#
|
|
289
|
-
|
|
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
|
|
303
|
-
|
|
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>'
|
|
@@ -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
|
-
#
|
|
281
|
-
#
|
|
282
|
-
|
|
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
|
|
302
|
-
|
|
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>'
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|