onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__py3-none-any.whl
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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/executor/validator.py
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Validates Python code before execution:
|
|
4
4
|
- Syntax validation via ast.parse()
|
|
5
|
-
-
|
|
5
|
+
- Allowlist-based security validation (dangerous calls, imports, builtins)
|
|
6
6
|
- Optional Ruff linting integration for style warnings
|
|
7
7
|
|
|
8
|
-
Security
|
|
8
|
+
Security Model:
|
|
9
|
+
Block everything by default, explicitly allow what's safe.
|
|
10
|
+
Configuration via security.yaml with three categories:
|
|
11
|
+
- builtins: Allowed builtin functions and types
|
|
12
|
+
- imports: Allowed modules for import statements
|
|
13
|
+
- calls: Blocked/warned qualified function calls
|
|
9
14
|
|
|
15
|
+
Tool namespaces (ot.*, brave.*, etc.) are auto-allowed.
|
|
16
|
+
|
|
17
|
+
Configuration:
|
|
10
18
|
security:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* Import statements: import subprocess, from os import system
|
|
20
|
-
- Patterns WITH dots (e.g., 'subprocess.*', 'os.system') match:
|
|
21
|
-
* Qualified function calls: subprocess.run(), os.system()
|
|
19
|
+
builtins:
|
|
20
|
+
allow: [str, int, list, print, ...]
|
|
21
|
+
imports:
|
|
22
|
+
allow: [json, re, math, ...]
|
|
23
|
+
warn: [yaml]
|
|
24
|
+
calls:
|
|
25
|
+
block: [pickle.*, yaml.load]
|
|
26
|
+
warn: [random.seed]
|
|
22
27
|
|
|
23
28
|
Wildcard patterns use fnmatch syntax:
|
|
24
29
|
- '*' matches any characters (e.g., 'subprocess.*' matches 'subprocess.run')
|
|
@@ -37,8 +42,10 @@ from __future__ import annotations
|
|
|
37
42
|
|
|
38
43
|
import ast
|
|
39
44
|
import fnmatch
|
|
45
|
+
import sys
|
|
40
46
|
from dataclasses import dataclass, field
|
|
41
|
-
from
|
|
47
|
+
from functools import lru_cache
|
|
48
|
+
from typing import TYPE_CHECKING, Any
|
|
42
49
|
|
|
43
50
|
if TYPE_CHECKING:
|
|
44
51
|
from ot.config.loader import SecurityConfig
|
|
@@ -55,47 +62,43 @@ class ValidationResult:
|
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
# =============================================================================
|
|
58
|
-
#
|
|
59
|
-
# =============================================================================
|
|
60
|
-
# These are used when config is not available. Patterns use fnmatch wildcards.
|
|
61
|
-
#
|
|
62
|
-
# Pattern matching is determined by structure:
|
|
63
|
-
# - No dot in pattern → matches builtins (calls) AND imports
|
|
64
|
-
# - Dot in pattern → matches qualified function calls only
|
|
65
|
-
#
|
|
66
|
-
# This two-category design (blocked/warned) replaces the previous four-category
|
|
67
|
-
# design (blocked_builtins, blocked_functions, warned_functions, warned_imports).
|
|
68
|
-
# The validator automatically determines the match type based on pattern structure.
|
|
65
|
+
# Fallback Defaults (used when config is unavailable)
|
|
69
66
|
# =============================================================================
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
67
|
+
# These are minimal safe defaults used only when security.yaml cannot be loaded.
|
|
68
|
+
# In normal operation, the full allowlists come from security.yaml.
|
|
69
|
+
|
|
70
|
+
FALLBACK_ALLOWED_BUILTINS = frozenset({
|
|
71
|
+
# Type constructors
|
|
72
|
+
"str", "int", "float", "bool", "bytes", "list", "dict", "set", "tuple",
|
|
73
|
+
# Type checking
|
|
74
|
+
"isinstance", "issubclass", "type", "callable",
|
|
75
|
+
# Iteration
|
|
76
|
+
"len", "iter", "next", "range", "enumerate", "zip", "reversed", "sorted",
|
|
77
|
+
# Math
|
|
78
|
+
"min", "max", "sum", "abs", "round", "pow",
|
|
79
|
+
# Sequence operations
|
|
80
|
+
"all", "any", "filter", "map",
|
|
81
|
+
# String/repr
|
|
82
|
+
"repr", "format", "print",
|
|
83
|
+
# Exceptions
|
|
84
|
+
"Exception", "ValueError", "TypeError", "KeyError",
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
FALLBACK_ALLOWED_IMPORTS = frozenset({
|
|
88
|
+
"json", "re", "math", "datetime", "collections", "typing",
|
|
89
|
+
"itertools", "functools", "copy", "dataclasses",
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
FALLBACK_WARNED_IMPORTS = frozenset({
|
|
93
|
+
"yaml",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Cache stdlib module names for performance (checked per qualified call)
|
|
97
|
+
# Python 3.10+ has sys.stdlib_module_names; older versions get empty set
|
|
98
|
+
try:
|
|
99
|
+
STDLIB_MODULE_NAMES: frozenset[str] = frozenset(sys.stdlib_module_names)
|
|
100
|
+
except AttributeError:
|
|
101
|
+
STDLIB_MODULE_NAMES = frozenset()
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def _matches_pattern(name: str, patterns: frozenset[str]) -> str | None:
|
|
@@ -124,19 +127,12 @@ def _matches_pattern(name: str, patterns: frozenset[str]) -> str | None:
|
|
|
124
127
|
return None
|
|
125
128
|
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
"""Check if a pattern contains a dot (qualified name).
|
|
129
|
-
|
|
130
|
-
Used to determine matching strategy:
|
|
131
|
-
- No dot: match builtins and imports
|
|
132
|
-
- Has dot: match qualified function calls
|
|
133
|
-
"""
|
|
134
|
-
return "." in pattern
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
@lru_cache(maxsize=1)
|
|
137
131
|
def _get_security_config() -> SecurityConfig | None:
|
|
138
132
|
"""Get security configuration from global config.
|
|
139
133
|
|
|
134
|
+
Results are cached for performance. Cache is cleared on ot.reload().
|
|
135
|
+
|
|
140
136
|
Returns:
|
|
141
137
|
SecurityConfig if available, None otherwise.
|
|
142
138
|
"""
|
|
@@ -146,125 +142,392 @@ def _get_security_config() -> SecurityConfig | None:
|
|
|
146
142
|
config = get_config()
|
|
147
143
|
return config.security
|
|
148
144
|
except Exception:
|
|
149
|
-
# Config not loaded yet or error - use defaults
|
|
145
|
+
# Config not loaded yet or error - use fallback defaults
|
|
150
146
|
return None
|
|
151
147
|
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
@lru_cache(maxsize=1)
|
|
150
|
+
def _get_tool_namespaces() -> frozenset[str]:
|
|
151
|
+
"""Get all tool pack namespaces for auto-allow.
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
Tool namespaces (ot.*, brave.*, file.*, etc.) are auto-allowed
|
|
154
|
+
since they're the whole point of OneTool.
|
|
158
155
|
|
|
159
|
-
|
|
160
|
-
- Patterns without dots match builtins and imports
|
|
161
|
-
- Patterns with dots match qualified function calls
|
|
156
|
+
Results are cached for performance (registry doesn't change during session).
|
|
162
157
|
|
|
163
|
-
|
|
158
|
+
Returns:
|
|
159
|
+
Frozenset of namespace patterns like "ot.*", "brave.*"
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
from ot.executor.tool_loader import load_tool_registry
|
|
163
|
+
from ot.proxy import get_proxy_manager
|
|
164
|
+
|
|
165
|
+
registry = load_tool_registry()
|
|
166
|
+
proxy = get_proxy_manager()
|
|
167
|
+
|
|
168
|
+
# Collect all pack names
|
|
169
|
+
namespaces = set(registry.packs.keys())
|
|
170
|
+
namespaces.update(proxy.servers)
|
|
171
|
+
|
|
172
|
+
# Convert to wildcard patterns
|
|
173
|
+
return frozenset(f"{ns}.*" for ns in namespaces)
|
|
174
|
+
except Exception:
|
|
175
|
+
# Registry not loaded - return minimal set
|
|
176
|
+
return frozenset({"ot.*"})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AllowlistValidator(ast.NodeVisitor):
|
|
180
|
+
"""AST visitor that validates code against allowlists.
|
|
181
|
+
|
|
182
|
+
Security model: Block everything by default, allow what's explicitly listed.
|
|
183
|
+
Tool namespaces are auto-allowed.
|
|
184
|
+
|
|
185
|
+
Categories:
|
|
186
|
+
- builtins: Allowed builtin function calls
|
|
187
|
+
- imports: Allowed module imports
|
|
188
|
+
- calls: Blocked/warned qualified function calls
|
|
189
|
+
|
|
190
|
+
Tracks import aliases and from-imports to prevent bypass attacks:
|
|
191
|
+
- `import subprocess as sp; sp.run()` - alias tracked
|
|
192
|
+
- `from subprocess import run; run()` - function tracked
|
|
164
193
|
"""
|
|
165
194
|
|
|
166
195
|
def __init__(
|
|
167
196
|
self,
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
allowed_builtins: frozenset[str],
|
|
198
|
+
allowed_imports: frozenset[str],
|
|
199
|
+
warned_imports: frozenset[str],
|
|
200
|
+
blocked_calls: frozenset[str],
|
|
201
|
+
warned_calls: frozenset[str],
|
|
202
|
+
allowed_calls: frozenset[str],
|
|
203
|
+
tool_namespaces: frozenset[str],
|
|
204
|
+
allowed_dunders: frozenset[str],
|
|
170
205
|
) -> None:
|
|
171
|
-
"""Initialize
|
|
206
|
+
"""Initialize validator with allowlists.
|
|
172
207
|
|
|
173
208
|
Args:
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
allowed_builtins: Allowed builtin functions and types
|
|
210
|
+
allowed_imports: Allowed module imports
|
|
211
|
+
warned_imports: Imports that trigger warnings but are allowed
|
|
212
|
+
blocked_calls: Blocked qualified function calls
|
|
213
|
+
warned_calls: Qualified calls that trigger warnings
|
|
214
|
+
allowed_calls: Explicitly allowed qualified calls
|
|
215
|
+
tool_namespaces: Auto-allowed tool namespace patterns (e.g., "ot.*")
|
|
216
|
+
allowed_dunders: Allowed magic variables (e.g., "__format__")
|
|
176
217
|
"""
|
|
177
218
|
self.errors: list[str] = []
|
|
178
219
|
self.warnings: list[str] = []
|
|
179
220
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
self.allowed_builtins = allowed_builtins
|
|
222
|
+
self.allowed_imports = allowed_imports
|
|
223
|
+
self.warned_imports = warned_imports
|
|
224
|
+
self.blocked_calls = blocked_calls
|
|
225
|
+
self.warned_calls = warned_calls
|
|
226
|
+
self.allowed_calls = allowed_calls
|
|
227
|
+
self.tool_namespaces = tool_namespaces
|
|
228
|
+
self.allowed_dunders = allowed_dunders
|
|
229
|
+
|
|
230
|
+
# Track import aliases: alias_name -> original_module
|
|
231
|
+
# e.g., "import subprocess as sp" -> {"sp": "subprocess"}
|
|
232
|
+
self._import_aliases: dict[str, str] = {}
|
|
233
|
+
|
|
234
|
+
# Track from-imports from blocked modules: function_name -> original_module
|
|
235
|
+
# e.g., "from subprocess import run" -> {"run": "subprocess"}
|
|
236
|
+
self._from_imports: dict[str, str] = {}
|
|
183
237
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
238
|
+
def _is_tool_namespace(self, name: str) -> bool:
|
|
239
|
+
"""Check if a qualified name is in an auto-allowed tool namespace."""
|
|
240
|
+
return _matches_pattern(name, self.tool_namespaces) is not None
|
|
241
|
+
|
|
242
|
+
def _is_allowed_dunder(self, name: str) -> bool:
|
|
243
|
+
"""Check if a name is an allowed magic variable."""
|
|
244
|
+
return name in self.allowed_dunders
|
|
191
245
|
|
|
192
246
|
def visit_Call(self, node: ast.Call) -> None:
|
|
193
|
-
"""Check function calls
|
|
247
|
+
"""Check function calls against allowlists."""
|
|
248
|
+
# First check for __builtins__ bypass via getattr/hasattr
|
|
249
|
+
if self._is_builtins_bypass_call(node):
|
|
250
|
+
self.errors.append(
|
|
251
|
+
f"Line {node.lineno}: Access to '__builtins__' via getattr/hasattr is not allowed "
|
|
252
|
+
f"(potential security bypass)"
|
|
253
|
+
)
|
|
254
|
+
self.generic_visit(node)
|
|
255
|
+
return
|
|
194
256
|
|
|
195
|
-
Priority order: blocked > warned (allow handled at setup)
|
|
196
|
-
"""
|
|
197
257
|
func_name = self._get_call_name(node)
|
|
198
258
|
|
|
199
259
|
if not func_name:
|
|
200
260
|
self.generic_visit(node)
|
|
201
261
|
return
|
|
202
262
|
|
|
203
|
-
# Determine which pattern sets to check based on call type
|
|
204
263
|
is_qualified = "." in func_name
|
|
205
264
|
|
|
206
265
|
if is_qualified:
|
|
207
|
-
# Qualified call (e.g., subprocess.run)
|
|
208
|
-
|
|
209
|
-
|
|
266
|
+
# Qualified call (e.g., json.loads, subprocess.run)
|
|
267
|
+
self._check_qualified_call(func_name, node.lineno)
|
|
268
|
+
else:
|
|
269
|
+
# Simple call (builtin like print, len, etc.)
|
|
270
|
+
self._check_builtin_call(func_name, node.lineno)
|
|
271
|
+
|
|
272
|
+
self.generic_visit(node)
|
|
273
|
+
|
|
274
|
+
def _is_builtins_bypass_call(self, node: ast.Call) -> bool:
|
|
275
|
+
"""Check if this is an attempt to bypass via getattr(__builtins__, ...).
|
|
276
|
+
|
|
277
|
+
Detects patterns like:
|
|
278
|
+
- getattr(__builtins__, 'exec')
|
|
279
|
+
- hasattr(__builtins__, 'exec')
|
|
280
|
+
"""
|
|
281
|
+
func_name = self._get_call_name(node)
|
|
282
|
+
if func_name not in ("getattr", "hasattr"):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
# Check if first argument is __builtins__
|
|
286
|
+
return (
|
|
287
|
+
bool(node.args)
|
|
288
|
+
and isinstance(node.args[0], ast.Name)
|
|
289
|
+
and node.args[0].id == "__builtins__"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _check_builtin_call(self, func_name: str, lineno: int) -> None:
|
|
293
|
+
"""Check if a builtin call is allowed.
|
|
294
|
+
|
|
295
|
+
Also checks for from-import bypass:
|
|
296
|
+
`from subprocess import run; run()` - run is tracked as from subprocess
|
|
297
|
+
|
|
298
|
+
Only checks known dangerous builtins. User-defined functions
|
|
299
|
+
(like functions defined in the same code) are allowed.
|
|
300
|
+
"""
|
|
301
|
+
# Check for from-import bypass first
|
|
302
|
+
# e.g., "from subprocess import run; run()" -> func_name is "run"
|
|
303
|
+
if func_name in self._from_imports:
|
|
304
|
+
source_module = self._from_imports[func_name]
|
|
305
|
+
# Check if the source module is allowed
|
|
306
|
+
if _matches_pattern(source_module, self.allowed_imports) is None:
|
|
210
307
|
self.errors.append(
|
|
211
|
-
f"Line {
|
|
212
|
-
f"
|
|
308
|
+
f"Line {lineno}: Call to '{func_name}' is not allowed "
|
|
309
|
+
f"(imported from blocked module '{source_module}'). "
|
|
310
|
+
f"Use ot.security(check='{source_module}') to check security rules."
|
|
213
311
|
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
312
|
+
return
|
|
313
|
+
# Module is allowed, check if specific call is blocked
|
|
314
|
+
qualified_name = f"{source_module}.{func_name}"
|
|
315
|
+
if pattern := _matches_pattern(qualified_name, self.blocked_calls):
|
|
316
|
+
self.errors.append(
|
|
317
|
+
f"Line {lineno}: Call to '{func_name}' is blocked "
|
|
318
|
+
f"(matches '{pattern}' via 'from {source_module} import'). "
|
|
319
|
+
f"Use ot.security(check='{qualified_name}') to check security rules."
|
|
218
320
|
)
|
|
321
|
+
return
|
|
322
|
+
# Allowed module, not blocked call - allow
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# Allowed dunders pass through
|
|
326
|
+
if self._is_allowed_dunder(func_name):
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Check against allowed builtins
|
|
330
|
+
if _matches_pattern(func_name, self.allowed_builtins) is not None:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Check if this is a known dangerous builtin that we explicitly block
|
|
334
|
+
# User-defined functions (not in Python's builtins) are allowed
|
|
335
|
+
try:
|
|
336
|
+
import builtins
|
|
337
|
+
if not hasattr(builtins, func_name):
|
|
338
|
+
# Not a builtin - allow user-defined functions
|
|
339
|
+
return
|
|
340
|
+
except (ImportError, AttributeError):
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# Not allowed - block
|
|
344
|
+
self.errors.append(
|
|
345
|
+
f"Line {lineno}: Builtin '{func_name}' is not allowed. "
|
|
346
|
+
f"Use ot.security(check='{func_name}') to check security rules."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _check_qualified_call(self, func_name: str, lineno: int) -> None:
|
|
350
|
+
"""Check if a qualified call is allowed.
|
|
351
|
+
|
|
352
|
+
Also resolves import aliases:
|
|
353
|
+
`import subprocess as sp; sp.run()` -> resolves sp to subprocess
|
|
354
|
+
|
|
355
|
+
Only checks known dangerous patterns. Method calls on variables
|
|
356
|
+
(like results.append()) are allowed by default since we can't
|
|
357
|
+
statically determine if 'results' is a dangerous module.
|
|
358
|
+
"""
|
|
359
|
+
# Resolve import aliases first
|
|
360
|
+
# e.g., "import subprocess as sp; sp.run()" -> func_name is "sp.run"
|
|
361
|
+
# We need to resolve "sp" to "subprocess"
|
|
362
|
+
parts = func_name.split(".")
|
|
363
|
+
module_part = parts[0]
|
|
364
|
+
original_module = self._import_aliases.get(module_part, module_part)
|
|
365
|
+
|
|
366
|
+
# If alias was resolved, reconstruct the full name
|
|
367
|
+
if original_module != module_part:
|
|
368
|
+
resolved_name = ".".join([original_module, *parts[1:]])
|
|
219
369
|
else:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
370
|
+
resolved_name = func_name
|
|
371
|
+
|
|
372
|
+
# Tool namespaces are auto-allowed
|
|
373
|
+
if self._is_tool_namespace(resolved_name):
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
# Explicitly allowed calls pass through
|
|
377
|
+
if _matches_pattern(resolved_name, self.allowed_calls) is not None:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Check blocked patterns first - these are explicitly dangerous
|
|
381
|
+
if pattern := _matches_pattern(resolved_name, self.blocked_calls):
|
|
382
|
+
if original_module != module_part:
|
|
383
|
+
# Alias was used - include both in error message
|
|
223
384
|
self.errors.append(
|
|
224
|
-
f"Line {
|
|
225
|
-
f"
|
|
385
|
+
f"Line {lineno}: Call '{func_name}' is blocked "
|
|
386
|
+
f"('{module_part}' is alias for '{original_module}', matches '{pattern}'). "
|
|
387
|
+
f"Use ot.security(check='{resolved_name}') to check security rules."
|
|
226
388
|
)
|
|
227
|
-
|
|
228
|
-
self.
|
|
229
|
-
f"Line {
|
|
230
|
-
f"(
|
|
389
|
+
else:
|
|
390
|
+
self.errors.append(
|
|
391
|
+
f"Line {lineno}: Call '{func_name}' is blocked (matches '{pattern}'). "
|
|
392
|
+
f"Use ot.security(check='{func_name}') to check security rules."
|
|
231
393
|
)
|
|
394
|
+
return
|
|
232
395
|
|
|
233
|
-
#
|
|
234
|
-
self.
|
|
396
|
+
# Check warned patterns
|
|
397
|
+
if pattern := _matches_pattern(resolved_name, self.warned_calls):
|
|
398
|
+
self.warnings.append(
|
|
399
|
+
f"Line {lineno}: Call '{resolved_name}' may be unsafe (matches '{pattern}')"
|
|
400
|
+
)
|
|
401
|
+
return
|
|
235
402
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
#
|
|
240
|
-
|
|
403
|
+
# Check if the module part is in allowed imports
|
|
404
|
+
# e.g., for json.loads, check if "json" is allowed
|
|
405
|
+
if _matches_pattern(original_module, self.allowed_imports) is not None:
|
|
406
|
+
# Module is allowed, so its functions are implicitly allowed
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# For method calls on variables (not known modules), allow by default
|
|
410
|
+
# We can't statically determine if 'results.append()' is dangerous
|
|
411
|
+
# vs 'os.system()' without runtime type information.
|
|
412
|
+
# Only block if explicitly in blocked_calls or if module_part is
|
|
413
|
+
# a known dangerous import that's not allowed.
|
|
414
|
+
#
|
|
415
|
+
# Check if module_part looks like a known stdlib module that
|
|
416
|
+
# should be blocked (e.g., os, subprocess, pickle)
|
|
417
|
+
# Uses cached STDLIB_MODULE_NAMES for performance
|
|
418
|
+
if STDLIB_MODULE_NAMES and original_module in STDLIB_MODULE_NAMES:
|
|
419
|
+
if original_module != module_part:
|
|
241
420
|
self.errors.append(
|
|
242
|
-
f"Line {
|
|
243
|
-
f"(
|
|
421
|
+
f"Line {lineno}: Module '{original_module}' "
|
|
422
|
+
f"(aliased as '{module_part}') is not in allowed imports. "
|
|
423
|
+
f"Use ot.security(check='{original_module}') to check security rules."
|
|
244
424
|
)
|
|
245
|
-
|
|
246
|
-
self.
|
|
247
|
-
f"Line {
|
|
248
|
-
f"
|
|
425
|
+
else:
|
|
426
|
+
self.errors.append(
|
|
427
|
+
f"Line {lineno}: Module '{original_module}' is not in allowed imports. "
|
|
428
|
+
f"Use ot.security(check='{original_module}') to check security rules."
|
|
249
429
|
)
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
# Not a known stdlib module - allow method calls on variables
|
|
433
|
+
|
|
434
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
435
|
+
"""Check import statements against allowlists.
|
|
436
|
+
|
|
437
|
+
Also tracks aliases for bypass prevention:
|
|
438
|
+
`import subprocess as sp` -> tracks sp -> subprocess
|
|
439
|
+
"""
|
|
440
|
+
for alias in node.names:
|
|
441
|
+
module_name = alias.name
|
|
442
|
+
self._check_import(module_name, node.lineno)
|
|
443
|
+
|
|
444
|
+
# Track alias if present (e.g., "import subprocess as sp")
|
|
445
|
+
# This prevents bypass via: import blocked as alias; alias.func()
|
|
446
|
+
if alias.asname:
|
|
447
|
+
self._import_aliases[alias.asname] = module_name
|
|
448
|
+
else:
|
|
449
|
+
# Even without alias, track the module name for consistency
|
|
450
|
+
# e.g., "import subprocess" -> subprocess.run() uses "subprocess"
|
|
451
|
+
self._import_aliases[module_name] = module_name
|
|
452
|
+
|
|
250
453
|
self.generic_visit(node)
|
|
251
454
|
|
|
252
455
|
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
253
|
-
"""Check from imports
|
|
456
|
+
"""Check from imports against allowlists.
|
|
457
|
+
|
|
458
|
+
Also tracks imported names for bypass prevention:
|
|
459
|
+
`from subprocess import run` -> tracks run -> subprocess
|
|
460
|
+
`from subprocess import run as r` -> tracks r -> subprocess
|
|
461
|
+
"""
|
|
254
462
|
if node.module:
|
|
255
|
-
|
|
256
|
-
|
|
463
|
+
self._check_import(node.module, node.lineno)
|
|
464
|
+
|
|
465
|
+
# Track all imported names from this module
|
|
466
|
+
# This prevents bypass via: from blocked import func; func()
|
|
467
|
+
for alias in node.names:
|
|
468
|
+
if alias.name == "*":
|
|
469
|
+
# Star imports are dangerous - we can't track what's imported
|
|
470
|
+
# The module check above will already block if not allowed
|
|
471
|
+
self.warnings.append(
|
|
472
|
+
f"Line {node.lineno}: Star import from '{node.module}' - "
|
|
473
|
+
f"cannot track imported names for security validation"
|
|
474
|
+
)
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Use the alias name if present, otherwise the original name
|
|
478
|
+
local_name = alias.asname if alias.asname else alias.name
|
|
479
|
+
# Map local name -> (module, original_name)
|
|
480
|
+
self._from_imports[local_name] = node.module
|
|
481
|
+
|
|
482
|
+
self.generic_visit(node)
|
|
483
|
+
|
|
484
|
+
def _check_import(self, module_name: str, lineno: int) -> None:
|
|
485
|
+
"""Check if a module import is allowed."""
|
|
486
|
+
# Check allowed imports
|
|
487
|
+
if _matches_pattern(module_name, self.allowed_imports) is not None:
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# Check warned imports
|
|
491
|
+
if pattern := _matches_pattern(module_name, self.warned_imports):
|
|
492
|
+
self.warnings.append(
|
|
493
|
+
f"Line {lineno}: Import of '{module_name}' may enable "
|
|
494
|
+
f"unsafe operations (matches '{pattern}')"
|
|
495
|
+
)
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
# Not allowed - block
|
|
499
|
+
self.errors.append(
|
|
500
|
+
f"Line {lineno}: Import of '{module_name}' is not allowed. "
|
|
501
|
+
f"Use ot.security(check='{module_name}') to check security rules."
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
505
|
+
"""Check assignments to magic variables."""
|
|
506
|
+
for target in node.targets:
|
|
507
|
+
if (
|
|
508
|
+
isinstance(target, ast.Name)
|
|
509
|
+
and target.id.startswith("__")
|
|
510
|
+
and target.id.endswith("__")
|
|
511
|
+
and not self._is_allowed_dunder(target.id)
|
|
512
|
+
):
|
|
257
513
|
self.errors.append(
|
|
258
|
-
f"Line {node.lineno}:
|
|
259
|
-
f"(matches '{pattern}')"
|
|
260
|
-
)
|
|
261
|
-
elif pattern := _matches_pattern(node.module, self.warned_simple):
|
|
262
|
-
self.warnings.append(
|
|
263
|
-
f"Line {node.lineno}: Import from '{node.module}' may enable "
|
|
264
|
-
f"dangerous operations (matches '{pattern}')"
|
|
514
|
+
f"Line {node.lineno}: Assignment to '{target.id}' is not allowed"
|
|
265
515
|
)
|
|
266
516
|
self.generic_visit(node)
|
|
267
517
|
|
|
518
|
+
def visit_Subscript(self, node: ast.Subscript) -> None:
|
|
519
|
+
"""Block subscript access to __builtins__.
|
|
520
|
+
|
|
521
|
+
Prevents bypass via: __builtins__['exec']('code')
|
|
522
|
+
"""
|
|
523
|
+
# Check if subscripting __builtins__
|
|
524
|
+
if isinstance(node.value, ast.Name) and node.value.id == "__builtins__":
|
|
525
|
+
self.errors.append(
|
|
526
|
+
f"Line {node.lineno}: Access to '__builtins__' is not allowed "
|
|
527
|
+
f"(potential security bypass)"
|
|
528
|
+
)
|
|
529
|
+
self.generic_visit(node)
|
|
530
|
+
|
|
268
531
|
def _get_call_name(self, node: ast.Call) -> str:
|
|
269
532
|
"""Extract the full name of a function call.
|
|
270
533
|
|
|
@@ -295,9 +558,8 @@ def validate_python_code(
|
|
|
295
558
|
) -> ValidationResult:
|
|
296
559
|
"""Validate Python code for syntax and security issues.
|
|
297
560
|
|
|
298
|
-
Security
|
|
299
|
-
If config is not available,
|
|
300
|
-
Patterns support fnmatch wildcards (*, ?, [seq]).
|
|
561
|
+
Security validation uses allowlist-based rules from security.yaml.
|
|
562
|
+
If config is not available, minimal fallback defaults are used.
|
|
301
563
|
|
|
302
564
|
Args:
|
|
303
565
|
code: Python code to validate
|
|
@@ -320,40 +582,37 @@ def validate_python_code(
|
|
|
320
582
|
result.errors.append(f"Syntax error{line_info}: {e.msg}")
|
|
321
583
|
return result
|
|
322
584
|
|
|
323
|
-
# Step 2: Security
|
|
585
|
+
# Step 2: Security validation
|
|
324
586
|
if check_security:
|
|
325
|
-
# Load patterns from config or use defaults
|
|
326
587
|
security_config = _get_security_config()
|
|
327
588
|
|
|
328
|
-
if security_config is not None and security_config.enabled:
|
|
329
|
-
# Merge config patterns with defaults (additive behavior)
|
|
330
|
-
# This prevents accidental removal of critical security patterns
|
|
331
|
-
allow_set = frozenset(security_config.allow)
|
|
332
|
-
warned_set = frozenset(security_config.warned)
|
|
333
|
-
|
|
334
|
-
# Pattern priority (highest to lowest):
|
|
335
|
-
# 1. allow - completely exempt, no action
|
|
336
|
-
# 2. warned (user) - downgrades blocked defaults to warnings
|
|
337
|
-
# 3. blocked - prevents execution
|
|
338
|
-
# 4. warned (default) - generates warnings
|
|
339
|
-
#
|
|
340
|
-
# This lets users:
|
|
341
|
-
# - Add to blocked/warned (extends defaults)
|
|
342
|
-
# - Downgrade blocked→warned (e.g., warned: [os.popen])
|
|
343
|
-
# - Exempt entirely (e.g., allow: [open])
|
|
344
|
-
blocked = (DEFAULT_BLOCKED | set(security_config.blocked)) - allow_set - warned_set
|
|
345
|
-
warned = (DEFAULT_WARNED | set(security_config.warned)) - allow_set
|
|
346
|
-
|
|
347
|
-
visitor = DangerousPatternVisitor(
|
|
348
|
-
blocked=frozenset(blocked),
|
|
349
|
-
warned=frozenset(warned),
|
|
350
|
-
)
|
|
351
|
-
elif security_config is not None and not security_config.enabled:
|
|
589
|
+
if security_config is not None and not security_config.enabled:
|
|
352
590
|
# Security disabled in config - skip validation
|
|
353
591
|
visitor = None
|
|
592
|
+
elif security_config is not None:
|
|
593
|
+
# Use config-driven allowlists
|
|
594
|
+
visitor = AllowlistValidator(
|
|
595
|
+
allowed_builtins=security_config.get_allowed_builtins(),
|
|
596
|
+
allowed_imports=security_config.get_allowed_imports(),
|
|
597
|
+
warned_imports=security_config.get_warned_imports(),
|
|
598
|
+
blocked_calls=security_config.get_blocked_calls(),
|
|
599
|
+
warned_calls=security_config.get_warned_calls(),
|
|
600
|
+
allowed_calls=security_config.get_allowed_calls(),
|
|
601
|
+
tool_namespaces=_get_tool_namespaces(),
|
|
602
|
+
allowed_dunders=security_config.get_allowed_dunders(),
|
|
603
|
+
)
|
|
354
604
|
else:
|
|
355
|
-
# No config - use defaults
|
|
356
|
-
visitor =
|
|
605
|
+
# No config - use fallback defaults
|
|
606
|
+
visitor = AllowlistValidator(
|
|
607
|
+
allowed_builtins=FALLBACK_ALLOWED_BUILTINS,
|
|
608
|
+
allowed_imports=FALLBACK_ALLOWED_IMPORTS,
|
|
609
|
+
warned_imports=FALLBACK_WARNED_IMPORTS,
|
|
610
|
+
blocked_calls=frozenset(),
|
|
611
|
+
warned_calls=frozenset(),
|
|
612
|
+
allowed_calls=frozenset(),
|
|
613
|
+
tool_namespaces=_get_tool_namespaces(),
|
|
614
|
+
allowed_dunders=frozenset({"__format__", "__sanitize__"}),
|
|
615
|
+
)
|
|
357
616
|
|
|
358
617
|
if visitor is not None:
|
|
359
618
|
visitor.visit(tree)
|
|
@@ -396,3 +655,183 @@ def validate_for_exec(code: str) -> ValidationResult:
|
|
|
396
655
|
# For example, checking for top-level returns outside functions
|
|
397
656
|
|
|
398
657
|
return result
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
# =============================================================================
|
|
661
|
+
# Security Introspection (used by ot.security())
|
|
662
|
+
# =============================================================================
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def get_security_status(pattern: str) -> dict[str, str | bool]:
|
|
666
|
+
"""Check the security status of a specific pattern.
|
|
667
|
+
|
|
668
|
+
Used by ot.security(check=pattern) for agent introspection.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
pattern: Pattern to check (e.g., "os", "json.loads", "pickle.*")
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Dict with 'pattern', 'status' (allowed/blocked/warned), 'category',
|
|
675
|
+
and 'reason' explaining why.
|
|
676
|
+
"""
|
|
677
|
+
security_config = _get_security_config()
|
|
678
|
+
tool_namespaces = _get_tool_namespaces()
|
|
679
|
+
|
|
680
|
+
is_qualified = "." in pattern
|
|
681
|
+
|
|
682
|
+
if is_qualified:
|
|
683
|
+
# Check if it's a tool namespace
|
|
684
|
+
if _matches_pattern(pattern, tool_namespaces):
|
|
685
|
+
namespace = pattern.split(".")[0]
|
|
686
|
+
return {
|
|
687
|
+
"pattern": pattern,
|
|
688
|
+
"status": "allowed",
|
|
689
|
+
"category": "tool_namespace",
|
|
690
|
+
"reason": f"Tool namespace '{namespace}' is auto-allowed",
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if security_config:
|
|
694
|
+
# Check explicitly allowed calls
|
|
695
|
+
if _matches_pattern(pattern, security_config.get_allowed_calls()):
|
|
696
|
+
return {
|
|
697
|
+
"pattern": pattern,
|
|
698
|
+
"status": "allowed",
|
|
699
|
+
"category": "calls",
|
|
700
|
+
"reason": "Explicitly allowed in security.yaml calls.allow",
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
# Check blocked calls
|
|
704
|
+
if match := _matches_pattern(pattern, security_config.get_blocked_calls()):
|
|
705
|
+
return {
|
|
706
|
+
"pattern": pattern,
|
|
707
|
+
"status": "blocked",
|
|
708
|
+
"category": "calls",
|
|
709
|
+
"reason": f"Blocked by pattern '{match}' in security.yaml calls.block",
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
# Check warned calls
|
|
713
|
+
if match := _matches_pattern(pattern, security_config.get_warned_calls()):
|
|
714
|
+
return {
|
|
715
|
+
"pattern": pattern,
|
|
716
|
+
"status": "warned",
|
|
717
|
+
"category": "calls",
|
|
718
|
+
"reason": f"Warned by pattern '{match}' in security.yaml calls.warn",
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
# Check if module part is allowed
|
|
722
|
+
module_part = pattern.split(".")[0]
|
|
723
|
+
if _matches_pattern(module_part, security_config.get_allowed_imports()):
|
|
724
|
+
return {
|
|
725
|
+
"pattern": pattern,
|
|
726
|
+
"status": "allowed",
|
|
727
|
+
"category": "imports",
|
|
728
|
+
"reason": f"Module '{module_part}' is in allowed imports",
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
# Default: blocked
|
|
732
|
+
return {
|
|
733
|
+
"pattern": pattern,
|
|
734
|
+
"status": "blocked",
|
|
735
|
+
"category": "calls",
|
|
736
|
+
"reason": "Not in any allowlist (default: block)",
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
else:
|
|
740
|
+
# Unqualified name - check builtins and imports
|
|
741
|
+
if security_config:
|
|
742
|
+
# Check allowed builtins
|
|
743
|
+
if _matches_pattern(pattern, security_config.get_allowed_builtins()):
|
|
744
|
+
return {
|
|
745
|
+
"pattern": pattern,
|
|
746
|
+
"status": "allowed",
|
|
747
|
+
"category": "builtins",
|
|
748
|
+
"reason": "In security.yaml builtins.allow",
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
# Check allowed imports
|
|
752
|
+
if _matches_pattern(pattern, security_config.get_allowed_imports()):
|
|
753
|
+
return {
|
|
754
|
+
"pattern": pattern,
|
|
755
|
+
"status": "allowed",
|
|
756
|
+
"category": "imports",
|
|
757
|
+
"reason": "In security.yaml imports.allow",
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
# Check warned imports
|
|
761
|
+
if match := _matches_pattern(pattern, security_config.get_warned_imports()):
|
|
762
|
+
return {
|
|
763
|
+
"pattern": pattern,
|
|
764
|
+
"status": "warned",
|
|
765
|
+
"category": "imports",
|
|
766
|
+
"reason": f"Warned by pattern '{match}' in security.yaml imports.warn",
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
# Check allowed dunders
|
|
770
|
+
if pattern in security_config.get_allowed_dunders():
|
|
771
|
+
return {
|
|
772
|
+
"pattern": pattern,
|
|
773
|
+
"status": "allowed",
|
|
774
|
+
"category": "dunders",
|
|
775
|
+
"reason": "In security.yaml dunders.allow",
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# Default: blocked
|
|
779
|
+
return {
|
|
780
|
+
"pattern": pattern,
|
|
781
|
+
"status": "blocked",
|
|
782
|
+
"category": "builtins",
|
|
783
|
+
"reason": "Not in any allowlist (default: block)",
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def get_security_summary() -> dict[str, Any]:
|
|
788
|
+
"""Get a summary of the current security configuration.
|
|
789
|
+
|
|
790
|
+
Used by ot.security() for agent introspection.
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
Dict with counts and sample items for each category.
|
|
794
|
+
"""
|
|
795
|
+
security_config = _get_security_config()
|
|
796
|
+
tool_namespaces = _get_tool_namespaces()
|
|
797
|
+
|
|
798
|
+
if security_config is None:
|
|
799
|
+
return {
|
|
800
|
+
"status": "fallback",
|
|
801
|
+
"message": "Using fallback defaults (security.yaml not loaded)",
|
|
802
|
+
"builtins_allowed": len(FALLBACK_ALLOWED_BUILTINS),
|
|
803
|
+
"imports_allowed": len(FALLBACK_ALLOWED_IMPORTS),
|
|
804
|
+
"tool_namespaces": sorted(tool_namespaces),
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
builtins = security_config.get_allowed_builtins()
|
|
808
|
+
imports = security_config.get_allowed_imports()
|
|
809
|
+
warned_imports = security_config.get_warned_imports()
|
|
810
|
+
blocked_calls = security_config.get_blocked_calls()
|
|
811
|
+
warned_calls = security_config.get_warned_calls()
|
|
812
|
+
dunders = security_config.get_allowed_dunders()
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
"status": "configured",
|
|
816
|
+
"enabled": security_config.enabled,
|
|
817
|
+
"builtins": {
|
|
818
|
+
"allowed_count": len(builtins),
|
|
819
|
+
"sample": sorted(builtins)[:10],
|
|
820
|
+
},
|
|
821
|
+
"imports": {
|
|
822
|
+
"allowed_count": len(imports),
|
|
823
|
+
"warned_count": len(warned_imports),
|
|
824
|
+
"sample_allowed": sorted(imports)[:10],
|
|
825
|
+
"warned": sorted(warned_imports),
|
|
826
|
+
},
|
|
827
|
+
"calls": {
|
|
828
|
+
"blocked_count": len(blocked_calls),
|
|
829
|
+
"warned_count": len(warned_calls),
|
|
830
|
+
"blocked": sorted(blocked_calls),
|
|
831
|
+
"warned": sorted(warned_calls),
|
|
832
|
+
},
|
|
833
|
+
"dunders": {
|
|
834
|
+
"allowed": sorted(dunders),
|
|
835
|
+
},
|
|
836
|
+
"tool_namespaces": sorted(tool_namespaces),
|
|
837
|
+
}
|