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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user