onetool-mcp 1.0.0rc2__py3-none-any.whl → 1.0.0rc3__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 +2 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/METADATA +26 -33
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/RECORD +31 -33
- ot/config/__init__.py +90 -48
- ot/config/global_templates/__init__.py +2 -2
- ot/config/global_templates/diagram-templates/api-flow.mmd +33 -33
- ot/config/global_templates/diagram-templates/c4-context.puml +30 -30
- ot/config/global_templates/diagram-templates/class-diagram.mmd +87 -87
- ot/config/global_templates/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/global_templates/diagram-templates/microservices.d2 +81 -81
- ot/config/global_templates/diagram-templates/project-gantt.mmd +37 -37
- ot/config/global_templates/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -167
- ot/config/global_templates/onetool.yaml +2 -0
- ot/config/global_templates/prompts.yaml +102 -102
- ot/config/global_templates/security.yaml +1 -4
- ot/config/global_templates/servers.yaml +1 -1
- ot/config/global_templates/tool_templates/__init__.py +7 -7
- ot/config/loader.py +226 -869
- ot/config/models.py +735 -0
- ot/config/secrets.py +243 -192
- ot/executor/tool_loader.py +10 -1
- ot/executor/validator.py +11 -1
- ot/meta.py +338 -33
- ot/prompts.py +228 -218
- ot/proxy/manager.py +168 -8
- ot/registry/__init__.py +199 -189
- ot/config/dynamic.py +0 -121
- ot/config/mcp.py +0 -149
- ot/config/tool_config.py +0 -125
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/entry_points.txt +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
- {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/NOTICE.txt +0 -0
ot/config/models.py
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
"""Pydantic models for OneTool configuration with embedded defaults.
|
|
2
|
+
|
|
3
|
+
All default values are embedded directly in model definitions (single source of truth).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
12
|
+
|
|
13
|
+
# ==================== Snippet Models ====================
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SnippetParam(BaseModel):
|
|
17
|
+
"""Parameter definition for a snippet."""
|
|
18
|
+
|
|
19
|
+
required: bool = Field(
|
|
20
|
+
default=True, description="Whether this parameter is required"
|
|
21
|
+
)
|
|
22
|
+
default: Any = Field(default=None, description="Default value if not provided")
|
|
23
|
+
description: str = Field(default="", description="Description of the parameter")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SnippetDef(BaseModel):
|
|
27
|
+
"""Definition of a reusable snippet template."""
|
|
28
|
+
|
|
29
|
+
description: str = Field(
|
|
30
|
+
default="", description="Description of what this snippet does"
|
|
31
|
+
)
|
|
32
|
+
params: dict[str, SnippetParam] = Field(
|
|
33
|
+
default_factory=dict, description="Parameter definitions"
|
|
34
|
+
)
|
|
35
|
+
body: str = Field(
|
|
36
|
+
..., description="Jinja2 template body that expands to Python code"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ==================== Transform Configuration ====================
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TransformConfig(BaseModel):
|
|
44
|
+
"""Configuration for the transform() tool."""
|
|
45
|
+
|
|
46
|
+
model: str = Field(default="", description="Model for code generation")
|
|
47
|
+
base_url: str = Field(default="", description="Base URL for OpenAI-compatible API")
|
|
48
|
+
max_tokens: int = Field(default=4096, description="Max output tokens")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ==================== Security Configuration ====================
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BuiltinsConfig(BaseModel):
|
|
55
|
+
"""Builtins allowlist configuration."""
|
|
56
|
+
|
|
57
|
+
allow: list[str] = Field(
|
|
58
|
+
default_factory=lambda: [
|
|
59
|
+
# Types
|
|
60
|
+
"bool",
|
|
61
|
+
"bytes",
|
|
62
|
+
"dict",
|
|
63
|
+
"float",
|
|
64
|
+
"frozenset",
|
|
65
|
+
"int",
|
|
66
|
+
"list",
|
|
67
|
+
"set",
|
|
68
|
+
"str",
|
|
69
|
+
"tuple",
|
|
70
|
+
"type",
|
|
71
|
+
# Functions
|
|
72
|
+
"abs",
|
|
73
|
+
"all",
|
|
74
|
+
"any",
|
|
75
|
+
"ascii",
|
|
76
|
+
"callable",
|
|
77
|
+
"chr",
|
|
78
|
+
"delattr",
|
|
79
|
+
"dir",
|
|
80
|
+
"divmod",
|
|
81
|
+
"enumerate",
|
|
82
|
+
"filter",
|
|
83
|
+
"format",
|
|
84
|
+
"getattr",
|
|
85
|
+
"hasattr",
|
|
86
|
+
"hash",
|
|
87
|
+
"id",
|
|
88
|
+
"isinstance",
|
|
89
|
+
"issubclass",
|
|
90
|
+
"iter",
|
|
91
|
+
"len",
|
|
92
|
+
"map",
|
|
93
|
+
"max",
|
|
94
|
+
"min",
|
|
95
|
+
"next",
|
|
96
|
+
"ord",
|
|
97
|
+
"pow",
|
|
98
|
+
"print",
|
|
99
|
+
"range",
|
|
100
|
+
"repr",
|
|
101
|
+
"reversed",
|
|
102
|
+
"round",
|
|
103
|
+
"setattr",
|
|
104
|
+
"slice",
|
|
105
|
+
"sorted",
|
|
106
|
+
"sum",
|
|
107
|
+
"vars",
|
|
108
|
+
"zip",
|
|
109
|
+
# Exceptions
|
|
110
|
+
"*Error",
|
|
111
|
+
"*Exception",
|
|
112
|
+
"StopIteration",
|
|
113
|
+
],
|
|
114
|
+
description="Allowed builtin functions and types",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ImportsConfig(BaseModel):
|
|
119
|
+
"""Imports allowlist configuration."""
|
|
120
|
+
|
|
121
|
+
allow: list[str] = Field(
|
|
122
|
+
default_factory=lambda: [
|
|
123
|
+
# Note: pathlib intentionally excluded - use file.* tools instead for sandboxed filesystem access
|
|
124
|
+
"abc",
|
|
125
|
+
"array",
|
|
126
|
+
"base64",
|
|
127
|
+
"bisect",
|
|
128
|
+
"calendar",
|
|
129
|
+
"collections",
|
|
130
|
+
"copy",
|
|
131
|
+
"csv",
|
|
132
|
+
"dataclasses",
|
|
133
|
+
"datetime",
|
|
134
|
+
"decimal",
|
|
135
|
+
"difflib",
|
|
136
|
+
"enum",
|
|
137
|
+
"fractions",
|
|
138
|
+
"functools",
|
|
139
|
+
"hashlib",
|
|
140
|
+
"heapq",
|
|
141
|
+
"html",
|
|
142
|
+
"html.parser",
|
|
143
|
+
"itertools",
|
|
144
|
+
"json",
|
|
145
|
+
"math",
|
|
146
|
+
"operator",
|
|
147
|
+
"random",
|
|
148
|
+
"re",
|
|
149
|
+
"statistics",
|
|
150
|
+
"string",
|
|
151
|
+
"textwrap",
|
|
152
|
+
"time",
|
|
153
|
+
"types",
|
|
154
|
+
"typing",
|
|
155
|
+
"urllib.parse",
|
|
156
|
+
"uuid",
|
|
157
|
+
"zoneinfo",
|
|
158
|
+
],
|
|
159
|
+
description="Allowed import modules",
|
|
160
|
+
)
|
|
161
|
+
warn: list[str] = Field(
|
|
162
|
+
default_factory=lambda: ["yaml"],
|
|
163
|
+
description="Imports that trigger warnings but are allowed",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class CallsConfig(BaseModel):
|
|
168
|
+
"""Qualified calls configuration."""
|
|
169
|
+
|
|
170
|
+
allow: list[str] = Field(
|
|
171
|
+
default_factory=list,
|
|
172
|
+
description="Allowed qualified function calls (e.g., 'json.loads')",
|
|
173
|
+
)
|
|
174
|
+
block: list[str] = Field(
|
|
175
|
+
default_factory=list,
|
|
176
|
+
description="Blocked qualified function calls (e.g., 'pickle.*')",
|
|
177
|
+
)
|
|
178
|
+
warn: list[str] = Field(
|
|
179
|
+
default_factory=list,
|
|
180
|
+
description="Qualified calls that trigger warnings",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class DundersConfig(BaseModel):
|
|
185
|
+
"""Magic variable (dunder) configuration."""
|
|
186
|
+
|
|
187
|
+
allow: list[str] = Field(
|
|
188
|
+
default_factory=lambda: ["__format__", "__sanitize__"],
|
|
189
|
+
description="Allowed magic variables (e.g., '__format__')",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class OutputSanitizationConfig(BaseModel):
|
|
194
|
+
"""Output sanitization configuration for prompt injection protection.
|
|
195
|
+
|
|
196
|
+
Protects against indirect prompt injection by sanitizing tool outputs
|
|
197
|
+
that may contain malicious payloads from external content.
|
|
198
|
+
|
|
199
|
+
Three-layer defense:
|
|
200
|
+
1. Trigger sanitization: Replace __ot, mcp__onetool patterns
|
|
201
|
+
2. Tag sanitization: Remove <external-content-*> patterns
|
|
202
|
+
3. GUID-tagged boundaries: Wrap content in unpredictable tags
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
enabled: bool = Field(
|
|
206
|
+
default=True,
|
|
207
|
+
description="Global toggle for output sanitization",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class SecurityConfig(BaseModel):
|
|
212
|
+
"""Code validation security configuration.
|
|
213
|
+
|
|
214
|
+
Allowlist-based security model: block everything by default, explicitly
|
|
215
|
+
allow what's safe. Tool namespaces (ot.*, brave.*, etc.) are auto-allowed.
|
|
216
|
+
|
|
217
|
+
Configuration structure:
|
|
218
|
+
security:
|
|
219
|
+
builtins:
|
|
220
|
+
allow: [str, int, list, ...]
|
|
221
|
+
imports:
|
|
222
|
+
allow: [json, re, math, ...]
|
|
223
|
+
warn: [yaml]
|
|
224
|
+
calls:
|
|
225
|
+
block: [pickle.*, yaml.load]
|
|
226
|
+
warn: [random.seed]
|
|
227
|
+
dunders:
|
|
228
|
+
allow: [__format__, __sanitize__]
|
|
229
|
+
|
|
230
|
+
Patterns support fnmatch wildcards:
|
|
231
|
+
- '*' matches any characters (e.g., 'subprocess.*' matches 'subprocess.run')
|
|
232
|
+
- '?' matches a single character
|
|
233
|
+
- '[seq]' matches any character in seq
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
# Cached frozensets (computed lazily on first access)
|
|
237
|
+
_allowed_builtins: frozenset[str] | None = PrivateAttr(default=None)
|
|
238
|
+
_allowed_imports: frozenset[str] | None = PrivateAttr(default=None)
|
|
239
|
+
_warned_imports: frozenset[str] | None = PrivateAttr(default=None)
|
|
240
|
+
_blocked_calls: frozenset[str] | None = PrivateAttr(default=None)
|
|
241
|
+
_warned_calls: frozenset[str] | None = PrivateAttr(default=None)
|
|
242
|
+
_allowed_calls: frozenset[str] | None = PrivateAttr(default=None)
|
|
243
|
+
_allowed_dunders: frozenset[str] | None = PrivateAttr(default=None)
|
|
244
|
+
|
|
245
|
+
validate_code: bool = Field(
|
|
246
|
+
default=True,
|
|
247
|
+
description="Enable AST-based code validation before execution",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
enabled: bool = Field(
|
|
251
|
+
default=True,
|
|
252
|
+
description="Enable security pattern checking (requires validate_code)",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
builtins: BuiltinsConfig = Field(
|
|
256
|
+
default_factory=BuiltinsConfig,
|
|
257
|
+
description="Builtins allowlist configuration",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
imports: ImportsConfig = Field(
|
|
261
|
+
default_factory=ImportsConfig,
|
|
262
|
+
description="Imports allowlist configuration",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
calls: CallsConfig = Field(
|
|
266
|
+
default_factory=CallsConfig,
|
|
267
|
+
description="Qualified calls configuration",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
dunders: DundersConfig = Field(
|
|
271
|
+
default_factory=DundersConfig,
|
|
272
|
+
description="Magic variable (dunder) configuration",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
sanitize: OutputSanitizationConfig = Field(
|
|
276
|
+
default_factory=OutputSanitizationConfig,
|
|
277
|
+
description="Output sanitization for prompt injection protection",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def get_allowed_builtins(self) -> frozenset[str]:
|
|
281
|
+
"""Get the set of allowed builtins (cached)."""
|
|
282
|
+
if self._allowed_builtins is None:
|
|
283
|
+
self._allowed_builtins = frozenset(self.builtins.allow)
|
|
284
|
+
return self._allowed_builtins
|
|
285
|
+
|
|
286
|
+
def get_allowed_imports(self) -> frozenset[str]:
|
|
287
|
+
"""Get the set of allowed imports (cached)."""
|
|
288
|
+
if self._allowed_imports is None:
|
|
289
|
+
self._allowed_imports = frozenset(self.imports.allow)
|
|
290
|
+
return self._allowed_imports
|
|
291
|
+
|
|
292
|
+
def get_warned_imports(self) -> frozenset[str]:
|
|
293
|
+
"""Get the set of imports that trigger warnings (cached)."""
|
|
294
|
+
if self._warned_imports is None:
|
|
295
|
+
self._warned_imports = frozenset(self.imports.warn)
|
|
296
|
+
return self._warned_imports
|
|
297
|
+
|
|
298
|
+
def get_blocked_calls(self) -> frozenset[str]:
|
|
299
|
+
"""Get the set of blocked qualified calls (cached)."""
|
|
300
|
+
if self._blocked_calls is None:
|
|
301
|
+
self._blocked_calls = frozenset(self.calls.block)
|
|
302
|
+
return self._blocked_calls
|
|
303
|
+
|
|
304
|
+
def get_warned_calls(self) -> frozenset[str]:
|
|
305
|
+
"""Get the set of qualified calls that trigger warnings (cached)."""
|
|
306
|
+
if self._warned_calls is None:
|
|
307
|
+
self._warned_calls = frozenset(self.calls.warn)
|
|
308
|
+
return self._warned_calls
|
|
309
|
+
|
|
310
|
+
def get_allowed_calls(self) -> frozenset[str]:
|
|
311
|
+
"""Get the set of explicitly allowed qualified calls (cached)."""
|
|
312
|
+
if self._allowed_calls is None:
|
|
313
|
+
self._allowed_calls = frozenset(self.calls.allow)
|
|
314
|
+
return self._allowed_calls
|
|
315
|
+
|
|
316
|
+
def get_allowed_dunders(self) -> frozenset[str]:
|
|
317
|
+
"""Get the set of allowed magic variables (cached)."""
|
|
318
|
+
if self._allowed_dunders is None:
|
|
319
|
+
self._allowed_dunders = frozenset(self.dunders.allow)
|
|
320
|
+
return self._allowed_dunders
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ==================== Output Configuration ====================
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class OutputConfig(BaseModel):
|
|
327
|
+
"""Large output handling configuration.
|
|
328
|
+
|
|
329
|
+
When tool outputs exceed max_inline_size, they are stored to disk
|
|
330
|
+
and a summary with a query handle is returned instead.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
max_inline_size: int = Field(
|
|
334
|
+
default=50000,
|
|
335
|
+
ge=0,
|
|
336
|
+
description="Max output size in bytes before storing to disk. Set to 0 to disable.",
|
|
337
|
+
)
|
|
338
|
+
result_store_dir: str = Field(
|
|
339
|
+
default="tmp",
|
|
340
|
+
description="Directory for result files (relative to .onetool/)",
|
|
341
|
+
)
|
|
342
|
+
result_ttl: int = Field(
|
|
343
|
+
default=3600,
|
|
344
|
+
ge=0,
|
|
345
|
+
description="Time-to-live in seconds for stored results (0 = no expiry)",
|
|
346
|
+
)
|
|
347
|
+
preview_lines: int = Field(
|
|
348
|
+
default=10,
|
|
349
|
+
ge=0,
|
|
350
|
+
description="Number of preview lines to include in summary",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ==================== Stats Configuration ====================
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class StatsConfig(BaseModel):
|
|
358
|
+
"""Runtime statistics collection configuration."""
|
|
359
|
+
|
|
360
|
+
enabled: bool = Field(
|
|
361
|
+
default=True,
|
|
362
|
+
description="Enable statistics collection",
|
|
363
|
+
)
|
|
364
|
+
persist_dir: str = Field(
|
|
365
|
+
default="stats",
|
|
366
|
+
description="Directory for stats files (relative to .onetool/)",
|
|
367
|
+
)
|
|
368
|
+
persist_path: str = Field(
|
|
369
|
+
default="stats.jsonl",
|
|
370
|
+
description="Filename for stats persistence (within persist_dir)",
|
|
371
|
+
)
|
|
372
|
+
flush_interval_seconds: int = Field(
|
|
373
|
+
default=30,
|
|
374
|
+
ge=1,
|
|
375
|
+
le=300,
|
|
376
|
+
description="Interval in seconds between flushing stats to disk",
|
|
377
|
+
)
|
|
378
|
+
context_per_call: int = Field(
|
|
379
|
+
default=30000,
|
|
380
|
+
ge=0,
|
|
381
|
+
description="Estimated context tokens saved per consolidated tool call",
|
|
382
|
+
)
|
|
383
|
+
time_overhead_per_call_ms: int = Field(
|
|
384
|
+
default=4000,
|
|
385
|
+
ge=0,
|
|
386
|
+
description="Estimated time overhead in ms saved per consolidated tool call",
|
|
387
|
+
)
|
|
388
|
+
model: str = Field(
|
|
389
|
+
default="anthropic/claude-opus-4.5",
|
|
390
|
+
description="Model for cost estimation (e.g., anthropic/claude-opus-4.5)",
|
|
391
|
+
)
|
|
392
|
+
cost_per_million_input_tokens: float = Field(
|
|
393
|
+
default=15.0,
|
|
394
|
+
ge=0,
|
|
395
|
+
description="Cost in USD per million input tokens",
|
|
396
|
+
)
|
|
397
|
+
cost_per_million_output_tokens: float = Field(
|
|
398
|
+
default=75.0,
|
|
399
|
+
ge=0,
|
|
400
|
+
description="Cost in USD per million output tokens",
|
|
401
|
+
)
|
|
402
|
+
chars_per_token: float = Field(
|
|
403
|
+
default=4.0,
|
|
404
|
+
ge=1.0,
|
|
405
|
+
description="Average characters per token for estimation",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ==================== Message Configuration ====================
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class MsgTopicConfig(BaseModel):
|
|
413
|
+
"""Topic-to-file mapping for message routing."""
|
|
414
|
+
|
|
415
|
+
pattern: str = Field(
|
|
416
|
+
...,
|
|
417
|
+
description="Glob-style topic pattern (e.g., 'status:*', 'doc:*')",
|
|
418
|
+
)
|
|
419
|
+
file: str = Field(
|
|
420
|
+
...,
|
|
421
|
+
description="File path for messages matching this pattern (supports ~ and ${VAR})",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class MsgConfig(BaseModel):
|
|
426
|
+
"""Message tool configuration."""
|
|
427
|
+
|
|
428
|
+
topics: list[MsgTopicConfig] = Field(
|
|
429
|
+
default_factory=list,
|
|
430
|
+
description="Topic patterns mapped to output files (first match wins)",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ==================== MCP Server Configuration ====================
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class AuthConfig(BaseModel):
|
|
438
|
+
"""Authentication configuration for MCP servers."""
|
|
439
|
+
|
|
440
|
+
type: Literal["oauth", "bearer"] = Field(
|
|
441
|
+
description="Authentication type",
|
|
442
|
+
)
|
|
443
|
+
scopes: list[str] = Field(
|
|
444
|
+
default_factory=list,
|
|
445
|
+
description="OAuth scopes (for type=oauth)",
|
|
446
|
+
)
|
|
447
|
+
token: str | None = Field(
|
|
448
|
+
default=None,
|
|
449
|
+
description="Bearer token (for type=bearer, supports ${VAR} expansion)",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class McpServerConfig(BaseModel):
|
|
454
|
+
"""Configuration for an MCP server connection.
|
|
455
|
+
|
|
456
|
+
Compatible with bench ServerConfig format, with additional
|
|
457
|
+
`enabled` field for toggling servers without removing config.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
type: Literal["http", "stdio"] = Field(description="Server connection type")
|
|
461
|
+
enabled: bool = Field(default=True, description="Whether this server is enabled")
|
|
462
|
+
url: str | None = Field(default=None, description="URL for HTTP servers")
|
|
463
|
+
headers: dict[str, str] = Field(
|
|
464
|
+
default_factory=dict, description="Headers for HTTP servers"
|
|
465
|
+
)
|
|
466
|
+
command: str | None = Field(default=None, description="Command for stdio servers")
|
|
467
|
+
args: list[str] = Field(
|
|
468
|
+
default_factory=list, description="Arguments for stdio command"
|
|
469
|
+
)
|
|
470
|
+
env: dict[str, str] = Field(
|
|
471
|
+
default_factory=dict, description="Environment variables for stdio servers"
|
|
472
|
+
)
|
|
473
|
+
timeout: int = Field(default=30, description="Connection timeout in seconds")
|
|
474
|
+
auth: AuthConfig | None = Field(
|
|
475
|
+
default=None,
|
|
476
|
+
description="Authentication configuration for HTTP servers",
|
|
477
|
+
)
|
|
478
|
+
instructions: str | None = Field(
|
|
479
|
+
default=None,
|
|
480
|
+
description="Agent instructions for using this server's tools (surfaced in MCP instructions)",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ==================== Tools Configuration ====================
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class ToolsConfig(BaseModel):
|
|
488
|
+
"""Aggregated tool configurations.
|
|
489
|
+
|
|
490
|
+
Core configs (msg, stats) are typed fields. Tool-specific configs
|
|
491
|
+
(brave, ground, etc.) are allowed as extra fields and accessed via
|
|
492
|
+
get_tool_config() with schemas defined in tool files.
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
model_config = ConfigDict(extra="allow")
|
|
496
|
+
|
|
497
|
+
# Core configs - always available
|
|
498
|
+
msg: MsgConfig = Field(default_factory=MsgConfig)
|
|
499
|
+
stats: StatsConfig = Field(default_factory=StatsConfig)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ==================== Root Configuration ====================
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class OneToolConfig(BaseModel):
|
|
506
|
+
"""Root configuration for OneTool (global-only, V2)."""
|
|
507
|
+
|
|
508
|
+
# Private attribute to track config file location (not serialized)
|
|
509
|
+
_config_dir: Path | None = PrivateAttr(default=None)
|
|
510
|
+
|
|
511
|
+
version: int = Field(
|
|
512
|
+
default=1,
|
|
513
|
+
description="Config schema version for migration support",
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
include: list[str] = Field(
|
|
517
|
+
default_factory=list,
|
|
518
|
+
description="Files to deep-merge into config (processed before validation)",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Root-level environment variables for subprocesses
|
|
522
|
+
env: dict[str, str] = Field(
|
|
523
|
+
default_factory=dict,
|
|
524
|
+
description="Shared environment variables for all MCP servers",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
transform: TransformConfig = Field(
|
|
528
|
+
default_factory=TransformConfig, description="transform() tool configuration"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
alias: dict[str, str] = Field(
|
|
532
|
+
default_factory=dict,
|
|
533
|
+
description="Short alias names mapping to full function names (e.g., ws -> brave.web_search)",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
snippets: dict[str, SnippetDef] = Field(
|
|
537
|
+
default_factory=dict,
|
|
538
|
+
description="Reusable snippet templates with Jinja2 variable substitution",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
servers: dict[str, McpServerConfig] = Field(
|
|
542
|
+
default_factory=dict,
|
|
543
|
+
description="External MCP servers to proxy through OneTool",
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
tools: ToolsConfig = Field(
|
|
547
|
+
default_factory=ToolsConfig,
|
|
548
|
+
description="Tool-specific configuration (timeouts, limits, etc.)",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
security: SecurityConfig = Field(
|
|
552
|
+
default_factory=SecurityConfig,
|
|
553
|
+
description="Code validation and security pattern configuration",
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
stats: StatsConfig = Field(
|
|
557
|
+
default_factory=StatsConfig,
|
|
558
|
+
description="Runtime statistics collection configuration",
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
output: OutputConfig = Field(
|
|
562
|
+
default_factory=OutputConfig,
|
|
563
|
+
description="Large output handling configuration",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
tools_dir: list[str] = Field(
|
|
567
|
+
default_factory=lambda: ["tools/*.py"],
|
|
568
|
+
description="Glob patterns for tool discovery (relative to .onetool/, or absolute)",
|
|
569
|
+
)
|
|
570
|
+
secrets_file: str = Field(
|
|
571
|
+
default="config/secrets.yaml",
|
|
572
|
+
description="Path to secrets file (relative to .onetool/, or absolute)",
|
|
573
|
+
)
|
|
574
|
+
prompts: dict[str, Any] | None = Field(
|
|
575
|
+
default=None,
|
|
576
|
+
description="Inline prompts config (can also be loaded via include:)",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field(
|
|
580
|
+
default="INFO", description="Logging level"
|
|
581
|
+
)
|
|
582
|
+
log_dir: str = Field(
|
|
583
|
+
default="logs",
|
|
584
|
+
description="Directory for log files (relative to .onetool/)",
|
|
585
|
+
)
|
|
586
|
+
compact_max_length: int = Field(
|
|
587
|
+
default=120, description="Max value length in compact console output"
|
|
588
|
+
)
|
|
589
|
+
log_verbose: bool = Field(
|
|
590
|
+
default=False,
|
|
591
|
+
description="Disable log truncation for debugging (full values in output)",
|
|
592
|
+
)
|
|
593
|
+
debug_tracebacks: bool = Field(
|
|
594
|
+
default=False,
|
|
595
|
+
description="Show verbose tracebacks with local variables on errors",
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
@field_validator("snippets", "servers", "alias", mode="before")
|
|
599
|
+
@classmethod
|
|
600
|
+
def empty_dict_if_none(cls, v: Any) -> Any:
|
|
601
|
+
"""Convert None to empty dict for dict-type fields.
|
|
602
|
+
|
|
603
|
+
This handles YAML files where the key exists but all values are commented out,
|
|
604
|
+
which YAML parses as None instead of an empty dict.
|
|
605
|
+
"""
|
|
606
|
+
return {} if v is None else v
|
|
607
|
+
|
|
608
|
+
def get_tool_files(self) -> list[Path]:
|
|
609
|
+
"""Get list of tool files matching configured glob patterns.
|
|
610
|
+
|
|
611
|
+
Pattern resolution (all relative to OT_DIR .onetool/):
|
|
612
|
+
- Absolute paths: used as-is
|
|
613
|
+
- ~ paths: expanded to home directory
|
|
614
|
+
- Relative patterns: resolved relative to OT_DIR (.onetool/)
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
List of Path objects for tool files
|
|
618
|
+
"""
|
|
619
|
+
import glob
|
|
620
|
+
|
|
621
|
+
from ot.paths import get_global_dir
|
|
622
|
+
|
|
623
|
+
tool_files: list[Path] = []
|
|
624
|
+
|
|
625
|
+
# Determine OT_DIR for resolving patterns
|
|
626
|
+
if self._config_dir is not None:
|
|
627
|
+
# _config_dir is the config/ subdirectory, go up to .onetool/
|
|
628
|
+
ot_dir = self._config_dir.parent
|
|
629
|
+
else:
|
|
630
|
+
# Fallback: global .onetool
|
|
631
|
+
ot_dir = get_global_dir()
|
|
632
|
+
|
|
633
|
+
for pattern in self.tools_dir:
|
|
634
|
+
# Expand ~ first
|
|
635
|
+
expanded = Path(pattern).expanduser()
|
|
636
|
+
|
|
637
|
+
# Determine resolved pattern for globbing
|
|
638
|
+
if expanded.is_absolute():
|
|
639
|
+
# Absolute pattern - use as-is
|
|
640
|
+
resolved_pattern = str(expanded)
|
|
641
|
+
else:
|
|
642
|
+
# All relative patterns resolve against OT_DIR
|
|
643
|
+
resolved_pattern = str(ot_dir / pattern)
|
|
644
|
+
|
|
645
|
+
# Use glob.glob() for cross-platform compatibility
|
|
646
|
+
for match in glob.glob(resolved_pattern, recursive=True): # noqa: PTH207
|
|
647
|
+
path = Path(match)
|
|
648
|
+
if path.is_file() and path.suffix == ".py":
|
|
649
|
+
tool_files.append(path)
|
|
650
|
+
|
|
651
|
+
return sorted(set(tool_files))
|
|
652
|
+
|
|
653
|
+
def _resolve_onetool_relative_path(self, path_str: str) -> Path:
|
|
654
|
+
"""Resolve a path relative to the .onetool directory.
|
|
655
|
+
|
|
656
|
+
Handles:
|
|
657
|
+
- Absolute paths: returned as-is
|
|
658
|
+
- ~ expansion: expanded to home directory
|
|
659
|
+
- Relative paths: resolved relative to .onetool/ directory (parent of config/)
|
|
660
|
+
|
|
661
|
+
Note: Does NOT expand ${VAR} - use ~/path instead of ${HOME}/path.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
path_str: Path string to resolve
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Resolved absolute Path
|
|
668
|
+
"""
|
|
669
|
+
from ot.paths import get_global_dir
|
|
670
|
+
|
|
671
|
+
# Only expand ~ (no ${VAR} expansion)
|
|
672
|
+
path = Path(path_str).expanduser()
|
|
673
|
+
|
|
674
|
+
# If absolute after expansion, use as-is
|
|
675
|
+
if path.is_absolute():
|
|
676
|
+
return path
|
|
677
|
+
|
|
678
|
+
# Resolve relative to .onetool/ directory (parent of config/)
|
|
679
|
+
if self._config_dir is not None:
|
|
680
|
+
# _config_dir is the config/ subdirectory, go up to .onetool/
|
|
681
|
+
onetool_dir = self._config_dir.parent
|
|
682
|
+
return (onetool_dir / path).resolve()
|
|
683
|
+
|
|
684
|
+
# Fallback: resolve relative to global .onetool/
|
|
685
|
+
return (get_global_dir() / path).resolve()
|
|
686
|
+
|
|
687
|
+
def get_secrets_file_path(self) -> Path:
|
|
688
|
+
"""Get the resolved path to the secrets configuration file.
|
|
689
|
+
|
|
690
|
+
Path is resolved relative to .onetool/ directory.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Absolute Path to secrets file
|
|
694
|
+
"""
|
|
695
|
+
return self._resolve_onetool_relative_path(self.secrets_file)
|
|
696
|
+
|
|
697
|
+
def get_log_dir_path(self) -> Path:
|
|
698
|
+
"""Get the resolved path to the log directory.
|
|
699
|
+
|
|
700
|
+
Path is resolved relative to the .onetool/ directory.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Absolute Path to log directory
|
|
704
|
+
"""
|
|
705
|
+
return self._resolve_onetool_relative_path(self.log_dir)
|
|
706
|
+
|
|
707
|
+
def get_stats_dir_path(self) -> Path:
|
|
708
|
+
"""Get the resolved path to the stats directory.
|
|
709
|
+
|
|
710
|
+
Path is resolved relative to the .onetool/ directory.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
Absolute Path to stats directory
|
|
714
|
+
"""
|
|
715
|
+
return self._resolve_onetool_relative_path(self.stats.persist_dir)
|
|
716
|
+
|
|
717
|
+
def get_stats_file_path(self) -> Path:
|
|
718
|
+
"""Get the resolved path to the stats JSONL file.
|
|
719
|
+
|
|
720
|
+
Stats file is stored in the stats directory.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
Absolute Path to stats file
|
|
724
|
+
"""
|
|
725
|
+
return self.get_stats_dir_path() / self.stats.persist_path
|
|
726
|
+
|
|
727
|
+
def get_result_store_path(self) -> Path:
|
|
728
|
+
"""Get the resolved path to the result store directory.
|
|
729
|
+
|
|
730
|
+
Path is resolved relative to the .onetool/ directory.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Absolute Path to result store directory
|
|
734
|
+
"""
|
|
735
|
+
return self._resolve_onetool_relative_path(self.output.result_store_dir)
|