onetool-mcp 1.0.0b1__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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. ot_tools/web_fetch.py +384 -0
@@ -0,0 +1,398 @@
1
+ """AST-based code validation for OneTool.
2
+
3
+ Validates Python code before execution:
4
+ - Syntax validation via ast.parse()
5
+ - Security pattern detection (dangerous calls)
6
+ - Optional Ruff linting integration for style warnings
7
+
8
+ Security patterns are configurable via onetool.yaml and support wildcards:
9
+
10
+ security:
11
+ validate_code: true
12
+ enabled: true
13
+ blocked: [exec, eval, compile, __import__, subprocess.*, os.system]
14
+ warned: [subprocess, os, open, pickle.*, yaml.load]
15
+
16
+ Pattern matching logic (automatic based on pattern structure):
17
+ - Patterns WITHOUT dots (e.g., 'exec', 'subprocess') match:
18
+ * Builtin function calls: exec(), eval()
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()
22
+
23
+ Wildcard patterns use fnmatch syntax:
24
+ - '*' matches any characters (e.g., 'subprocess.*' matches 'subprocess.run')
25
+ - '?' matches a single character
26
+ - '[seq]' matches any character in seq
27
+
28
+ Example:
29
+ result = validate_python_code(code)
30
+ if not result.valid:
31
+ print(f"Validation errors: {result.errors}")
32
+ if result.warnings:
33
+ print(f"Warnings: {result.warnings}")
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import ast
39
+ import fnmatch
40
+ from dataclasses import dataclass, field
41
+ from typing import TYPE_CHECKING
42
+
43
+ if TYPE_CHECKING:
44
+ from ot.config.loader import SecurityConfig
45
+
46
+
47
+ @dataclass
48
+ class ValidationResult:
49
+ """Result of code validation."""
50
+
51
+ valid: bool = True
52
+ errors: list[str] = field(default_factory=list)
53
+ warnings: list[str] = field(default_factory=list)
54
+ ast_tree: ast.Module | None = None
55
+
56
+
57
+ # =============================================================================
58
+ # Default Security Patterns
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.
69
+ # =============================================================================
70
+
71
+ DEFAULT_BLOCKED = frozenset(
72
+ {
73
+ # Builtins - arbitrary code execution risk (no dots = match calls + imports)
74
+ "exec",
75
+ "eval",
76
+ "compile",
77
+ "__import__",
78
+ # Qualified functions - command injection (dots = match calls only)
79
+ "subprocess.*", # All subprocess functions
80
+ "os.system", # Shell command execution
81
+ "os.popen", # Shell command execution
82
+ "os.spawn*", # spawnl, spawnle, spawnv, etc.
83
+ "os.exec*", # execl, execle, execv, etc.
84
+ }
85
+ )
86
+
87
+ DEFAULT_WARNED = frozenset(
88
+ {
89
+ # Module imports - enables dangerous operations (no dots = match imports)
90
+ "subprocess",
91
+ "os",
92
+ # Qualified functions - file access, deserialization (dots = match calls)
93
+ "open", # File access (common but risky)
94
+ "pickle.*", # Deserialization attacks
95
+ "yaml.load", # Unsafe YAML deserialization
96
+ "marshal.*", # Object deserialization
97
+ }
98
+ )
99
+
100
+
101
+ def _matches_pattern(name: str, patterns: frozenset[str]) -> str | None:
102
+ """Check if a name matches any pattern in the set.
103
+
104
+ Supports both exact matches and fnmatch wildcards.
105
+
106
+ Args:
107
+ name: The function/module name to check
108
+ patterns: Set of patterns (may contain wildcards)
109
+
110
+ Returns:
111
+ The matching pattern if found, None otherwise
112
+ """
113
+ # Fast path: exact match
114
+ if name in patterns:
115
+ return name
116
+
117
+ # Check wildcard patterns
118
+ for pattern in patterns:
119
+ if ("*" in pattern or "?" in pattern or "[" in pattern) and fnmatch.fnmatch(
120
+ name, pattern
121
+ ):
122
+ return pattern
123
+
124
+ return None
125
+
126
+
127
+ def _has_dot(pattern: str) -> bool:
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
+
137
+ def _get_security_config() -> SecurityConfig | None:
138
+ """Get security configuration from global config.
139
+
140
+ Returns:
141
+ SecurityConfig if available, None otherwise.
142
+ """
143
+ try:
144
+ from ot.config.loader import get_config
145
+
146
+ config = get_config()
147
+ return config.security
148
+ except Exception:
149
+ # Config not loaded yet or error - use defaults
150
+ return None
151
+
152
+
153
+ class DangerousPatternVisitor(ast.NodeVisitor):
154
+ """AST visitor that detects dangerous code patterns.
155
+
156
+ Patterns are configurable via onetool.yaml security section.
157
+ Supports fnmatch wildcards (*, ?, [seq]).
158
+
159
+ Pattern matching is automatic based on structure:
160
+ - Patterns without dots match builtins and imports
161
+ - Patterns with dots match qualified function calls
162
+
163
+ Two-tier priority: allow > warned > blocked
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ blocked: frozenset[str] | None = None,
169
+ warned: frozenset[str] | None = None,
170
+ ) -> None:
171
+ """Initialize visitor with security patterns.
172
+
173
+ Args:
174
+ blocked: Patterns that block execution (cause errors)
175
+ warned: Patterns that generate warnings (allow execution)
176
+ """
177
+ self.errors: list[str] = []
178
+ self.warnings: list[str] = []
179
+
180
+ # Use provided patterns or defaults
181
+ blocked_all = blocked or DEFAULT_BLOCKED
182
+ warned_all = warned or DEFAULT_WARNED
183
+
184
+ # Split patterns by type for efficient matching
185
+ # Patterns without dots → match builtins and imports
186
+ # Patterns with dots → match qualified calls
187
+ self.blocked_simple = frozenset(p for p in blocked_all if not _has_dot(p))
188
+ self.blocked_qualified = frozenset(p for p in blocked_all if _has_dot(p))
189
+ self.warned_simple = frozenset(p for p in warned_all if not _has_dot(p))
190
+ self.warned_qualified = frozenset(p for p in warned_all if _has_dot(p))
191
+
192
+ def visit_Call(self, node: ast.Call) -> None:
193
+ """Check function calls for dangerous patterns.
194
+
195
+ Priority order: blocked > warned (allow handled at setup)
196
+ """
197
+ func_name = self._get_call_name(node)
198
+
199
+ if not func_name:
200
+ self.generic_visit(node)
201
+ return
202
+
203
+ # Determine which pattern sets to check based on call type
204
+ is_qualified = "." in func_name
205
+
206
+ if is_qualified:
207
+ # Qualified call (e.g., subprocess.run) - check qualified patterns
208
+ # Priority: blocked > warned
209
+ if pattern := _matches_pattern(func_name, self.blocked_qualified):
210
+ self.errors.append(
211
+ f"Line {node.lineno}: Dangerous function '{func_name}' is not "
212
+ f"allowed (matches '{pattern}')"
213
+ )
214
+ elif pattern := _matches_pattern(func_name, self.warned_qualified):
215
+ self.warnings.append(
216
+ f"Line {node.lineno}: Potentially unsafe function '{func_name}' "
217
+ f"(matches '{pattern}')"
218
+ )
219
+ else:
220
+ # Simple call (e.g., exec) - check simple patterns (builtins)
221
+ # Priority: blocked > warned
222
+ if pattern := _matches_pattern(func_name, self.blocked_simple):
223
+ self.errors.append(
224
+ f"Line {node.lineno}: Dangerous builtin '{func_name}' is not "
225
+ f"allowed (matches '{pattern}')"
226
+ )
227
+ elif pattern := _matches_pattern(func_name, self.warned_simple):
228
+ self.warnings.append(
229
+ f"Line {node.lineno}: Potentially unsafe function '{func_name}' "
230
+ f"(matches '{pattern}')"
231
+ )
232
+
233
+ # Continue visiting child nodes
234
+ self.generic_visit(node)
235
+
236
+ def visit_Import(self, node: ast.Import) -> None:
237
+ """Check imports for dangerous modules."""
238
+ for alias in node.names:
239
+ # Check simple patterns (no dots) against import names
240
+ if pattern := _matches_pattern(alias.name, self.blocked_simple):
241
+ self.errors.append(
242
+ f"Line {node.lineno}: Import of '{alias.name}' is not allowed "
243
+ f"(matches '{pattern}')"
244
+ )
245
+ elif pattern := _matches_pattern(alias.name, self.warned_simple):
246
+ self.warnings.append(
247
+ f"Line {node.lineno}: Import of '{alias.name}' may enable "
248
+ f"dangerous operations (matches '{pattern}')"
249
+ )
250
+ self.generic_visit(node)
251
+
252
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
253
+ """Check from imports for dangerous modules."""
254
+ if node.module:
255
+ # Check simple patterns against the module name
256
+ if pattern := _matches_pattern(node.module, self.blocked_simple):
257
+ self.errors.append(
258
+ f"Line {node.lineno}: Import from '{node.module}' is not allowed "
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}')"
265
+ )
266
+ self.generic_visit(node)
267
+
268
+ def _get_call_name(self, node: ast.Call) -> str:
269
+ """Extract the full name of a function call.
270
+
271
+ Handles:
272
+ - Simple calls: func()
273
+ - Attribute calls: module.func()
274
+ - Chained calls: module.submodule.func()
275
+ """
276
+ if isinstance(node.func, ast.Name):
277
+ return node.func.id
278
+ elif isinstance(node.func, ast.Attribute):
279
+ parts: list[str] = [node.func.attr]
280
+ current = node.func.value
281
+ while isinstance(current, ast.Attribute):
282
+ parts.append(current.attr)
283
+ current = current.value
284
+ if isinstance(current, ast.Name):
285
+ parts.append(current.id)
286
+ return ".".join(reversed(parts))
287
+ return ""
288
+
289
+
290
+ def validate_python_code(
291
+ code: str,
292
+ check_security: bool = True,
293
+ lint_warnings: bool = False,
294
+ filename: str = "<string>",
295
+ ) -> ValidationResult:
296
+ """Validate Python code for syntax and security issues.
297
+
298
+ Security patterns are loaded from onetool.yaml configuration.
299
+ If config is not available, built-in defaults are used.
300
+ Patterns support fnmatch wildcards (*, ?, [seq]).
301
+
302
+ Args:
303
+ code: Python code to validate
304
+ check_security: Whether to check for dangerous patterns (default True)
305
+ lint_warnings: Whether to include Ruff style warnings (default False)
306
+ filename: Filename for error messages
307
+
308
+ Returns:
309
+ ValidationResult with valid flag, errors, and warnings
310
+ """
311
+ result = ValidationResult()
312
+
313
+ # Step 1: Syntax validation
314
+ try:
315
+ tree = ast.parse(code, filename=filename)
316
+ result.ast_tree = tree
317
+ except SyntaxError as e:
318
+ result.valid = False
319
+ line_info = f" at line {e.lineno}" if e.lineno else ""
320
+ result.errors.append(f"Syntax error{line_info}: {e.msg}")
321
+ return result
322
+
323
+ # Step 2: Security pattern detection
324
+ if check_security:
325
+ # Load patterns from config or use defaults
326
+ security_config = _get_security_config()
327
+
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:
352
+ # Security disabled in config - skip validation
353
+ visitor = None
354
+ else:
355
+ # No config - use defaults
356
+ visitor = DangerousPatternVisitor()
357
+
358
+ if visitor is not None:
359
+ visitor.visit(tree)
360
+
361
+ if visitor.errors:
362
+ result.valid = False
363
+ result.errors.extend(visitor.errors)
364
+
365
+ result.warnings.extend(visitor.warnings)
366
+
367
+ # Step 3: Optional Ruff linting (style warnings only)
368
+ if lint_warnings:
369
+ from ot.executor.linter import lint_code
370
+
371
+ lint_result = lint_code(code)
372
+ if lint_result.available:
373
+ result.warnings.extend(lint_result.warnings)
374
+
375
+ return result
376
+
377
+
378
+ def validate_for_exec(code: str) -> ValidationResult:
379
+ """Validate code specifically for exec() execution.
380
+
381
+ This is a stricter validation that also checks for patterns
382
+ that are problematic in exec() context.
383
+
384
+ Args:
385
+ code: Python code to validate
386
+
387
+ Returns:
388
+ ValidationResult with validation status
389
+ """
390
+ result = validate_python_code(code, check_security=True)
391
+
392
+ if not result.valid:
393
+ return result
394
+
395
+ # Additional exec-specific checks could go here
396
+ # For example, checking for top-level returns outside functions
397
+
398
+ return result