Add registry improvements, tag filtering, and admin tools

Registry API improvements:
- Add /api/v1/me POST endpoint for profile updates
- Add /api/v1/me/password POST endpoint for password changes
- Fix visibility filtering on categories and tags endpoints to only
  show approved public tools
- Allow underscores in tool names for fabric pattern compatibility
- Fix sqlite3.Row access (use bracket notation instead of .get())

Web UI improvements:
- Add tag filter to /tools page with three-state buttons
  (include/exclude/neutral)
- Add mobile-friendly tag filter in responsive view
- Display tags on tool cards in the tools listing
- Add dashboard settings form handlers for profile and password

Admin improvements:
- Add scrutiny audit page for reviewing tool safety analysis
- Improve pending tools page with scrutiny report display
- Add scrutiny stats to admin dashboard

Fabric sync improvements:
- Add direct database publishing with scrutiny vetting
- Support auto-approve for tools passing scrutiny
- Improve error handling and logging
- Add source attribution for imported tools

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-16 06:20:16 -04:00
co-authored by Claude Opus 4.5
parent 78025aac8e
commit 5e01a335c0
11 changed files with 1072 additions and 74 deletions
+304 -48
View File
@@ -241,85 +241,311 @@ def vet_pattern(pattern_dir: Path, provider: str = DEFAULT_PROVIDER) -> tuple[bo
return False, f"Vetting error: {e}"
def publish_to_registry(
name: str,
config_yaml: str,
readme: str,
provider: str,
auto_approve: bool = False
) -> tuple[bool, str, dict]:
"""Publish a tool directly to the registry database with vetting.
Returns:
Tuple of (success, message, scrutiny_report)
"""
try:
# Add src to path for registry imports
src_dir = Path(__file__).parent.parent / "src"
if str(src_dir) not in sys.path:
sys.path.insert(0, str(src_dir))
from cmdforge.registry.db import connect_db, query_one
from cmdforge.registry.scrutiny import scrutinize_tool
from cmdforge.hash_utils import compute_yaml_hash
# Parse config
config = yaml.safe_load(config_yaml)
version = config.get("version", "1.0.0")
description = config.get("description", "")
category = config.get("category")
tags = config.get("tags", [])
conn = connect_db()
# Check if already exists
existing = query_one(
conn,
"SELECT id, version FROM tools WHERE owner = ? AND name = ?",
["official", name],
)
if existing:
# Check if same version
if existing["version"] == version:
conn.close()
return True, "Already exists (same version)", {}
# Run scrutiny
scrutiny_report = {}
try:
scrutiny_report = scrutinize_tool(config_yaml, description, readme)
except Exception as e:
logger.warning(f"Scrutiny failed for {name}: {e}")
# Check scrutiny decision
scrutiny_decision = scrutiny_report.get("decision", "review")
if scrutiny_decision == "reject":
fail_findings = [f for f in scrutiny_report.get("findings", []) if f.get("result") == "fail"]
fail_msg = fail_findings[0]["message"] if fail_findings else "quality too low"
conn.close()
return False, f"Rejected by scrutiny: {fail_msg}", scrutiny_report
# Determine statuses
if scrutiny_decision == "approve":
scrutiny_status = "approved"
elif scrutiny_decision == "review":
scrutiny_status = "pending_review"
else:
scrutiny_status = "pending"
# Moderation status based on auto_approve setting
if auto_approve and scrutiny_status == "approved":
moderation_status = "approved"
else:
moderation_status = "pending"
# Compute hash
config_hash = compute_yaml_hash(config_yaml)
# Source attribution
source_json = json.dumps({
"type": "imported",
"original_tool": f"fabric/patterns/{name}",
"url": "https://github.com/danielmiessler/fabric",
"license": "MIT",
"author": "Daniel Miessler"
})
tags_json = json.dumps(tags) if tags else "[]"
scrutiny_json = json.dumps(scrutiny_report) if scrutiny_report else None
# Ensure official publisher exists
publisher = query_one(conn, "SELECT id FROM publishers WHERE slug = ?", ["official"])
if not publisher:
conn.execute(
"INSERT INTO publishers (email, password_hash, slug, display_name, verified) VALUES (?, ?, ?, ?, ?)",
["[email protected]", "", "official", "Official", True]
)
publisher_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
else:
publisher_id = publisher["id"]
if existing:
# Update existing tool
conn.execute(
"""
UPDATE tools SET
version = ?, description = ?, category = ?, tags = ?,
config_yaml = ?, readme = ?, scrutiny_status = ?, scrutiny_report = ?,
source_json = ?, config_hash = ?, moderation_status = ?,
published_at = ?
WHERE id = ?
""",
[
version, description, category, tags_json,
config_yaml, readme, scrutiny_status, scrutiny_json,
source_json, config_hash, moderation_status,
datetime.now(timezone.utc).isoformat(),
existing["id"]
]
)
else:
# Insert new tool
conn.execute(
"""
INSERT INTO tools (
owner, name, version, description, category, tags, config_yaml, readme,
publisher_id, scrutiny_status, scrutiny_report, source_json,
config_hash, visibility, moderation_status, published_at, downloads
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
"official", name, version, description, category, tags_json,
config_yaml, readme, publisher_id, scrutiny_status, scrutiny_json,
source_json, config_hash, "public", moderation_status,
datetime.now(timezone.utc).isoformat(), 0
]
)
conn.commit()
conn.close()
decision = scrutiny_report.get("decision", "unknown")
return True, f"Published (scrutiny: {decision}, moderation: {moderation_status})", scrutiny_report
except Exception as e:
logger.error(f"Registry publish failed: {e}")
import traceback
traceback.print_exc()
return False, str(e), {}
def sync_pattern(
pattern_dir: Path,
output_dir: Path,
provider: str,
state: SyncState,
dry_run: bool = False
dry_run: bool = False,
publish_to_db: bool = True,
auto_approve: bool = False
) -> bool:
"""Sync a single pattern.
Args:
pattern_dir: Path to the pattern directory
output_dir: Output directory for local tools (if not publishing to DB)
provider: AI provider name
state: Sync state object
dry_run: If True, don't make changes
publish_to_db: If True, publish to registry database; if False, create local files
auto_approve: If True, auto-approve tools that pass scrutiny
Returns:
True if successful
"""
name = pattern_dir.name
pattern_hash = hash_pattern(pattern_dir)
# Vet the pattern
passed, reason = vet_pattern(pattern_dir, provider)
if not passed:
logger.warning(f" ✗ {name}: {reason}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": reason,
"synced_at": datetime.now(timezone.utc).isoformat(),
}
# Read pattern content
system_md = pattern_dir / "system.md"
if not system_md.exists():
logger.warning(f" ✗ {name}: No system.md found")
return False
if dry_run:
logger.info(f" [DRY RUN] Would sync: {name} ({reason})")
return True
system_prompt = system_md.read_text()
# Import the pattern
try:
script_dir = Path(__file__).parent
# Import helper functions
script_dir = Path(__file__).parent
if str(script_dir) not in sys.path:
sys.path.insert(0, str(script_dir))
from import_fabric import import_pattern
from import_fabric import create_tool_config, pattern_to_display_name
success = import_pattern(
name,
pattern_dir.parent,
output_dir,
provider,
dry_run=False,
registry_format=False,
)
# Create tool config
config = create_tool_config(name, system_prompt, provider)
config_yaml = yaml.dump(config, default_flow_style=False, sort_keys=False)
# Create README with usage and attribution
display_name = pattern_to_display_name(name)
readme = f"""# {display_name}
{config.get('description', '')}
## Usage
```bash
cat input.txt | {name}
```
## Source
This tool was imported from [Fabric](https://github.com/danielmiessler/fabric) patterns.
- **Original pattern**: `{name}`
- **Author**: Daniel Miessler
- **License**: MIT
"""
if dry_run:
# Quick vet check for dry run
passed, reason = vet_pattern(pattern_dir, provider)
if passed:
logger.info(f" [DRY RUN] Would sync: {name} ({reason})")
else:
logger.info(f" [DRY RUN] Would skip: {name} ({reason})")
return passed
if publish_to_db:
# Publish directly to registry database
success, message, report = publish_to_registry(name, config_yaml, readme, provider, auto_approve)
if success:
logger.info(f" ✓ {name}: {reason}")
logger.info(f" ✓ {name} -> registry ({message})")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "synced",
"message": message,
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return True
else:
logger.error(f" ✗ {name}: Import failed")
logger.warning(f" ✗ {name}: {message}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": "Import failed",
"reason": message,
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return False
else:
# Original behavior: create local files
passed, reason = vet_pattern(pattern_dir, provider)
if not passed:
logger.warning(f" ✗ {name}: {reason}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": reason,
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return False
except Exception as e:
logger.error(f" ✗ {name}: {e}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": str(e),
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return False
try:
from import_fabric import import_pattern
success = import_pattern(
name,
pattern_dir.parent,
output_dir,
provider,
dry_run=False,
registry_format=False,
)
if success:
logger.info(f" ✓ {name} -> {output_dir}/{name}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "synced",
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return True
else:
logger.error(f" ✗ {name}: Import failed")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": "Import failed",
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return False
except Exception as e:
logger.error(f" ✗ {name}: {e}")
state.patterns[name] = {
"name": name,
"hash": pattern_hash,
"status": "failed",
"reason": str(e),
"synced_at": datetime.now(timezone.utc).isoformat(),
}
return False
def run_sync(
@@ -328,10 +554,22 @@ def run_sync(
state_file: Path,
provider: str,
dry_run: bool = False,
force_patterns: list[str] = None
force_patterns: list[str] = None,
publish_to_db: bool = True,
auto_approve: bool = False
) -> dict:
"""Run the sync process.
Args:
sync_dir: Directory for sync data (Fabric clone)
output_dir: Output directory for local tools (if not publishing to DB)
state_file: Path to state file
provider: AI provider name
dry_run: If True, don't make changes
force_patterns: List of pattern names to force resync
publish_to_db: If True, publish to registry database
auto_approve: If True, auto-approve tools that pass scrutiny
Returns:
Summary dict with counts
"""
@@ -365,10 +603,11 @@ def run_sync(
failed = 0
if to_sync:
logger.info(f"\nSyncing {len(to_sync)} patterns...")
dest = "registry database" if publish_to_db else output_dir
logger.info(f"\nSyncing {len(to_sync)} patterns to {dest}...")
for name in to_sync:
pattern_dir = patterns_dir / name
if sync_pattern(pattern_dir, output_dir, provider, state, dry_run):
if sync_pattern(pattern_dir, output_dir, provider, state, dry_run, publish_to_db, auto_approve):
synced += 1
else:
failed += 1
@@ -438,14 +677,17 @@ def daemon_loop(
output_dir: Path,
state_file: Path,
provider: str,
interval: int
interval: int,
publish_to_db: bool = True,
auto_approve: bool = False
):
"""Run sync in a loop."""
logger.info(f"Starting daemon mode with {interval}s interval")
while True:
try:
run_sync(sync_dir, output_dir, state_file, provider)
run_sync(sync_dir, output_dir, state_file, provider,
publish_to_db=publish_to_db, auto_approve=auto_approve)
except Exception as e:
logger.error(f"Sync failed: {e}")
@@ -514,6 +756,16 @@ def main():
default=DEFAULT_PROVIDER,
help=f"Default provider for tools (default: {DEFAULT_PROVIDER})"
)
parser.add_argument(
"--local-only",
action="store_true",
help="Create local tools only (don't publish to registry database)"
)
parser.add_argument(
"--auto-approve",
action="store_true",
help="Auto-approve tools that pass scrutiny (skip moderation queue)"
)
args = parser.parse_args()
@@ -530,7 +782,9 @@ def main():
args.output,
state_file,
args.provider,
args.interval
args.interval,
publish_to_db=not args.local_only,
auto_approve=args.auto_approve
)
return 0
@@ -541,7 +795,9 @@ def main():
state_file,
args.provider,
dry_run=args.dry_run,
force_patterns=args.force
force_patterns=args.force,
publish_to_db=not args.local_only,
auto_approve=args.auto_approve
)
if summary["failed"] > 0: