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.
- tweek/__init__.py +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6605
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +17 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +170 -74
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/hooks/pre_tool_use.py
CHANGED
|
@@ -54,6 +54,7 @@ from tweek.skills.guard import (
|
|
|
54
54
|
)
|
|
55
55
|
from tweek.skills.fingerprints import get_fingerprints
|
|
56
56
|
from tweek.sandbox.project import get_project_sandbox
|
|
57
|
+
from tweek.plugins.base import ReDoSProtection, RegexTimeoutError
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
# =============================================================================
|
|
@@ -264,6 +265,7 @@ def _resolve_enforcement(
|
|
|
264
265
|
enforcement_policy: Optional[EnforcementPolicy],
|
|
265
266
|
has_non_pattern_trigger: bool = False,
|
|
266
267
|
memory_adjustment: Optional[Dict] = None,
|
|
268
|
+
taint_level: str = "clean",
|
|
267
269
|
) -> str:
|
|
268
270
|
"""Determine the enforcement decision based on severity + confidence.
|
|
269
271
|
|
|
@@ -296,6 +298,7 @@ def _resolve_enforcement(
|
|
|
296
298
|
decision = "ask"
|
|
297
299
|
|
|
298
300
|
# Break-glass: downgrade "deny" to "ask" if active override exists
|
|
301
|
+
break_glass_used = False
|
|
299
302
|
if decision == "deny":
|
|
300
303
|
try:
|
|
301
304
|
from tweek.hooks.break_glass import check_override
|
|
@@ -318,6 +321,7 @@ def _resolve_enforcement(
|
|
|
318
321
|
},
|
|
319
322
|
)
|
|
320
323
|
decision = "ask" # Downgrade deny → ask (user still sees prompt)
|
|
324
|
+
break_glass_used = True
|
|
321
325
|
except ImportError:
|
|
322
326
|
pass # Break-glass module not available
|
|
323
327
|
|
|
@@ -337,6 +341,21 @@ def _resolve_enforcement(
|
|
|
337
341
|
except Exception:
|
|
338
342
|
pass # Memory is best-effort
|
|
339
343
|
|
|
344
|
+
# Provenance-based adjustment: modify decision based on session taint
|
|
345
|
+
# Skip when break-glass is active — explicit user overrides take priority
|
|
346
|
+
if not break_glass_used:
|
|
347
|
+
try:
|
|
348
|
+
from tweek.memory.provenance import adjust_enforcement_for_taint
|
|
349
|
+
if pattern_match:
|
|
350
|
+
decision = adjust_enforcement_for_taint(
|
|
351
|
+
base_decision=decision,
|
|
352
|
+
severity=severity,
|
|
353
|
+
confidence=confidence,
|
|
354
|
+
taint_level=taint_level,
|
|
355
|
+
)
|
|
356
|
+
except Exception:
|
|
357
|
+
pass # Provenance is best-effort
|
|
358
|
+
|
|
340
359
|
return decision
|
|
341
360
|
|
|
342
361
|
|
|
@@ -362,15 +381,25 @@ class TierManager:
|
|
|
362
381
|
return yaml.safe_load(f) or {}
|
|
363
382
|
|
|
364
383
|
def get_base_tier(self, tool_name: str, skill_name: Optional[str] = None) -> str:
|
|
365
|
-
"""Get the base tier for a tool or skill.
|
|
366
|
-
# Skills override tools if specified
|
|
367
|
-
if skill_name and skill_name in self.skills:
|
|
368
|
-
return self.skills[skill_name]
|
|
384
|
+
"""Get the base tier for a tool or skill.
|
|
369
385
|
|
|
370
|
-
|
|
371
|
-
|
|
386
|
+
Skills can only ESCALATE a tool's tier, never relax it.
|
|
387
|
+
This prevents a safe skill from disabling screening on dangerous tools.
|
|
388
|
+
For example, the 'explore' skill (safe) cannot downgrade Bash (dangerous).
|
|
389
|
+
But the 'deploy' skill (dangerous) can escalate Read (default) to dangerous.
|
|
390
|
+
"""
|
|
391
|
+
tier_priority = {"safe": 0, "default": 1, "risky": 2, "dangerous": 3}
|
|
372
392
|
|
|
373
|
-
|
|
393
|
+
# Start with the tool's inherent tier
|
|
394
|
+
tool_tier = self.tools.get(tool_name, self.default_tier)
|
|
395
|
+
|
|
396
|
+
if skill_name and skill_name in self.skills:
|
|
397
|
+
skill_tier = self.skills[skill_name]
|
|
398
|
+
# Skill can only escalate, never relax
|
|
399
|
+
if tier_priority.get(skill_tier, 1) > tier_priority.get(tool_tier, 1):
|
|
400
|
+
return skill_tier
|
|
401
|
+
|
|
402
|
+
return tool_tier
|
|
374
403
|
|
|
375
404
|
def check_escalations(self, content: str) -> Optional[Dict]:
|
|
376
405
|
"""Check if content triggers any escalation patterns.
|
|
@@ -551,6 +580,21 @@ class PatternMatcher:
|
|
|
551
580
|
extra = self._load_patterns(user_patterns)
|
|
552
581
|
self._merge_patterns(extra)
|
|
553
582
|
|
|
583
|
+
# Pre-compile all regex patterns for performance and ReDoS protection.
|
|
584
|
+
# These are our own trusted patterns (not user-supplied), so skip
|
|
585
|
+
# ReDoS validation during compilation (validate=False).
|
|
586
|
+
self._compiled_patterns: List[Tuple[re.Pattern, dict]] = []
|
|
587
|
+
for pattern in self.patterns:
|
|
588
|
+
regex = pattern.get("regex", "")
|
|
589
|
+
if not regex:
|
|
590
|
+
continue
|
|
591
|
+
try:
|
|
592
|
+
compiled = re.compile(regex, re.IGNORECASE | re.DOTALL)
|
|
593
|
+
self._compiled_patterns.append((compiled, pattern))
|
|
594
|
+
except re.error:
|
|
595
|
+
# Skip invalid patterns at compile time
|
|
596
|
+
continue
|
|
597
|
+
|
|
554
598
|
def _merge_patterns(self, extra_patterns: List[dict]):
|
|
555
599
|
"""Merge additional patterns into existing set (additive).
|
|
556
600
|
|
|
@@ -587,25 +631,33 @@ class PatternMatcher:
|
|
|
587
631
|
"""Check content against all patterns.
|
|
588
632
|
|
|
589
633
|
Returns the first matching pattern, or None.
|
|
634
|
+
Uses timeout-protected regex execution to prevent ReDoS.
|
|
590
635
|
"""
|
|
591
636
|
content = self._normalize(content)
|
|
592
|
-
|
|
637
|
+
if len(content) > ReDoSProtection.MAX_INPUT_LENGTH:
|
|
638
|
+
content = content[:ReDoSProtection.MAX_INPUT_LENGTH]
|
|
639
|
+
for compiled, pattern in self._compiled_patterns:
|
|
593
640
|
try:
|
|
594
|
-
if
|
|
641
|
+
if ReDoSProtection.safe_search(compiled, content, timeout=2.0):
|
|
595
642
|
return pattern
|
|
596
|
-
except re.error:
|
|
643
|
+
except (RegexTimeoutError, re.error):
|
|
597
644
|
continue
|
|
598
645
|
return None
|
|
599
646
|
|
|
600
647
|
def check_all(self, content: str) -> List[dict]:
|
|
601
|
-
"""Check content against all patterns, returning all matches.
|
|
648
|
+
"""Check content against all patterns, returning all matches.
|
|
649
|
+
|
|
650
|
+
Uses timeout-protected regex execution to prevent ReDoS.
|
|
651
|
+
"""
|
|
602
652
|
content = self._normalize(content)
|
|
653
|
+
if len(content) > ReDoSProtection.MAX_INPUT_LENGTH:
|
|
654
|
+
content = content[:ReDoSProtection.MAX_INPUT_LENGTH]
|
|
603
655
|
matches = []
|
|
604
|
-
for pattern in self.
|
|
656
|
+
for compiled, pattern in self._compiled_patterns:
|
|
605
657
|
try:
|
|
606
|
-
if
|
|
658
|
+
if ReDoSProtection.safe_search(compiled, content, timeout=2.0):
|
|
607
659
|
matches.append(pattern)
|
|
608
|
-
except re.error:
|
|
660
|
+
except (RegexTimeoutError, re.error):
|
|
609
661
|
continue
|
|
610
662
|
return matches
|
|
611
663
|
|
|
@@ -762,7 +814,7 @@ def _extract_paths_from_bash(command: str) -> List[str]:
|
|
|
762
814
|
python3 /home/user/script.py
|
|
763
815
|
|
|
764
816
|
Intentionally simple -- does NOT handle pipes, subshells, or variable
|
|
765
|
-
expansion. Content-based escalations (
|
|
817
|
+
expansion. Content-based escalations (262 patterns) cover the rest.
|
|
766
818
|
"""
|
|
767
819
|
import shlex
|
|
768
820
|
|
|
@@ -877,6 +929,39 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
|
|
|
877
929
|
# Fingerprint check is best-effort — never break the hook
|
|
878
930
|
pass
|
|
879
931
|
|
|
932
|
+
# =========================================================================
|
|
933
|
+
# SKILL CONTEXT TRACKING: Detect active skill from Skill tool invocations
|
|
934
|
+
# When Claude invokes the Skill tool, we extract the skill name and write
|
|
935
|
+
# a breadcrumb. Subsequent tool calls read it for tier/memory context.
|
|
936
|
+
# =========================================================================
|
|
937
|
+
_active_skill = None
|
|
938
|
+
try:
|
|
939
|
+
from tweek.skills.context import (
|
|
940
|
+
extract_skill_from_tool_input,
|
|
941
|
+
write_skill_breadcrumb,
|
|
942
|
+
read_skill_context,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
if tool_name == "Skill" and session_id:
|
|
946
|
+
# Skill tool invocation — extract and record the active skill
|
|
947
|
+
skill = extract_skill_from_tool_input(tool_input)
|
|
948
|
+
if skill:
|
|
949
|
+
write_skill_breadcrumb(skill, session_id)
|
|
950
|
+
_active_skill = skill
|
|
951
|
+
_log(
|
|
952
|
+
EventType.SCREENING_COMPLETE,
|
|
953
|
+
tool_name,
|
|
954
|
+
decision="allow",
|
|
955
|
+
decision_reason=f"Skill context recorded: {skill}",
|
|
956
|
+
metadata={"skill_context": skill},
|
|
957
|
+
)
|
|
958
|
+
elif session_id:
|
|
959
|
+
# Non-Skill tool — check for active skill breadcrumb
|
|
960
|
+
_active_skill = read_skill_context(session_id)
|
|
961
|
+
except Exception:
|
|
962
|
+
# Skill context is best-effort — never break the hook
|
|
963
|
+
_active_skill = None
|
|
964
|
+
|
|
880
965
|
# =========================================================================
|
|
881
966
|
# SELF-PROTECTION: Block AI from modifying security override config
|
|
882
967
|
# This file can only be edited by a human directly.
|
|
@@ -1112,9 +1197,10 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
|
|
|
1112
1197
|
tier_mgr = _get_tier_manager()
|
|
1113
1198
|
pattern_matcher = _get_pattern_matcher()
|
|
1114
1199
|
|
|
1115
|
-
# Determine effective tier
|
|
1200
|
+
# Determine effective tier (skill context overrides tool tier if configured)
|
|
1116
1201
|
effective_tier, escalation = tier_mgr.get_effective_tier(
|
|
1117
1202
|
tool_name, content,
|
|
1203
|
+
skill_name=_active_skill,
|
|
1118
1204
|
target_paths=target_paths,
|
|
1119
1205
|
working_dir=working_dir,
|
|
1120
1206
|
)
|
|
@@ -1192,6 +1278,23 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
|
|
|
1192
1278
|
metadata={"escalation": escalation}
|
|
1193
1279
|
)
|
|
1194
1280
|
|
|
1281
|
+
# =========================================================================
|
|
1282
|
+
# PROVENANCE: Record tool call and get session taint level
|
|
1283
|
+
# Tracks every tool call for taint decay; provides taint_level for
|
|
1284
|
+
# enforcement adjustment later in the pipeline.
|
|
1285
|
+
# =========================================================================
|
|
1286
|
+
# Default to "low" when no session tracking — clean session relaxation
|
|
1287
|
+
# requires active provenance tracking as a safety prerequisite.
|
|
1288
|
+
_session_taint_level = "low"
|
|
1289
|
+
try:
|
|
1290
|
+
from tweek.memory.provenance import get_taint_store
|
|
1291
|
+
if session_id:
|
|
1292
|
+
_taint_store = get_taint_store()
|
|
1293
|
+
_taint_state = _taint_store.record_tool_call(session_id, tool_name)
|
|
1294
|
+
_session_taint_level = _taint_state.get("taint_level", "clean")
|
|
1295
|
+
except Exception:
|
|
1296
|
+
_session_taint_level = "low" # Provenance unavailable — no relaxation
|
|
1297
|
+
|
|
1195
1298
|
# =========================================================================
|
|
1196
1299
|
# LAYER 1: Rate Limiting
|
|
1197
1300
|
# =========================================================================
|
|
@@ -1538,7 +1641,7 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
|
|
|
1538
1641
|
if pattern_match or llm_triggered or session_triggered or sandbox_triggered or compliance_triggered:
|
|
1539
1642
|
# Determine graduated enforcement decision
|
|
1540
1643
|
has_non_pattern = llm_triggered or session_triggered or sandbox_triggered or compliance_triggered
|
|
1541
|
-
decision = _resolve_enforcement(pattern_match, enforcement_policy, has_non_pattern, memory_adjustment)
|
|
1644
|
+
decision = _resolve_enforcement(pattern_match, enforcement_policy, has_non_pattern, memory_adjustment, _session_taint_level)
|
|
1542
1645
|
|
|
1543
1646
|
# Build the warning message for ask/deny decisions
|
|
1544
1647
|
final_msg = format_prompt_message(
|
|
@@ -1721,53 +1824,50 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
|
|
|
1721
1824
|
return {}
|
|
1722
1825
|
|
|
1723
1826
|
|
|
1724
|
-
def
|
|
1827
|
+
def check_hook_enabled(hook_name: str = "pre_tool_use") -> bool:
|
|
1725
1828
|
"""
|
|
1726
|
-
Check if
|
|
1829
|
+
Check if a Tweek hook should run in the current working directory.
|
|
1727
1830
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1831
|
+
Looks for a .tweek.yaml file in the project root (cwd). This file
|
|
1832
|
+
is created during `tweek protect claude-code` with hooks enabled
|
|
1833
|
+
by default. Users can manually set hooks to false to disable
|
|
1834
|
+
screening in a specific directory.
|
|
1835
|
+
|
|
1836
|
+
.tweek.yaml format:
|
|
1837
|
+
hooks:
|
|
1838
|
+
pre_tool_use: true # set false to disable pre-screening
|
|
1839
|
+
post_tool_use: true # set false to disable post-screening
|
|
1840
|
+
|
|
1841
|
+
If no .tweek.yaml exists, hooks are ENABLED (protected by default).
|
|
1842
|
+
This file is protected by Tweek self-protection — only a human
|
|
1843
|
+
can create or edit it.
|
|
1844
|
+
|
|
1845
|
+
Args:
|
|
1846
|
+
hook_name: "pre_tool_use" or "post_tool_use"
|
|
1730
1847
|
|
|
1731
1848
|
Returns:
|
|
1732
|
-
True if
|
|
1849
|
+
True if the hook should run, False to skip
|
|
1733
1850
|
"""
|
|
1734
|
-
|
|
1851
|
+
tweek_config = Path.cwd() / ".tweek.yaml"
|
|
1735
1852
|
|
|
1736
|
-
if not
|
|
1737
|
-
# No config =
|
|
1738
|
-
return
|
|
1853
|
+
if not tweek_config.exists():
|
|
1854
|
+
# No config = hooks enabled (protected by default)
|
|
1855
|
+
return True
|
|
1739
1856
|
|
|
1740
1857
|
try:
|
|
1741
|
-
with open(
|
|
1858
|
+
with open(tweek_config) as f:
|
|
1742
1859
|
config = yaml.safe_load(f) or {}
|
|
1743
1860
|
except Exception:
|
|
1744
|
-
return
|
|
1745
|
-
|
|
1746
|
-
# Check if globally enabled (production mode)
|
|
1747
|
-
if config.get("global_enabled", False):
|
|
1748
|
-
return True
|
|
1749
|
-
|
|
1750
|
-
# Check allowed directories
|
|
1751
|
-
allowed_dirs = config.get("allowed_directories", [])
|
|
1752
|
-
cwd = Path.cwd().resolve()
|
|
1753
|
-
|
|
1754
|
-
for allowed in allowed_dirs:
|
|
1755
|
-
allowed_path = Path(allowed).expanduser().resolve()
|
|
1756
|
-
try:
|
|
1757
|
-
# Check if cwd is the allowed dir or a subdirectory
|
|
1758
|
-
cwd.relative_to(allowed_path)
|
|
1759
|
-
return True
|
|
1760
|
-
except ValueError:
|
|
1761
|
-
continue
|
|
1861
|
+
return True # Can't read config = fail safe, keep hooks on
|
|
1762
1862
|
|
|
1763
|
-
|
|
1863
|
+
hooks = config.get("hooks", {})
|
|
1864
|
+
return hooks.get(hook_name, True)
|
|
1764
1865
|
|
|
1765
1866
|
|
|
1766
1867
|
def main():
|
|
1767
1868
|
"""Entry point for the hook."""
|
|
1768
|
-
#
|
|
1769
|
-
if not
|
|
1770
|
-
# Not in allowed directory - pass through without screening
|
|
1869
|
+
# Check if pre_tool_use hook is enabled for this directory
|
|
1870
|
+
if not check_hook_enabled("pre_tool_use"):
|
|
1771
1871
|
print("{}")
|
|
1772
1872
|
return
|
|
1773
1873
|
|
tweek/integrations/openclaw.py
CHANGED
|
@@ -441,3 +441,87 @@ def setup_openclaw_protection(
|
|
|
441
441
|
|
|
442
442
|
result.success = True
|
|
443
443
|
return result
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def remove_openclaw_protection() -> dict:
|
|
447
|
+
"""
|
|
448
|
+
Remove Tweek protection from OpenClaw.
|
|
449
|
+
|
|
450
|
+
Reverses setup_openclaw_protection():
|
|
451
|
+
1. Uninstalls the Tweek plugin from OpenClaw
|
|
452
|
+
2. Removes the Tweek plugin entry from openclaw.json
|
|
453
|
+
3. Removes the openclaw section from ~/.tweek/config.yaml
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
dict with 'success', 'message', 'details', and optionally 'error'
|
|
457
|
+
"""
|
|
458
|
+
details = []
|
|
459
|
+
|
|
460
|
+
# 1. Uninstall the npm plugin
|
|
461
|
+
if _check_plugin_installed():
|
|
462
|
+
try:
|
|
463
|
+
proc = subprocess.run(
|
|
464
|
+
["openclaw", "plugins", "uninstall", OPENCLAW_PLUGIN_NAME],
|
|
465
|
+
capture_output=True,
|
|
466
|
+
text=True,
|
|
467
|
+
timeout=60,
|
|
468
|
+
)
|
|
469
|
+
if proc.returncode == 0:
|
|
470
|
+
details.append(f"Uninstalled {OPENCLAW_PLUGIN_NAME} plugin")
|
|
471
|
+
else:
|
|
472
|
+
details.append(f"Plugin uninstall returned non-zero (may already be removed)")
|
|
473
|
+
except subprocess.TimeoutExpired:
|
|
474
|
+
return {"success": False, "error": "Plugin uninstall timed out"}
|
|
475
|
+
except FileNotFoundError:
|
|
476
|
+
details.append("openclaw CLI not found, skipping plugin uninstall")
|
|
477
|
+
else:
|
|
478
|
+
details.append("Tweek plugin not found in OpenClaw (already removed)")
|
|
479
|
+
|
|
480
|
+
# 2. Remove Tweek entry from openclaw.json
|
|
481
|
+
if OPENCLAW_CONFIG.exists():
|
|
482
|
+
try:
|
|
483
|
+
with open(OPENCLAW_CONFIG) as f:
|
|
484
|
+
config = json.load(f)
|
|
485
|
+
|
|
486
|
+
plugins = config.get("plugins", {})
|
|
487
|
+
entries = plugins.get("entries", {})
|
|
488
|
+
if "tweek" in entries:
|
|
489
|
+
del entries["tweek"]
|
|
490
|
+
with open(OPENCLAW_CONFIG, "w") as f:
|
|
491
|
+
json.dump(config, f, indent=2)
|
|
492
|
+
details.append(f"Removed tweek entry from {OPENCLAW_CONFIG}")
|
|
493
|
+
else:
|
|
494
|
+
details.append("No tweek entry found in openclaw.json")
|
|
495
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
496
|
+
details.append(f"Could not update openclaw.json: {e}")
|
|
497
|
+
|
|
498
|
+
# 3. Remove openclaw section from ~/.tweek/config.yaml
|
|
499
|
+
tweek_config_path = Path.home() / ".tweek" / "config.yaml"
|
|
500
|
+
if tweek_config_path.exists():
|
|
501
|
+
try:
|
|
502
|
+
import yaml
|
|
503
|
+
except ImportError:
|
|
504
|
+
yaml = None
|
|
505
|
+
|
|
506
|
+
if yaml:
|
|
507
|
+
try:
|
|
508
|
+
with open(tweek_config_path) as f:
|
|
509
|
+
tweek_config = yaml.safe_load(f) or {}
|
|
510
|
+
|
|
511
|
+
if "openclaw" in tweek_config:
|
|
512
|
+
del tweek_config["openclaw"]
|
|
513
|
+
with open(tweek_config_path, "w") as f:
|
|
514
|
+
yaml.dump(tweek_config, f, default_flow_style=False)
|
|
515
|
+
details.append("Removed openclaw section from ~/.tweek/config.yaml")
|
|
516
|
+
else:
|
|
517
|
+
details.append("No openclaw section in ~/.tweek/config.yaml")
|
|
518
|
+
except Exception as e:
|
|
519
|
+
details.append(f"Could not update ~/.tweek/config.yaml: {e}")
|
|
520
|
+
else:
|
|
521
|
+
details.append("PyYAML not available, skipping config.yaml cleanup")
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
"success": True,
|
|
525
|
+
"message": "OpenClaw protection removed",
|
|
526
|
+
"details": details,
|
|
527
|
+
}
|
tweek/licensing.py
CHANGED
|
@@ -74,7 +74,7 @@ class LicenseInfo:
|
|
|
74
74
|
# Only compliance and team management features require a license.
|
|
75
75
|
TIER_FEATURES = {
|
|
76
76
|
Tier.FREE: [
|
|
77
|
-
"pattern_matching", # All
|
|
77
|
+
"pattern_matching", # All 262 patterns included free
|
|
78
78
|
"basic_logging",
|
|
79
79
|
"vault_storage",
|
|
80
80
|
"cli_commands",
|
tweek/mcp/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Tweek MCP Security
|
|
3
|
+
Tweek MCP Security Proxy
|
|
4
4
|
|
|
5
5
|
MCP (Model Context Protocol) integration for desktop LLM applications:
|
|
6
6
|
- Claude Desktop
|
|
@@ -8,17 +8,15 @@ MCP (Model Context Protocol) integration for desktop LLM applications:
|
|
|
8
8
|
- Gemini CLI
|
|
9
9
|
- VS Code (Continue.dev)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
capabilities not available as built-in desktop client tools.
|
|
17
|
-
Use: tweek mcp serve
|
|
11
|
+
Transparently wraps upstream MCP servers with security screening and
|
|
12
|
+
human-in-the-loop approval. Also provides built-in tweek_vault and
|
|
13
|
+
tweek_status tools alongside proxied upstream tools.
|
|
14
|
+
|
|
15
|
+
Use: tweek mcp proxy
|
|
18
16
|
|
|
19
17
|
Built-in desktop client tools (Bash, Read, Write, etc.) cannot be
|
|
20
18
|
intercepted via MCP — use CLI hooks for Claude Code, or the HTTP
|
|
21
19
|
proxy for Cursor/direct API calls.
|
|
22
20
|
"""
|
|
23
21
|
|
|
24
|
-
__all__ = ["
|
|
22
|
+
__all__ = ["create_proxy"]
|
tweek/mcp/clients/chatgpt.py
CHANGED
|
@@ -31,8 +31,8 @@ class ChatGPTClient:
|
|
|
31
31
|
"""Get the arguments for the tweek MCP server."""
|
|
32
32
|
tweek_path = shutil.which("tweek")
|
|
33
33
|
if tweek_path:
|
|
34
|
-
return ["mcp", "
|
|
35
|
-
return ["-m", "tweek.mcp", "
|
|
34
|
+
return ["mcp", "proxy"]
|
|
35
|
+
return ["-m", "tweek.mcp", "proxy"]
|
|
36
36
|
|
|
37
37
|
def install(self) -> Dict[str, Any]:
|
|
38
38
|
"""
|
|
@@ -52,8 +52,8 @@ class ClaudeDesktopClient:
|
|
|
52
52
|
"""Get the arguments for the tweek MCP server."""
|
|
53
53
|
tweek_path = shutil.which("tweek")
|
|
54
54
|
if tweek_path:
|
|
55
|
-
return ["mcp", "
|
|
56
|
-
return ["-m", "tweek.mcp", "
|
|
55
|
+
return ["mcp", "proxy"]
|
|
56
|
+
return ["-m", "tweek.mcp", "proxy"]
|
|
57
57
|
|
|
58
58
|
def _build_server_config(self) -> Dict[str, Any]:
|
|
59
59
|
"""Build the MCP server configuration entry."""
|
tweek/mcp/clients/gemini.py
CHANGED
|
@@ -35,8 +35,8 @@ class GeminiClient:
|
|
|
35
35
|
"""Get the arguments for the tweek MCP server."""
|
|
36
36
|
tweek_path = shutil.which("tweek")
|
|
37
37
|
if tweek_path:
|
|
38
|
-
return ["mcp", "
|
|
39
|
-
return ["-m", "tweek.mcp", "
|
|
38
|
+
return ["mcp", "proxy"]
|
|
39
|
+
return ["-m", "tweek.mcp", "proxy"]
|
|
40
40
|
|
|
41
41
|
def _build_server_config(self) -> Dict[str, Any]:
|
|
42
42
|
"""Build the MCP server configuration entry."""
|