matimo-core 0.1.0__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.
- matimo/__init__.py +318 -0
- matimo/approval/__init__.py +0 -0
- matimo/approval/handler.py +152 -0
- matimo/auth/__init__.py +27 -0
- matimo/auth/injection.py +141 -0
- matimo/auth/oauth2_config.py +76 -0
- matimo/auth/oauth2_handler.py +291 -0
- matimo/auth/oauth2_provider_loader.py +103 -0
- matimo/core/__init__.py +0 -0
- matimo/core/loader.py +215 -0
- matimo/core/models.py +512 -0
- matimo/core/registry.py +162 -0
- matimo/core/skill_content_parser.py +242 -0
- matimo/core/skill_loader.py +338 -0
- matimo/core/skill_registry.py +308 -0
- matimo/core/tfidf_embedding.py +155 -0
- matimo/decorators/__init__.py +133 -0
- matimo/encodings/__init__.py +0 -0
- matimo/encodings/parameter_encoding.py +160 -0
- matimo/errors.py +96 -0
- matimo/executors/__init__.py +0 -0
- matimo/executors/command_executor.py +154 -0
- matimo/executors/function_executor.py +193 -0
- matimo/executors/http_executor.py +292 -0
- matimo/instance.py +623 -0
- matimo/integrations/__init__.py +0 -0
- matimo/integrations/_pydantic_utils.py +79 -0
- matimo/integrations/crewai.py +137 -0
- matimo/integrations/langchain.py +190 -0
- matimo/logging/__init__.py +194 -0
- matimo/mcp/README.md +580 -0
- matimo/mcp/__init__.py +6 -0
- matimo/mcp/secrets/__init__.py +271 -0
- matimo/mcp/secrets/types.py +23 -0
- matimo/mcp/server.py +490 -0
- matimo/mcp/tool_converter.py +120 -0
- matimo/policy/__init__.py +0 -0
- matimo/policy/approval_manifest.py +205 -0
- matimo/policy/content_validator.py +212 -0
- matimo/policy/default_policy.py +325 -0
- matimo/policy/integrity_tracker.py +92 -0
- matimo/policy/policy_loader.py +107 -0
- matimo/policy/risk_classifier.py +47 -0
- matimo/policy/types.py +193 -0
- matimo/sync.py +122 -0
- matimo/tools/calculator/calculator.py +36 -0
- matimo/tools/calculator/definition.yaml +70 -0
- matimo/tools/edit/definition.yaml +108 -0
- matimo/tools/edit/edit.py +79 -0
- matimo/tools/execute/definition.yaml +90 -0
- matimo/tools/execute/execute.py +74 -0
- matimo/tools/matimo_approve_tool/definition.yaml +36 -0
- matimo/tools/matimo_approve_tool/matimo_approve_tool.py +61 -0
- matimo/tools/matimo_create_skill/definition.yaml +46 -0
- matimo/tools/matimo_create_skill/matimo_create_skill.py +64 -0
- matimo/tools/matimo_create_tool/definition.yaml +48 -0
- matimo/tools/matimo_create_tool/matimo_create_tool.py +101 -0
- matimo/tools/matimo_get_skill/definition.yaml +60 -0
- matimo/tools/matimo_get_skill/matimo_get_skill.py +119 -0
- matimo/tools/matimo_get_tool/definition.yaml +36 -0
- matimo/tools/matimo_get_tool/matimo_get_tool.py +48 -0
- matimo/tools/matimo_get_tool_status/definition.yaml +42 -0
- matimo/tools/matimo_get_tool_status/matimo_get_tool_status.py +65 -0
- matimo/tools/matimo_list_skills/definition.yaml +50 -0
- matimo/tools/matimo_list_skills/matimo_list_skills.py +95 -0
- matimo/tools/matimo_list_user_tools/definition.yaml +32 -0
- matimo/tools/matimo_list_user_tools/matimo_list_user_tools.py +51 -0
- matimo/tools/matimo_reload_tools/definition.yaml +35 -0
- matimo/tools/matimo_reload_tools/matimo_reload_tools.py +40 -0
- matimo/tools/matimo_search_tools/definition.yaml +32 -0
- matimo/tools/matimo_search_tools/matimo_search_tools.py +73 -0
- matimo/tools/matimo_validate_skill/definition.yaml +43 -0
- matimo/tools/matimo_validate_skill/matimo_validate_skill.py +134 -0
- matimo/tools/matimo_validate_tool/definition.yaml +34 -0
- matimo/tools/matimo_validate_tool/matimo_validate_tool.py +73 -0
- matimo/tools/read/definition.yaml +101 -0
- matimo/tools/read/read.py +53 -0
- matimo/tools/search/definition.yaml +132 -0
- matimo/tools/search/search.py +106 -0
- matimo/tools/web/definition.yaml +124 -0
- matimo/tools/web/web.py +82 -0
- matimo_core-0.1.0.dist-info/METADATA +324 -0
- matimo_core-0.1.0.dist-info/RECORD +85 -0
- matimo_core-0.1.0.dist-info/WHEEL +4 -0
- matimo_core-0.1.0.dist-info/entry_points.txt +2 -0
matimo/__init__.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matimo Python SDK — Public API
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
Write tools once in YAML, use them everywhere.
|
|
6
|
+
|
|
7
|
+
Quick start:
|
|
8
|
+
from matimo import Matimo
|
|
9
|
+
|
|
10
|
+
matimo = await Matimo.init('./tools')
|
|
11
|
+
result = await matimo.execute('my_tool', {'param': 'value'})
|
|
12
|
+
|
|
13
|
+
LangChain integration:
|
|
14
|
+
from matimo import Matimo, convert_tools_to_langchain
|
|
15
|
+
matimo = await Matimo.init('./tools')
|
|
16
|
+
tools = convert_tools_to_langchain(matimo.list_tools(), matimo)
|
|
17
|
+
|
|
18
|
+
CrewAI integration:
|
|
19
|
+
from matimo import Matimo, convert_tools_to_crewai
|
|
20
|
+
matimo = await Matimo.init('./tools')
|
|
21
|
+
tools = convert_tools_to_crewai(matimo.list_tools(), matimo)
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import importlib.resources
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from matimo.instance import Matimo
|
|
31
|
+
|
|
32
|
+
# Approval
|
|
33
|
+
from matimo.approval.handler import (
|
|
34
|
+
ApprovalCallback,
|
|
35
|
+
ApprovalHandler,
|
|
36
|
+
ApprovalRequest,
|
|
37
|
+
get_global_approval_handler,
|
|
38
|
+
set_global_approval_handler,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Auth
|
|
42
|
+
from matimo.auth.injection import extract_parameter_placeholders, inject_auth_parameters
|
|
43
|
+
from matimo.auth.oauth2_config import (
|
|
44
|
+
AuthorizationOptions,
|
|
45
|
+
OAuth2Config,
|
|
46
|
+
OAuth2Token,
|
|
47
|
+
TokenResponse,
|
|
48
|
+
)
|
|
49
|
+
from matimo.auth.oauth2_handler import OAuth2Handler
|
|
50
|
+
from matimo.auth.oauth2_provider_loader import OAuth2ProviderLoader
|
|
51
|
+
|
|
52
|
+
# Core loading + registry
|
|
53
|
+
from matimo.core.loader import ToolLoader
|
|
54
|
+
|
|
55
|
+
# Core models
|
|
56
|
+
from matimo.core.models import (
|
|
57
|
+
AuthConfig,
|
|
58
|
+
AuthType,
|
|
59
|
+
BundledResources,
|
|
60
|
+
CommandExecution,
|
|
61
|
+
ExecuteOptions,
|
|
62
|
+
ExecutionResult,
|
|
63
|
+
FunctionExecution,
|
|
64
|
+
HttpExecution,
|
|
65
|
+
OutputSchema,
|
|
66
|
+
Parameter,
|
|
67
|
+
ParameterEncoding,
|
|
68
|
+
ParameterEncodingConfig,
|
|
69
|
+
ParameterEncodingType,
|
|
70
|
+
ParameterType,
|
|
71
|
+
ParsedSkill,
|
|
72
|
+
PolicyContext,
|
|
73
|
+
ProviderDefinition,
|
|
74
|
+
RateLimitConfig,
|
|
75
|
+
SearchSkillsOptions,
|
|
76
|
+
SkillContentOptions,
|
|
77
|
+
SkillDefinition,
|
|
78
|
+
SkillFrontmatter,
|
|
79
|
+
SkillSection,
|
|
80
|
+
SkillSummary,
|
|
81
|
+
ToolDefinition,
|
|
82
|
+
ToolExample,
|
|
83
|
+
ToolStatus,
|
|
84
|
+
ValidationError,
|
|
85
|
+
ValidationResult,
|
|
86
|
+
)
|
|
87
|
+
from matimo.core.registry import ToolRegistry
|
|
88
|
+
|
|
89
|
+
# Skills
|
|
90
|
+
from matimo.core.skill_content_parser import (
|
|
91
|
+
ParsedSkillContent,
|
|
92
|
+
extract_skill_content,
|
|
93
|
+
list_skill_sections,
|
|
94
|
+
parse_skill_sections,
|
|
95
|
+
)
|
|
96
|
+
from matimo.core.skill_loader import SkillLoader, extract_skill_metadata, parse_skill_content
|
|
97
|
+
from matimo.core.skill_registry import SemanticSearchResult, SkillRegistry
|
|
98
|
+
from matimo.core.tfidf_embedding import (
|
|
99
|
+
EmbeddingProvider,
|
|
100
|
+
TfIdfEmbeddingProvider,
|
|
101
|
+
cosine_similarity,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Decorators
|
|
105
|
+
from matimo.decorators import (
|
|
106
|
+
get_global_matimo_instance,
|
|
107
|
+
set_global_matimo_instance,
|
|
108
|
+
tool,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Encodings
|
|
112
|
+
from matimo.encodings.parameter_encoding import apply_parameter_encodings
|
|
113
|
+
|
|
114
|
+
# Errors
|
|
115
|
+
from matimo.errors import (
|
|
116
|
+
ErrorCode,
|
|
117
|
+
MatimoError,
|
|
118
|
+
create_execution_error,
|
|
119
|
+
create_validation_error,
|
|
120
|
+
from_http_error,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Executors
|
|
124
|
+
from matimo.executors.command_executor import CommandExecutor
|
|
125
|
+
from matimo.executors.function_executor import FunctionExecutor
|
|
126
|
+
from matimo.executors.http_executor import HttpExecutor
|
|
127
|
+
|
|
128
|
+
# Main entry point + sync API
|
|
129
|
+
from matimo.instance import InitOptions, Matimo, ReloadResult, matimo
|
|
130
|
+
|
|
131
|
+
# Logging
|
|
132
|
+
from matimo.logging import (
|
|
133
|
+
MatimoLogger,
|
|
134
|
+
get_global_matimo_logger,
|
|
135
|
+
set_global_matimo_logger,
|
|
136
|
+
setup_logger,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# MCP
|
|
140
|
+
from matimo.mcp.secrets import (
|
|
141
|
+
AwsSecretsManagerResolver,
|
|
142
|
+
DotenvSecretResolver,
|
|
143
|
+
EnvSecretResolver,
|
|
144
|
+
SecretResolverChain,
|
|
145
|
+
VaultSecretResolver,
|
|
146
|
+
create_resolver_chain,
|
|
147
|
+
)
|
|
148
|
+
from matimo.mcp.server import MCPServer, MCPServerOptions, create_mcp_server
|
|
149
|
+
from matimo.mcp.tool_converter import convert_parameters_to_mcp_schema
|
|
150
|
+
|
|
151
|
+
# Policy
|
|
152
|
+
from matimo.policy.approval_manifest import ApprovalManifest, ApprovalRecord
|
|
153
|
+
from matimo.policy.content_validator import ContentViolation, validate_tool_content
|
|
154
|
+
from matimo.policy.default_policy import DefaultPolicyEngine, PolicyEngine, get_tier_for_tool
|
|
155
|
+
from matimo.policy.integrity_tracker import IntegrityAction, ToolIntegrityTracker
|
|
156
|
+
from matimo.policy.policy_loader import load_policy_from_file
|
|
157
|
+
from matimo.policy.risk_classifier import classify_risk
|
|
158
|
+
from matimo.policy.types import (
|
|
159
|
+
HITLCallback,
|
|
160
|
+
HITLRequest,
|
|
161
|
+
MatimoEvent,
|
|
162
|
+
MatimoEventHandler,
|
|
163
|
+
PolicyAllowed,
|
|
164
|
+
PolicyConfig,
|
|
165
|
+
PolicyDecision,
|
|
166
|
+
PolicyDenied,
|
|
167
|
+
PolicyPendingApproval,
|
|
168
|
+
PolicyTier,
|
|
169
|
+
RiskLevel,
|
|
170
|
+
)
|
|
171
|
+
from matimo.sync import MatimoSync
|
|
172
|
+
|
|
173
|
+
__version__ = "0.1.0"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_core_tools_path() -> str:
|
|
177
|
+
"""Return the absolute path to the bundled core tool definitions.
|
|
178
|
+
|
|
179
|
+
Used as a ``matimo.providers`` entry point so that core meta-tools
|
|
180
|
+
(execute, read, edit, search, web, calculator, matimo_create_tool, …)
|
|
181
|
+
are auto-discovered by :meth:`matimo.Matimo.init` when
|
|
182
|
+
``auto_discover=True``.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
ref = importlib.resources.files("matimo") / "tools"
|
|
186
|
+
return str(ref)
|
|
187
|
+
except Exception:
|
|
188
|
+
return str(Path(__file__).parent / "tools")
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Integrations (lazy — raise ImportError with hint on missing optional dep)
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def convert_tools_to_langchain(
|
|
196
|
+
tools: list[Any],
|
|
197
|
+
matimo_instance: Matimo,
|
|
198
|
+
credentials: dict[str, str] | None = None,
|
|
199
|
+
) -> list[Any]:
|
|
200
|
+
"""Convert Matimo tools to LangChain StructuredTool list. Requires langchain-core."""
|
|
201
|
+
from matimo.integrations.langchain import convert_tools_to_langchain as _inner
|
|
202
|
+
return _inner(tools, matimo_instance, credentials)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_skills_metadata(matimo_instance: Matimo) -> list[dict[str, str]]:
|
|
206
|
+
"""Return Level-1 metadata (name + description) for all available skills."""
|
|
207
|
+
from matimo.integrations.langchain import get_skills_metadata as _inner
|
|
208
|
+
return _inner(matimo_instance)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def build_relevant_skill_prompt(
|
|
212
|
+
matimo_instance: Matimo,
|
|
213
|
+
query: str,
|
|
214
|
+
*,
|
|
215
|
+
top_k: int = 3,
|
|
216
|
+
min_score: float = 0.3,
|
|
217
|
+
header: str | None = None,
|
|
218
|
+
) -> str:
|
|
219
|
+
"""Build a per-request skill context prompt using TF-IDF semantic search."""
|
|
220
|
+
from matimo.integrations.langchain import build_relevant_skill_prompt as _inner
|
|
221
|
+
return await _inner(matimo_instance, query, top_k=top_k, min_score=min_score, header=header)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def convert_tools_to_crewai(
|
|
225
|
+
tools: list[Any],
|
|
226
|
+
matimo_instance: Matimo,
|
|
227
|
+
credentials: dict[str, str] | None = None,
|
|
228
|
+
) -> list[Any]:
|
|
229
|
+
"""Convert Matimo tools to CrewAI BaseTool list. Requires crewai."""
|
|
230
|
+
from matimo.integrations.crewai import convert_tools_to_crewai as _inner
|
|
231
|
+
return _inner(tools, matimo_instance, credentials)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# Decorators
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Errors
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Main entry point
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Logging
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Synchronous API
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
# Core models
|
|
256
|
+
"ToolDefinition", "Parameter", "ParameterType", "AuthConfig", "AuthType",
|
|
257
|
+
"HttpExecution", "CommandExecution", "FunctionExecution",
|
|
258
|
+
"OutputSchema", "RateLimitConfig", "ToolExample",
|
|
259
|
+
"ParameterEncoding", "ParameterEncodingConfig", "ParameterEncodingType",
|
|
260
|
+
"ToolStatus",
|
|
261
|
+
"ExecutionResult", "ExecuteOptions", "PolicyContext",
|
|
262
|
+
"ValidationError", "ValidationResult",
|
|
263
|
+
"ParsedSkill", "SkillFrontmatter", "ProviderDefinition",
|
|
264
|
+
"SkillDefinition", "SkillSummary", "SkillSection", "SkillContentOptions",
|
|
265
|
+
"SearchSkillsOptions", "BundledResources",
|
|
266
|
+
# Core
|
|
267
|
+
"ToolLoader", "ToolRegistry",
|
|
268
|
+
# Skills
|
|
269
|
+
"SkillLoader", "SkillRegistry",
|
|
270
|
+
"parse_skill_content", "extract_skill_metadata",
|
|
271
|
+
"parse_skill_sections", "extract_skill_content", "list_skill_sections",
|
|
272
|
+
"ParsedSkillContent", "SemanticSearchResult",
|
|
273
|
+
"TfIdfEmbeddingProvider", "EmbeddingProvider", "cosine_similarity",
|
|
274
|
+
# Executors
|
|
275
|
+
"HttpExecutor", "CommandExecutor", "FunctionExecutor",
|
|
276
|
+
# Auth
|
|
277
|
+
"inject_auth_parameters", "extract_parameter_placeholders",
|
|
278
|
+
"OAuth2Handler", "OAuth2ProviderLoader",
|
|
279
|
+
"OAuth2Config", "OAuth2Token", "AuthorizationOptions", "TokenResponse",
|
|
280
|
+
# Approval
|
|
281
|
+
"ApprovalHandler", "ApprovalRequest", "ApprovalCallback",
|
|
282
|
+
"get_global_approval_handler", "set_global_approval_handler",
|
|
283
|
+
# Encodings
|
|
284
|
+
"apply_parameter_encodings",
|
|
285
|
+
# Policy
|
|
286
|
+
"PolicyEngine", "DefaultPolicyEngine", "PolicyConfig",
|
|
287
|
+
"PolicyDecision", "PolicyAllowed", "PolicyDenied", "PolicyPendingApproval",
|
|
288
|
+
"RiskLevel", "PolicyTier",
|
|
289
|
+
"MatimoEvent", "MatimoEventHandler", "HITLCallback", "HITLRequest",
|
|
290
|
+
"ContentViolation", "validate_tool_content",
|
|
291
|
+
"classify_risk", "get_tier_for_tool",
|
|
292
|
+
"ToolIntegrityTracker", "IntegrityAction",
|
|
293
|
+
"ApprovalManifest", "ApprovalRecord",
|
|
294
|
+
"load_policy_from_file",
|
|
295
|
+
# MCP
|
|
296
|
+
"MCPServer", "MCPServerOptions", "create_mcp_server",
|
|
297
|
+
"convert_parameters_to_mcp_schema",
|
|
298
|
+
"EnvSecretResolver", "DotenvSecretResolver",
|
|
299
|
+
"VaultSecretResolver", "AwsSecretsManagerResolver",
|
|
300
|
+
"SecretResolverChain", "create_resolver_chain",
|
|
301
|
+
# Integrations
|
|
302
|
+
"convert_tools_to_langchain",
|
|
303
|
+
"get_skills_metadata",
|
|
304
|
+
"build_relevant_skill_prompt",
|
|
305
|
+
"convert_tools_to_crewai",
|
|
306
|
+
# Decorators
|
|
307
|
+
"tool", "set_global_matimo_instance", "get_global_matimo_instance",
|
|
308
|
+
# Logging
|
|
309
|
+
"MatimoLogger", "setup_logger",
|
|
310
|
+
"get_global_matimo_logger", "set_global_matimo_logger",
|
|
311
|
+
# Errors
|
|
312
|
+
"MatimoError", "ErrorCode",
|
|
313
|
+
"create_execution_error", "create_validation_error", "from_http_error",
|
|
314
|
+
# Instance
|
|
315
|
+
"Matimo", "MatimoSync", "matimo", "InitOptions", "ReloadResult",
|
|
316
|
+
# Core tools path (entry point)
|
|
317
|
+
"get_core_tools_path",
|
|
318
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval handler — human-in-the-loop approval gating for sensitive tools.
|
|
3
|
+
Mirrors: packages/core/src/approval/approval-handler.ts
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import fnmatch
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("matimo")
|
|
15
|
+
|
|
16
|
+
# Destructive action keywords that trigger approval prompts
|
|
17
|
+
DEFAULT_DESTRUCTIVE_KEYWORDS: list[str] = [
|
|
18
|
+
"CREATE", "DELETE", "DESTROY", "DROP", "ALTER", "TRUNCATE", "UPDATE",
|
|
19
|
+
"INSERT", "UPSERT", "REPLACE", "MERGE", "GRANT", "REVOKE",
|
|
20
|
+
"EDIT", "WRITE", "APPEND", "REMOVE", "PURGE", "RENAME", "SHUTDOWN",
|
|
21
|
+
"EXECUTE", "EXEC",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ApprovalRequest:
|
|
27
|
+
"""Represents a pending approval for a tool execution."""
|
|
28
|
+
|
|
29
|
+
tool_name: str
|
|
30
|
+
description: str | None
|
|
31
|
+
params: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ApprovalCallback = Callable[[ApprovalRequest], Awaitable[bool]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApprovalHandler:
|
|
38
|
+
"""
|
|
39
|
+
Manages interactive approval gating for sensitive tool executions.
|
|
40
|
+
Mirrors: ApprovalHandler in approval-handler.ts
|
|
41
|
+
|
|
42
|
+
Approval flow:
|
|
43
|
+
1. auto_approve=True (MATIMO_AUTO_APPROVE env) → always approve
|
|
44
|
+
2. tool_name matches an approved pattern → approve
|
|
45
|
+
3. HITL callback set → invoke it and return its decision
|
|
46
|
+
4. Default → deny
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self.auto_approve: bool = (
|
|
51
|
+
os.environ.get("MATIMO_AUTO_APPROVE", "").lower() == "true"
|
|
52
|
+
)
|
|
53
|
+
self.approved_patterns: set[str] = self._load_approved_patterns()
|
|
54
|
+
self.destructive_keywords: list[str] = list(DEFAULT_DESTRUCTIVE_KEYWORDS)
|
|
55
|
+
self._callback: ApprovalCallback | None = None
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Public API
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def set_approval_callback(self, callback: ApprovalCallback) -> None:
|
|
62
|
+
"""Wire an async approval callback (e.g., Slack DM, CLI prompt)."""
|
|
63
|
+
self._callback = callback
|
|
64
|
+
|
|
65
|
+
async def request_approval(self, request: ApprovalRequest) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Gate execution on approval.
|
|
68
|
+
Returns True if approved, False if denied.
|
|
69
|
+
"""
|
|
70
|
+
# 1. Hard auto-approve (CI / testing)
|
|
71
|
+
if self.auto_approve:
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Auto-approving tool '%s' (MATIMO_AUTO_APPROVE=true)", request.tool_name
|
|
74
|
+
)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
# 2. Pattern allowlist
|
|
78
|
+
if self._matches_approved_pattern(request.tool_name):
|
|
79
|
+
logger.debug(
|
|
80
|
+
"Tool '%s' matches approved pattern — skipping approval prompt",
|
|
81
|
+
request.tool_name,
|
|
82
|
+
)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# 3. HITL callback
|
|
86
|
+
if self._callback is not None:
|
|
87
|
+
approved = await self._callback(request)
|
|
88
|
+
if not approved:
|
|
89
|
+
logger.info(
|
|
90
|
+
"Approval denied for tool '%s' by callback", request.tool_name
|
|
91
|
+
)
|
|
92
|
+
return approved
|
|
93
|
+
|
|
94
|
+
# 4. No callback → default deny
|
|
95
|
+
logger.warning(
|
|
96
|
+
"Approval required for tool '%s' but no callback configured — denying",
|
|
97
|
+
request.tool_name,
|
|
98
|
+
)
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def is_destructive(self, tool_name: str, params: dict[str, Any]) -> bool:
|
|
102
|
+
"""
|
|
103
|
+
Heuristically determine whether a tool invocation is destructive,
|
|
104
|
+
based on keyword scanning of the tool name and string parameter values.
|
|
105
|
+
"""
|
|
106
|
+
combined = tool_name.upper()
|
|
107
|
+
for v in params.values():
|
|
108
|
+
if isinstance(v, str):
|
|
109
|
+
combined += " " + v.upper()
|
|
110
|
+
|
|
111
|
+
return any(kw in combined for kw in self.destructive_keywords)
|
|
112
|
+
|
|
113
|
+
def add_approved_pattern(self, pattern: str) -> None:
|
|
114
|
+
"""Add a glob pattern to the pre-approved tool allowlist."""
|
|
115
|
+
self.approved_patterns.add(pattern)
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Internal
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def _load_approved_patterns(self) -> set[str]:
|
|
122
|
+
raw = os.environ.get("MATIMO_APPROVED_PATTERNS", "")
|
|
123
|
+
if not raw:
|
|
124
|
+
return set()
|
|
125
|
+
return {p.strip() for p in raw.split(",") if p.strip()}
|
|
126
|
+
|
|
127
|
+
def _matches_approved_pattern(self, tool_name: str) -> bool:
|
|
128
|
+
return any(
|
|
129
|
+
fnmatch.fnmatch(tool_name, pattern)
|
|
130
|
+
for pattern in self.approved_patterns
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Module-level singleton
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
_global_handler: ApprovalHandler | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_global_approval_handler() -> ApprovalHandler:
|
|
142
|
+
"""Return the global ApprovalHandler, creating one if necessary."""
|
|
143
|
+
global _global_handler
|
|
144
|
+
if _global_handler is None:
|
|
145
|
+
_global_handler = ApprovalHandler()
|
|
146
|
+
return _global_handler
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def set_global_approval_handler(handler: ApprovalHandler) -> None:
|
|
150
|
+
"""Replace the global approval handler (useful in tests)."""
|
|
151
|
+
global _global_handler
|
|
152
|
+
_global_handler = handler
|
matimo/auth/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Matimo auth module — injection helpers and OAuth2 support.
|
|
3
|
+
|
|
4
|
+
Mirrors: packages/core/src/auth/
|
|
5
|
+
"""
|
|
6
|
+
from matimo.auth.injection import extract_parameter_placeholders, inject_auth_parameters
|
|
7
|
+
from matimo.auth.oauth2_config import (
|
|
8
|
+
AuthorizationOptions,
|
|
9
|
+
OAuth2Config,
|
|
10
|
+
OAuth2Token,
|
|
11
|
+
TokenResponse,
|
|
12
|
+
)
|
|
13
|
+
from matimo.auth.oauth2_handler import OAuth2Handler
|
|
14
|
+
from matimo.auth.oauth2_provider_loader import OAuth2ProviderLoader
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
# Injection
|
|
18
|
+
"inject_auth_parameters",
|
|
19
|
+
"extract_parameter_placeholders",
|
|
20
|
+
# OAuth2
|
|
21
|
+
"AuthorizationOptions",
|
|
22
|
+
"OAuth2Config",
|
|
23
|
+
"OAuth2Handler",
|
|
24
|
+
"OAuth2ProviderLoader",
|
|
25
|
+
"OAuth2Token",
|
|
26
|
+
"TokenResponse",
|
|
27
|
+
]
|
matimo/auth/injection.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth parameter injection.
|
|
3
|
+
Mirrors: MatimoInstance.injectAuthParameters() in matimo-instance.ts
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from matimo.core.models import ToolDefinition
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("matimo")
|
|
15
|
+
|
|
16
|
+
# Patterns that indicate a parameter placeholder is auth-related
|
|
17
|
+
_AUTH_PATTERNS = (
|
|
18
|
+
"token", "key", "secret", "password", "credential",
|
|
19
|
+
"auth", "bearer", "api_key",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_PLACEHOLDER_RE = re.compile(r"\{([^}]+)\}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def inject_auth_parameters(
|
|
26
|
+
tool: ToolDefinition,
|
|
27
|
+
params: dict[str, Any],
|
|
28
|
+
credentials: dict[str, str] | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Scan all placeholders in the tool's execution config (URL, headers, body,
|
|
32
|
+
query_params). For any placeholder that looks auth-related and has no value
|
|
33
|
+
in params, attempt to resolve it from:
|
|
34
|
+
1. per-call credentials dict
|
|
35
|
+
2. process environment (MATIMO_{TOOL_NAME}_{KEY} or just {KEY})
|
|
36
|
+
|
|
37
|
+
SECURITY: injected values are never logged.
|
|
38
|
+
|
|
39
|
+
Returns a new params dict with the resolved auth values merged in.
|
|
40
|
+
"""
|
|
41
|
+
placeholders = extract_parameter_placeholders(tool)
|
|
42
|
+
result = dict(params)
|
|
43
|
+
|
|
44
|
+
for placeholder in placeholders:
|
|
45
|
+
# Already supplied by the caller
|
|
46
|
+
if placeholder in result:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
lower = placeholder.lower()
|
|
50
|
+
is_auth = any(pattern in lower for pattern in _AUTH_PATTERNS)
|
|
51
|
+
if not is_auth:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# Attempt resolution
|
|
55
|
+
value = _resolve_auth_value(tool.name, placeholder, credentials)
|
|
56
|
+
if value is not None:
|
|
57
|
+
result[placeholder] = value
|
|
58
|
+
logger.debug(
|
|
59
|
+
"Injected auth parameter '%s' for tool '%s'",
|
|
60
|
+
placeholder, tool.name
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_parameter_placeholders(tool: ToolDefinition) -> set[str]:
|
|
67
|
+
"""
|
|
68
|
+
Extract all {placeholder} names from the tool's execution config
|
|
69
|
+
(url, headers, body, query_params, params, args).
|
|
70
|
+
Mirrors: extractParameterPlaceholders() in matimo-instance.ts
|
|
71
|
+
"""
|
|
72
|
+
placeholders: set[str] = set()
|
|
73
|
+
exec_cfg = tool.execution
|
|
74
|
+
exec_type = exec_cfg.type
|
|
75
|
+
|
|
76
|
+
if exec_type == "http":
|
|
77
|
+
_scan_string(exec_cfg.url, placeholders) # type: ignore[attr-defined]
|
|
78
|
+
_scan_object(exec_cfg.headers, placeholders) # type: ignore[attr-defined]
|
|
79
|
+
_scan_object(exec_cfg.body, placeholders) # type: ignore[attr-defined]
|
|
80
|
+
_scan_object(exec_cfg.query_params, placeholders) # type: ignore[attr-defined]
|
|
81
|
+
_scan_object(exec_cfg.params, placeholders) # type: ignore[attr-defined]
|
|
82
|
+
|
|
83
|
+
elif exec_type == "command":
|
|
84
|
+
_scan_string(exec_cfg.command, placeholders) # type: ignore[attr-defined]
|
|
85
|
+
for arg in exec_cfg.args or []: # type: ignore[attr-defined]
|
|
86
|
+
_scan_string(arg, placeholders)
|
|
87
|
+
|
|
88
|
+
elif exec_type == "function":
|
|
89
|
+
# Function tools may have params passed directly — nothing to scan
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
return placeholders
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Internal helpers
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_auth_value(
|
|
101
|
+
tool_name: str,
|
|
102
|
+
placeholder: str,
|
|
103
|
+
credentials: dict[str, str] | None,
|
|
104
|
+
) -> str | None:
|
|
105
|
+
"""
|
|
106
|
+
Try to resolve an auth placeholder in priority order:
|
|
107
|
+
1. per-call credentials
|
|
108
|
+
2. env MATIMO_{TOOL_NAME_UPPER}_{PLACEHOLDER_UPPER}
|
|
109
|
+
3. env {PLACEHOLDER} directly
|
|
110
|
+
"""
|
|
111
|
+
if credentials and placeholder in credentials:
|
|
112
|
+
return credentials[placeholder]
|
|
113
|
+
|
|
114
|
+
# e.g. MATIMO_SLACK_SLACK_BOT_TOKEN
|
|
115
|
+
env_key_prefixed = f"MATIMO_{tool_name.upper()}_{placeholder.upper()}"
|
|
116
|
+
val = os.environ.get(env_key_prefixed)
|
|
117
|
+
if val:
|
|
118
|
+
return val
|
|
119
|
+
|
|
120
|
+
# env directly (e.g. SLACK_BOT_TOKEN)
|
|
121
|
+
return os.environ.get(placeholder)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _scan_string(value: str | None, out: set[str]) -> None:
|
|
125
|
+
if value is None:
|
|
126
|
+
return
|
|
127
|
+
for m in _PLACEHOLDER_RE.finditer(value):
|
|
128
|
+
out.add(m.group(1))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _scan_object(obj: object, out: set[str]) -> None:
|
|
132
|
+
if obj is None:
|
|
133
|
+
return
|
|
134
|
+
if isinstance(obj, str):
|
|
135
|
+
_scan_string(obj, out)
|
|
136
|
+
elif isinstance(obj, dict):
|
|
137
|
+
for v in obj.values():
|
|
138
|
+
_scan_object(v, out)
|
|
139
|
+
elif isinstance(obj, list):
|
|
140
|
+
for item in obj:
|
|
141
|
+
_scan_object(item, out)
|