Add tool settings files for configurable tool defaults

Tools can now ship with defaults.yaml containing configurable settings
that users can customize via settings.yaml (auto-created from defaults).

Features:
- ensure_settings() helper copies defaults to settings on first use
- Settings available as {settings.key} in templates (scalars only)
- Full dict access via settings['key'] in code steps
- CLI: cmdforge settings <tool> show/edit/reset/diff
- GUI: Defaults editor in Tool Builder, Configure button on Tools page
- Registry: defaults.yaml published with tools, included in downloads
- Secret detection warning on publish (api_key, password, token, etc.)

Fully backward compatible - tools without defaults work unchanged.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-27 11:56:07 -04:00
co-authored by Claude Opus 4.5
parent ce94ba89dc
commit 8eff55ed1e
19 changed files with 894 additions and 33 deletions
+28 -1
View File
@@ -1,6 +1,7 @@
"""Registry commands."""
import json
import re
import sys
from pathlib import Path
@@ -359,6 +360,32 @@ def _cmd_registry_publish(args):
readme_path = tool_path / "README.md"
readme = readme_path.read_text() if readme_path.exists() else ""
# Read defaults if exists
defaults_path = tool_path / "defaults.yaml"
defaults = ""
if defaults_path.exists():
defaults = defaults_path.read_text()
# Warn about potential secrets in defaults
defaults_lower = defaults.lower()
secret_patterns = ['api_key:', 'api_secret:', 'password:', 'token:', 'secret:']
for pattern in secret_patterns:
if pattern in defaults_lower:
# Check if it has a non-empty value
match = re.search(rf'{pattern}\s*["\']?([^"\'\n]+)', defaults_lower)
if match and match.group(1).strip() and match.group(1).strip() not in ('""', "''", ''):
print(f"Warning: defaults.yaml contains '{pattern[:-1]}' with a value.")
print(" Make sure you're not publishing actual credentials!")
if not getattr(args, 'force', False):
try:
confirm = input("Continue anyway? [y/N] ")
if confirm.lower() != 'y':
return 1
except (EOFError, KeyboardInterrupt):
print("\nCancelled.")
return 1
break
# Validate
try:
data = yaml.safe_load(config_yaml)
@@ -408,7 +435,7 @@ def _cmd_registry_publish(args):
try:
client = get_client()
result = client.publish_tool(config_yaml, readme)
result = client.publish_tool(config_yaml, readme, defaults)
pr_url = result.get("pr_url", "")
status = result.get("status", "")