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.
Files changed (34) hide show
  1. onetool/cli.py +2 -0
  2. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/METADATA +26 -33
  3. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/RECORD +31 -33
  4. ot/config/__init__.py +90 -48
  5. ot/config/global_templates/__init__.py +2 -2
  6. ot/config/global_templates/diagram-templates/api-flow.mmd +33 -33
  7. ot/config/global_templates/diagram-templates/c4-context.puml +30 -30
  8. ot/config/global_templates/diagram-templates/class-diagram.mmd +87 -87
  9. ot/config/global_templates/diagram-templates/feature-mindmap.mmd +70 -70
  10. ot/config/global_templates/diagram-templates/microservices.d2 +81 -81
  11. ot/config/global_templates/diagram-templates/project-gantt.mmd +37 -37
  12. ot/config/global_templates/diagram-templates/state-machine.mmd +42 -42
  13. ot/config/global_templates/diagram.yaml +167 -167
  14. ot/config/global_templates/onetool.yaml +2 -0
  15. ot/config/global_templates/prompts.yaml +102 -102
  16. ot/config/global_templates/security.yaml +1 -4
  17. ot/config/global_templates/servers.yaml +1 -1
  18. ot/config/global_templates/tool_templates/__init__.py +7 -7
  19. ot/config/loader.py +226 -869
  20. ot/config/models.py +735 -0
  21. ot/config/secrets.py +243 -192
  22. ot/executor/tool_loader.py +10 -1
  23. ot/executor/validator.py +11 -1
  24. ot/meta.py +338 -33
  25. ot/prompts.py +228 -218
  26. ot/proxy/manager.py +168 -8
  27. ot/registry/__init__.py +199 -189
  28. ot/config/dynamic.py +0 -121
  29. ot/config/mcp.py +0 -149
  30. ot/config/tool_config.py +0 -125
  31. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/WHEEL +0 -0
  32. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/entry_points.txt +0 -0
  33. {onetool_mcp-1.0.0rc2.dist-info → onetool_mcp-1.0.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
  34. {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 get_secrets()
8
- 2. OT_SECRETS_FILE environment variable
9
- 3. Config's secrets_file setting (if config loaded and file exists)
10
- 4. Default locations: project (.onetool/config/secrets.yaml) then global (~/.onetool/config/secrets.yaml)
11
-
12
- Example secrets.yaml:
13
-
14
- BRAVE_API_KEY: "your-brave-api-key"
15
- OPENAI_API_KEY: "sk-..."
16
- DATABASE_URL: "postgresql://..."
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import os
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
- def load_secrets(secrets_path: Path | str | None = None) -> dict[str, str]:
32
- """Load secrets from YAML file.
33
-
34
- Args:
35
- secrets_path: Path to secrets file. If None or doesn't exist,
36
- returns empty dict (no secrets).
37
-
38
- Returns:
39
- Dictionary of secret name -> value
40
-
41
- Raises:
42
- ValueError: If YAML is invalid
43
- """
44
- if secrets_path is None:
45
- logger.debug("No secrets path provided")
46
- return {}
47
-
48
- secrets_path = Path(secrets_path)
49
-
50
- if not secrets_path.exists():
51
- logger.debug(f"Secrets file not found: {secrets_path}")
52
- return {}
53
-
54
- logger.debug(f"Loading secrets from {secrets_path}")
55
-
56
- try:
57
- with secrets_path.open() as f:
58
- raw_data = yaml.safe_load(f)
59
- except yaml.YAMLError as e:
60
- raise ValueError(f"Invalid YAML in secrets file {secrets_path}: {e}") from e
61
- except OSError as e:
62
- raise ValueError(f"Error reading secrets file {secrets_path}: {e}") from e
63
-
64
- if raw_data is None:
65
- return {}
66
-
67
- if not isinstance(raw_data, dict):
68
- raise ValueError(
69
- f"Secrets file {secrets_path} must be a YAML mapping, not {type(raw_data).__name__}"
70
- )
71
-
72
- # Values are literal - no env var expansion
73
- secrets: dict[str, str] = {}
74
- for key, value in raw_data.items():
75
- if not isinstance(key, str):
76
- logger.warning(f"Ignoring non-string secret key: {key}")
77
- continue
78
-
79
- if value is None:
80
- continue
81
-
82
- # Store as literal string - no ${VAR} expansion
83
- secrets[key] = str(value)
84
-
85
- logger.info(f"Loaded {len(secrets)} secrets")
86
- return secrets
87
-
88
-
89
- def _load_from_default_locations() -> dict[str, str]:
90
- """Load secrets from default project and global locations.
91
-
92
- Searches in order (first found wins):
93
- 1. Project: {effective_cwd}/.onetool/config/secrets.yaml
94
- 2. Global: ~/.onetool/config/secrets.yaml
95
-
96
- Returns:
97
- Dictionary of secret name -> value (empty if no secrets found)
98
- """
99
- # Import here to avoid circular imports at module level
100
- from ot.paths import CONFIG_SUBDIR, get_effective_cwd, get_global_dir
101
-
102
- # Try project secrets first, then global
103
- paths_to_try = [
104
- get_effective_cwd() / ".onetool" / CONFIG_SUBDIR / "secrets.yaml",
105
- get_global_dir() / CONFIG_SUBDIR / "secrets.yaml",
106
- ]
107
-
108
- for secrets_path in paths_to_try:
109
- if secrets_path.exists():
110
- try:
111
- return load_secrets(secrets_path)
112
- except ValueError as e:
113
- # Silent during bootstrap - don't spam logs
114
- logger.debug(f"Error loading secrets from {secrets_path}: {e}")
115
- continue
116
-
117
- return {}
118
-
119
-
120
- def get_secrets(
121
- secrets_path: Path | str | None = None, reload: bool = False
122
- ) -> dict[str, str]:
123
- """Get or load the cached secrets.
124
-
125
- Resolution order (first match wins):
126
- 1. Explicit secrets_path argument
127
- 2. OT_SECRETS_FILE environment variable
128
- 3. Config's secrets_file setting (if config loaded and file exists)
129
- 4. Default locations (.onetool/config/secrets.yaml)
130
-
131
- Args:
132
- secrets_path: Path to secrets file (only used on first load or reload).
133
- reload: Force reload secrets from disk.
134
-
135
- Returns:
136
- Dictionary of secret name -> value
137
- """
138
- global _secrets
139
-
140
- if _secrets is None or reload:
141
- # Resolution chain: explicit > env var > config (if loaded) > defaults
142
- if secrets_path is None:
143
- # Check OT_SECRETS_FILE env var first (highest priority after explicit path)
144
- env_path = os.getenv("OT_SECRETS_FILE")
145
- if env_path:
146
- secrets_path = env_path
147
-
148
- if secrets_path is None:
149
- # WARNING: Do NOT call get_config() here!
150
- # =========================================
151
- # This function is called during config loading via:
152
- # get_config() → load_config() → expand_secrets() → get_early_secret() → get_secrets()
153
- #
154
- # If we call get_config() here, it triggers config loading again → infinite recursion.
155
- # Instead, we check _config directly - if it's None, config is still loading.
156
- try:
157
- import ot.config.loader
158
-
159
- if ot.config.loader._config is not None:
160
- config_path = ot.config.loader._config.get_secrets_file_path()
161
- if config_path.exists():
162
- secrets_path = config_path
163
- except Exception:
164
- pass # Module not loaded yet, fall through
165
-
166
- # Try default locations if still no path
167
- if secrets_path is None:
168
- loaded = _load_from_default_locations()
169
- if loaded:
170
- _secrets = loaded
171
- return _secrets
172
-
173
- _secrets = load_secrets(secrets_path)
174
-
175
- return _secrets
176
-
177
-
178
- def get_secret(name: str) -> str | None:
179
- """Get a single secret value by name.
180
-
181
- Args:
182
- name: Secret name (e.g., "BRAVE_API_KEY")
183
-
184
- Returns:
185
- Secret value, or None if not found
186
- """
187
- return get_secrets().get(name)
188
-
189
-
190
- # Alias for backward compatibility and semantic clarity during config loading
191
- # Both functions now use the same unified cache
192
- get_early_secret = get_secret
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
@@ -40,7 +40,7 @@ def _get_bundled_tools_dir() -> Path | None:
40
40
 
41
41
 
42
42
  if TYPE_CHECKING:
43
- from ot.config.loader import OneToolConfig
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.loader import SecurityConfig
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()