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/secrets.py
CHANGED
|
@@ -1,192 +1,243 @@
|
|
|
1
|
-
"""Secrets loading for OneTool.
|
|
2
|
-
|
|
3
|
-
Loads secrets from secrets.yaml (gitignored) separate from committed configuration.
|
|
4
|
-
Secrets are passed to workers via JSON-RPC, not exposed as environment variables.
|
|
5
|
-
|
|
6
|
-
The secrets file path is resolved in order:
|
|
7
|
-
1. Explicit path passed to
|
|
8
|
-
2. OT_SECRETS_FILE environment variable
|
|
9
|
-
3.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
import yaml
|
|
25
|
-
from loguru import logger
|
|
26
|
-
|
|
27
|
-
# Single global secrets cache
|
|
28
|
-
_secrets: dict[str, str] | None = None
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
continue
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
1
|
+
"""Secrets loading for OneTool V2 (global-only).
|
|
2
|
+
|
|
3
|
+
Loads secrets from secrets.yaml (gitignored) separate from committed configuration.
|
|
4
|
+
Secrets are passed to workers via JSON-RPC, not exposed as environment variables.
|
|
5
|
+
|
|
6
|
+
The secrets file path is resolved in order:
|
|
7
|
+
1. Explicit path passed to load_secrets()
|
|
8
|
+
2. OT_SECRETS_FILE environment variable
|
|
9
|
+
3. Global location: ~/.onetool/config/secrets.yaml
|
|
10
|
+
|
|
11
|
+
Example secrets.yaml:
|
|
12
|
+
|
|
13
|
+
BRAVE_API_KEY: "your-brave-api-key"
|
|
14
|
+
OPENAI_API_KEY: "sk-..."
|
|
15
|
+
DATABASE_URL: "postgresql://..."
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
# Single global secrets cache
|
|
28
|
+
_secrets: dict[str, str] | None = None
|
|
29
|
+
|
|
30
|
+
# Pre-compiled regex for variable expansion (avoids recompilation on each call)
|
|
31
|
+
_VAR_PATTERN = re.compile(r"\$\{([^}:]+)(?::-([^}]*))?\}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_secrets(secrets_path: Path | str | None = None) -> dict[str, str]:
|
|
35
|
+
"""Load secrets from YAML file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
secrets_path: Path to secrets file. If None or doesn't exist,
|
|
39
|
+
returns empty dict (no secrets).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary of secret name -> value
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If YAML is invalid
|
|
46
|
+
"""
|
|
47
|
+
if secrets_path is None:
|
|
48
|
+
logger.debug("No secrets path provided")
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
secrets_path = Path(secrets_path)
|
|
52
|
+
|
|
53
|
+
if not secrets_path.exists():
|
|
54
|
+
logger.debug(f"Secrets file not found: {secrets_path}")
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
logger.debug(f"Loading secrets from {secrets_path}")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with secrets_path.open() as f:
|
|
61
|
+
raw_data = yaml.safe_load(f)
|
|
62
|
+
except yaml.YAMLError as e:
|
|
63
|
+
raise ValueError(f"Invalid YAML in secrets file {secrets_path}: {e}") from e
|
|
64
|
+
except OSError as e:
|
|
65
|
+
raise ValueError(f"Error reading secrets file {secrets_path}: {e}") from e
|
|
66
|
+
|
|
67
|
+
if raw_data is None:
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
if not isinstance(raw_data, dict):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Secrets file {secrets_path} must be a YAML mapping, not {type(raw_data).__name__}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Values are literal - no env var expansion
|
|
76
|
+
secrets: dict[str, str] = {}
|
|
77
|
+
for key, value in raw_data.items():
|
|
78
|
+
if not isinstance(key, str):
|
|
79
|
+
logger.warning(f"Ignoring non-string secret key: {key}")
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if value is None:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Store as literal string - no ${VAR} expansion
|
|
86
|
+
secrets[key] = str(value)
|
|
87
|
+
|
|
88
|
+
logger.info(f"Loaded {len(secrets)} secrets")
|
|
89
|
+
return secrets
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolve_secrets_path(secrets_path: Path | str | None) -> Path | str | None:
|
|
93
|
+
"""Resolve secrets path from explicit path, env var, or global location.
|
|
94
|
+
|
|
95
|
+
Resolution order (first match wins):
|
|
96
|
+
1. Explicit secrets_path argument
|
|
97
|
+
2. OT_SECRETS_FILE environment variable
|
|
98
|
+
3. Global location (~/.onetool/config/secrets.yaml)
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
secrets_path: Explicit path to secrets file (may be None).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Resolved path or None if no secrets file found.
|
|
105
|
+
"""
|
|
106
|
+
if secrets_path is not None:
|
|
107
|
+
return secrets_path
|
|
108
|
+
|
|
109
|
+
env_path = os.getenv("OT_SECRETS_FILE")
|
|
110
|
+
if env_path:
|
|
111
|
+
return env_path
|
|
112
|
+
|
|
113
|
+
# Check global default location
|
|
114
|
+
from ot.paths import CONFIG_SUBDIR, get_global_dir
|
|
115
|
+
|
|
116
|
+
global_path = get_global_dir() / CONFIG_SUBDIR / "secrets.yaml"
|
|
117
|
+
if global_path.exists():
|
|
118
|
+
return global_path
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_secrets(
|
|
124
|
+
secrets_path: Path | str | None = None, reload: bool = False
|
|
125
|
+
) -> dict[str, str]:
|
|
126
|
+
"""Get or load the cached secrets.
|
|
127
|
+
|
|
128
|
+
Resolution order (first match wins):
|
|
129
|
+
1. Explicit secrets_path argument
|
|
130
|
+
2. OT_SECRETS_FILE environment variable
|
|
131
|
+
3. Global location (~/.onetool/config/secrets.yaml)
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
secrets_path: Path to secrets file (only used on first load or reload).
|
|
135
|
+
reload: Force reload secrets from disk.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary of secret name -> value
|
|
139
|
+
"""
|
|
140
|
+
global _secrets
|
|
141
|
+
|
|
142
|
+
if _secrets is not None and not reload:
|
|
143
|
+
return _secrets
|
|
144
|
+
|
|
145
|
+
resolved_path = _resolve_secrets_path(secrets_path)
|
|
146
|
+
_secrets = load_secrets(resolved_path)
|
|
147
|
+
return _secrets
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_secret(name: str) -> str | None:
|
|
151
|
+
"""Get a single secret value by name.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Secret name (e.g., "BRAVE_API_KEY")
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Secret value, or None if not found
|
|
158
|
+
"""
|
|
159
|
+
return get_secrets().get(name)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def expand_vars(value: str) -> str:
|
|
163
|
+
"""Expand ${VAR} patterns from secrets.yaml first, then config env: section.
|
|
164
|
+
|
|
165
|
+
Variable resolution order (first match wins):
|
|
166
|
+
1. secrets.yaml (sensitive, user-specific values)
|
|
167
|
+
2. config env: section (non-sensitive, shared values)
|
|
168
|
+
3. Default value if provided via ${VAR:-default} syntax
|
|
169
|
+
4. ValueError if no match found
|
|
170
|
+
|
|
171
|
+
Supports ${VAR_NAME} and ${VAR_NAME:-default} syntax.
|
|
172
|
+
No os.environ - all values must be explicit in config.
|
|
173
|
+
|
|
174
|
+
When to use:
|
|
175
|
+
- Tool configuration values that may reference secrets or env vars
|
|
176
|
+
- Subprocess environment variables (after root env + server env merge)
|
|
177
|
+
- Any config value that needs runtime expansion
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
value: String potentially containing ${VAR} patterns.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
String with variables expanded.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: If variable not found and no default provided.
|
|
187
|
+
"""
|
|
188
|
+
missing_vars: list[str] = []
|
|
189
|
+
|
|
190
|
+
def replace(match: re.Match[str]) -> str:
|
|
191
|
+
var_name = match.group(1)
|
|
192
|
+
default_value = match.group(2)
|
|
193
|
+
|
|
194
|
+
# 1. Check secrets first (sensitive, user-specific)
|
|
195
|
+
secret_value = get_secret(var_name)
|
|
196
|
+
if secret_value is not None:
|
|
197
|
+
return secret_value
|
|
198
|
+
|
|
199
|
+
# 2. Check config env: section (non-sensitive, shared)
|
|
200
|
+
try:
|
|
201
|
+
# Import here to avoid circular dependency
|
|
202
|
+
from ot.config.loader import get_config
|
|
203
|
+
|
|
204
|
+
config = get_config()
|
|
205
|
+
env_value = config.env.get(var_name)
|
|
206
|
+
if env_value is not None:
|
|
207
|
+
return env_value
|
|
208
|
+
except Exception:
|
|
209
|
+
# Config not loaded yet or no env section
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
# 3. Use default if provided
|
|
213
|
+
if default_value is not None:
|
|
214
|
+
return default_value
|
|
215
|
+
|
|
216
|
+
# 4. Error - variable not found
|
|
217
|
+
missing_vars.append(var_name)
|
|
218
|
+
return match.group(0)
|
|
219
|
+
|
|
220
|
+
result = _VAR_PATTERN.sub(replace, value)
|
|
221
|
+
|
|
222
|
+
if missing_vars:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Missing variables: {', '.join(missing_vars)}. "
|
|
225
|
+
f"Add them to secrets.yaml or env: section in config, "
|
|
226
|
+
f"or use ${{VAR:-default}} syntax."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# Alias for backward compatibility with external code
|
|
233
|
+
expand_secrets = expand_vars
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def reset() -> None:
|
|
237
|
+
"""Clear secrets cache for reload.
|
|
238
|
+
|
|
239
|
+
Use this as part of the config reload flow to force secrets to be
|
|
240
|
+
reloaded from disk on next access.
|
|
241
|
+
"""
|
|
242
|
+
global _secrets
|
|
243
|
+
_secrets = None
|
ot/executor/tool_loader.py
CHANGED
|
@@ -40,7 +40,7 @@ def _get_bundled_tools_dir() -> Path | None:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
if TYPE_CHECKING:
|
|
43
|
-
from ot.config
|
|
43
|
+
from ot.config import OneToolConfig
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
@@ -394,3 +394,12 @@ def load_tool_functions(tools_dir: Path | None = None) -> dict[str, Any]:
|
|
|
394
394
|
Dictionary mapping function names to callable functions.
|
|
395
395
|
"""
|
|
396
396
|
return load_tool_registry(tools_dir).functions
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def reset() -> None:
|
|
400
|
+
"""Clear tool loader module cache for reload.
|
|
401
|
+
|
|
402
|
+
Use this as part of the config reload flow to force tools to be
|
|
403
|
+
reloaded from disk on next access.
|
|
404
|
+
"""
|
|
405
|
+
_module_cache.clear()
|
ot/executor/validator.py
CHANGED
|
@@ -48,7 +48,7 @@ from functools import lru_cache
|
|
|
48
48
|
from typing import TYPE_CHECKING, Any
|
|
49
49
|
|
|
50
50
|
if TYPE_CHECKING:
|
|
51
|
-
from ot.config.
|
|
51
|
+
from ot.config.models import SecurityConfig
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
@dataclass
|
|
@@ -835,3 +835,13 @@ def get_security_summary() -> dict[str, Any]:
|
|
|
835
835
|
},
|
|
836
836
|
"tool_namespaces": sorted(tool_namespaces),
|
|
837
837
|
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def reset() -> None:
|
|
841
|
+
"""Clear validator caches for reload.
|
|
842
|
+
|
|
843
|
+
Use this as part of the config reload flow to force security config
|
|
844
|
+
and tool namespaces to be reloaded on next access.
|
|
845
|
+
"""
|
|
846
|
+
_get_security_config.cache_clear()
|
|
847
|
+
_get_tool_namespaces.cache_clear()
|