tweek 0.3.1__py3-none-any.whl → 0.4.1__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 (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +170 -74
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
  61. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/mcp/server.py DELETED
@@ -1,320 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Tweek MCP Gateway Server
4
-
5
- Minimal MCP server exposing only tools that add genuinely new capabilities
6
- not available as built-in desktop client tools:
7
- - tweek_vault: Secure keychain credential retrieval
8
- - tweek_status: Security status and activity reporting
9
-
10
- Desktop clients' built-in tools (Bash, Read, Write, etc.) cannot be
11
- intercepted via MCP. For upstream MCP server interception, use the
12
- proxy mode: tweek mcp proxy
13
-
14
- Usage:
15
- tweek mcp serve # stdio mode (desktop clients)
16
- """
17
- from __future__ import annotations
18
-
19
- import json
20
- import logging
21
- import os
22
- from typing import Any, Dict, Optional
23
-
24
- try:
25
- from mcp.server import Server
26
- from mcp.server.stdio import stdio_server
27
- from mcp.types import (
28
- TextContent,
29
- Tool,
30
- )
31
- MCP_AVAILABLE = True
32
- except ImportError:
33
- MCP_AVAILABLE = False
34
-
35
- from tweek.screening.context import ScreeningContext
36
-
37
- logger = logging.getLogger(__name__)
38
-
39
- # Version for MCP server identification
40
- MCP_SERVER_VERSION = "0.2.0"
41
-
42
-
43
- def _check_mcp_available():
44
- """Raise RuntimeError if MCP SDK is not installed."""
45
- if not MCP_AVAILABLE:
46
- raise RuntimeError(
47
- "MCP SDK not installed. Install with: pip install 'tweek[mcp]' "
48
- "or pip install mcp"
49
- )
50
-
51
-
52
- class TweekMCPServer:
53
- """
54
- Tweek MCP Gateway.
55
-
56
- Exposes vault and status tools via MCP. These are genuinely new
57
- capabilities not available as built-in desktop client tools.
58
-
59
- For intercepting upstream MCP server tool calls, use TweekMCPProxy
60
- from tweek.mcp.proxy instead.
61
- """
62
-
63
- def __init__(self, config: Optional[Dict[str, Any]] = None):
64
- _check_mcp_available()
65
- self.config = config or {}
66
- self.server = Server("tweek-security")
67
- self._setup_handlers()
68
- self._request_count = 0
69
- self._blocked_count = 0
70
-
71
- def _setup_handlers(self):
72
- """Register MCP protocol handlers."""
73
-
74
- @self.server.list_tools()
75
- async def list_tools() -> list[Tool]:
76
- """Return the list of tools this server provides."""
77
- tools = []
78
-
79
- tool_configs = self.config.get("mcp", {}).get("gateway", {}).get("tools", {})
80
-
81
- if tool_configs.get("vault", True):
82
- tools.append(Tool(
83
- name="tweek_vault",
84
- description=(
85
- "Retrieve a credential from Tweek's secure vault. "
86
- "Credentials are stored in the system keychain, not in .env files. "
87
- "Use this instead of reading .env files or hardcoding secrets."
88
- ),
89
- inputSchema={
90
- "type": "object",
91
- "properties": {
92
- "skill": {
93
- "type": "string",
94
- "description": "Skill namespace for the credential",
95
- },
96
- "key": {
97
- "type": "string",
98
- "description": "Credential key name",
99
- },
100
- },
101
- "required": ["skill", "key"],
102
- },
103
- ))
104
-
105
- if tool_configs.get("status", True):
106
- tools.append(Tool(
107
- name="tweek_status",
108
- description=(
109
- "Show Tweek security status including active plugins, "
110
- "recent activity, threat summary, and proxy statistics."
111
- ),
112
- inputSchema={
113
- "type": "object",
114
- "properties": {
115
- "detail": {
116
- "type": "string",
117
- "enum": ["summary", "plugins", "activity", "threats"],
118
- "description": "Level of detail (default: summary)",
119
- },
120
- },
121
- },
122
- ))
123
-
124
- return tools
125
-
126
- @self.server.call_tool()
127
- async def call_tool(name: str, arguments: dict) -> list[TextContent]:
128
- """Handle tool calls."""
129
- self._request_count += 1
130
-
131
- handler_map = {
132
- "tweek_vault": self._handle_vault,
133
- "tweek_status": self._handle_status,
134
- }
135
-
136
- handler = handler_map.get(name)
137
- if handler is None:
138
- return [TextContent(
139
- type="text",
140
- text=json.dumps({
141
- "error": f"Unknown tool: {name}",
142
- "available_tools": list(handler_map.keys()),
143
- }),
144
- )]
145
-
146
- try:
147
- result = await handler(arguments)
148
- return [TextContent(type="text", text=result)]
149
- except Exception as e:
150
- logger.error(f"Tool {name} failed: {e}")
151
- return [TextContent(
152
- type="text",
153
- text=json.dumps({"error": str(e), "tool": name}),
154
- )]
155
-
156
- def _build_context(
157
- self,
158
- tool_name: str,
159
- content: str,
160
- tool_input: Optional[Dict[str, Any]] = None,
161
- ) -> ScreeningContext:
162
- """Build a ScreeningContext for MCP tool calls."""
163
- return ScreeningContext(
164
- tool_name=tool_name,
165
- content=content,
166
- tier="default",
167
- working_dir=os.getcwd(),
168
- source="mcp",
169
- client_name=self.config.get("client_name"),
170
- tool_input=tool_input,
171
- )
172
-
173
- def _run_screening(self, context: ScreeningContext) -> Dict[str, Any]:
174
- """
175
- Run the shared screening pipeline.
176
-
177
- In gateway mode, should_prompt is converted to blocked
178
- since there is no interactive user to confirm.
179
- """
180
- from tweek.mcp.screening import run_mcp_screening
181
-
182
- result = run_mcp_screening(context)
183
-
184
- if result.get("should_prompt"):
185
- self._blocked_count += 1
186
- return {
187
- "allowed": False,
188
- "blocked": True,
189
- "reason": f"Requires user confirmation: {result.get('reason', '')}",
190
- "findings": result.get("findings", []),
191
- }
192
-
193
- if result.get("blocked"):
194
- self._blocked_count += 1
195
-
196
- return {
197
- "allowed": result.get("allowed", False),
198
- "blocked": result.get("blocked", False),
199
- "reason": result.get("reason"),
200
- "findings": result.get("findings", []),
201
- }
202
-
203
- async def _handle_vault(self, arguments: Dict[str, Any]) -> str:
204
- """Handle tweek_vault tool call."""
205
- skill = arguments.get("skill", "")
206
- key = arguments.get("key", "")
207
-
208
- # Screen vault access
209
- context = self._build_context("Vault", f"vault:{skill}/{key}", arguments)
210
- screening = self._run_screening(context)
211
-
212
- if screening["blocked"]:
213
- return json.dumps({
214
- "blocked": True,
215
- "reason": screening["reason"],
216
- })
217
-
218
- try:
219
- from tweek.vault.cross_platform import CrossPlatformVault
220
-
221
- vault = CrossPlatformVault()
222
- value = vault.get(skill, key)
223
-
224
- if value is None:
225
- return json.dumps({
226
- "error": f"Credential not found: {skill}/{key}",
227
- "available": False,
228
- })
229
-
230
- return json.dumps({
231
- "value": value,
232
- "skill": skill,
233
- "key": key,
234
- })
235
-
236
- except Exception as e:
237
- # Don't leak internal details across trust boundary
238
- logger.error(f"Vault operation failed: {e}")
239
- return json.dumps({"error": "Vault operation failed"})
240
-
241
- async def _handle_status(self, arguments: Dict[str, Any]) -> str:
242
- """Handle tweek_status tool call."""
243
- detail = arguments.get("detail", "summary")
244
-
245
- try:
246
- status = {
247
- "version": MCP_SERVER_VERSION,
248
- "source": "mcp",
249
- "mode": "gateway",
250
- "gateway_requests": self._request_count,
251
- "gateway_blocked": self._blocked_count,
252
- }
253
-
254
- if detail in ("summary", "plugins"):
255
- try:
256
- from tweek.plugins import get_registry
257
- registry = get_registry()
258
- stats = registry.get_stats()
259
- status["plugins"] = stats
260
- except ImportError:
261
- status["plugins"] = {"error": "Plugin system not available"}
262
-
263
- if detail in ("summary", "activity"):
264
- try:
265
- from tweek.logging.security_log import get_logger as get_sec_logger
266
- sec_logger = get_sec_logger()
267
- recent = sec_logger.get_recent(limit=10)
268
- status["recent_activity"] = [
269
- {
270
- "timestamp": str(e.timestamp),
271
- "event_type": e.event_type.value,
272
- "tool": e.tool_name,
273
- "decision": e.decision,
274
- }
275
- for e in recent
276
- ] if recent else []
277
- except (ImportError, Exception):
278
- status["recent_activity"] = []
279
-
280
- # Include approval queue stats if available
281
- try:
282
- from tweek.mcp.approval import ApprovalQueue
283
- queue = ApprovalQueue()
284
- status["approval_queue"] = queue.get_stats()
285
- except Exception:
286
- pass
287
-
288
- return json.dumps(status, indent=2)
289
-
290
- except Exception as e:
291
- return json.dumps({"error": str(e)})
292
-
293
-
294
- async def run_server(config: Optional[Dict[str, Any]] = None):
295
- """
296
- Run the Tweek MCP gateway server on stdio transport.
297
-
298
- Exposes tweek_vault and tweek_status tools. For upstream MCP
299
- server interception, use run_proxy() instead.
300
- """
301
- _check_mcp_available()
302
-
303
- server = TweekMCPServer(config=config)
304
-
305
- logger.info("Starting Tweek MCP Gateway...")
306
- logger.info(f"Version: {MCP_SERVER_VERSION}")
307
- logger.info("Tools: tweek_vault, tweek_status")
308
- logger.info("For upstream MCP interception, use: tweek mcp proxy")
309
-
310
- async with stdio_server() as (read_stream, write_stream):
311
- await server.server.run(
312
- read_stream,
313
- write_stream,
314
- server.server.create_initialization_options(),
315
- )
316
-
317
-
318
- def create_server(config: Optional[Dict[str, Any]] = None) -> "TweekMCPServer":
319
- """Create a TweekMCPServer instance for programmatic use."""
320
- return TweekMCPServer(config=config)
File without changes