Implement content hash system for integrity verification

- Add hash_utils.py module for SHA256 content hashing with normalized YAML
- Store config_hash in registry database on publish
- Include hash in download response for client verification
- Verify downloaded content matches registry hash on install
- Store registry_hash in local tool config for publish state tracking
- Show publish state indicators in Tools page UI:
  - Green checkmark: Published and up to date
  - Orange dot: Modified since last publish
  - No indicator: Local tool (never published)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-16 01:31:08 -04:00
co-authored by Claude Opus 4.5
parent 8e6b03cade
commit 78025aac8e
6 changed files with 240 additions and 10 deletions
+82 -4
View File
@@ -1,19 +1,58 @@
"""Tools page - main view for managing tools."""
from collections import defaultdict
from pathlib import Path
from typing import Optional, Tuple
import yaml
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
QTreeWidget, QTreeWidgetItem, QTextEdit, QLabel,
QPushButton, QGroupBox, QMessageBox, QFrame
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtGui import QFont, QColor, QBrush
from ...tool import (
Tool, ToolArgument, PromptStep, CodeStep, ToolStep,
list_tools, load_tool, delete_tool, DEFAULT_CATEGORIES
list_tools, load_tool, delete_tool, DEFAULT_CATEGORIES,
get_tools_dir
)
from ...config import load_config
from ...hash_utils import compute_config_hash
def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]:
"""
Get the publish state of a tool.
Returns:
Tuple of (state, registry_hash) where state is:
- "published" - has registry_hash and current hash matches
- "modified" - has registry_hash but current hash differs
- "local" - no registry_hash (never published/downloaded)
"""
config_path = get_tools_dir() / tool_name / "config.yaml"
if not config_path.exists():
return ("local", None)
try:
config = yaml.safe_load(config_path.read_text())
registry_hash = config.get("registry_hash")
if not registry_hash:
return ("local", None)
# Compute current hash (excluding hash fields)
current_hash = compute_config_hash(config)
if current_hash == registry_hash:
return ("published", registry_hash)
else:
return ("modified", registry_hash)
except Exception:
return ("local", None)
class ToolsPage(QWidget):
@@ -169,10 +208,34 @@ class ToolsPage(QWidget):
# Tools in category
for name, tool in sorted(tools_by_category[category], key=lambda x: x[0]):
tool_item = QTreeWidgetItem([name])
# Get publish state
state, registry_hash = get_tool_publish_state(name)
# Show state indicator in display name
if state == "published":
display_name = f"{name} ✓"
tooltip = "Published to registry - up to date"
color = QColor(56, 161, 105) # Green
elif state == "modified":
display_name = f"{name} ●"
tooltip = "Published to registry - local modifications"
color = QColor(221, 107, 32) # Orange
else:
display_name = name
tooltip = "Local tool - not published"
color = None
tool_item = QTreeWidgetItem([display_name])
tool_item.setData(0, Qt.UserRole, name)
if color:
tool_item.setForeground(0, QBrush(color))
# Build tooltip
if tool.source and tool.source.type == "imported":
tool_item.setToolTip(0, f"Imported from {tool.source.url or 'registry'}")
tooltip = f"Imported from {tool.source.url or 'registry'}"
tool_item.setToolTip(0, tooltip)
cat_item.addChild(tool_item)
self.tool_tree.addTopLevelItem(cat_item)
@@ -224,6 +287,21 @@ class ToolsPage(QWidget):
if tool.description:
lines.append(f"<p style='color: #4a5568; margin-bottom: 16px;'>{tool.description}</p>")
# Publish state
state, registry_hash = get_tool_publish_state(tool.name)
if state == "published":
lines.append(
"<p style='background: #c6f6d5; color: #276749; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>"
"✓ Published to registry - up to date</p>"
)
elif state == "modified":
lines.append(
"<p style='background: #feebc8; color: #c05621; padding: 6px 10px; "
"border-radius: 4px; margin-bottom: 12px; font-size: 12px;'>"
"● Modified since last publish - republish to update registry</p>"
)
# Source info
if tool.source:
source_type = tool.source.type