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:
@@ -21,6 +21,7 @@ from argon2.exceptions import VerifyMismatchError
|
||||
from .db import connect_db, init_db, query_all, query_one
|
||||
from .rate_limit import RateLimiter
|
||||
from .sync import process_webhook, get_categories_cache_path, get_repo_dir
|
||||
from ..hash_utils import compute_yaml_hash
|
||||
from .stats import (
|
||||
refresh_tool_stats, get_tool_stats, refresh_publisher_stats,
|
||||
get_publisher_stats, track_tool_usage, calculate_badges, BADGES,
|
||||
@@ -996,6 +997,7 @@ def create_app() -> Flask:
|
||||
"resolved_version": row["version"],
|
||||
"config": row["config_yaml"],
|
||||
"readme": row["readme"] or "",
|
||||
"config_hash": row.get("config_hash") or "",
|
||||
}
|
||||
})
|
||||
response.headers["Cache-Control"] = "max-age=3600, immutable"
|
||||
@@ -1792,6 +1794,9 @@ def create_app() -> Flask:
|
||||
|
||||
tags_json = json.dumps(tags)
|
||||
|
||||
# Compute content hash for integrity verification
|
||||
config_hash = compute_yaml_hash(config_text)
|
||||
|
||||
# Determine status based on scrutiny
|
||||
if scrutiny_report and scrutiny_report.get("decision") == "approve":
|
||||
scrutiny_status = "approved"
|
||||
@@ -1816,8 +1821,8 @@ def create_app() -> Flask:
|
||||
owner, name, version, description, category, tags, config_yaml, readme,
|
||||
publisher_id, deprecated, deprecated_message, replacement, downloads,
|
||||
scrutiny_status, scrutiny_report, source, source_url, source_json,
|
||||
visibility, moderation_status, published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
config_hash, visibility, moderation_status, published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
owner,
|
||||
@@ -1838,6 +1843,7 @@ def create_app() -> Flask:
|
||||
source,
|
||||
source_url,
|
||||
source_json,
|
||||
config_hash,
|
||||
visibility,
|
||||
moderation_status,
|
||||
datetime.utcnow().isoformat(),
|
||||
|
||||
@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS tools (
|
||||
source TEXT,
|
||||
source_url TEXT,
|
||||
source_json TEXT,
|
||||
config_hash TEXT,
|
||||
visibility TEXT DEFAULT 'public',
|
||||
moderation_status TEXT DEFAULT 'pending',
|
||||
moderation_note TEXT,
|
||||
@@ -448,6 +449,7 @@ def migrate_db(conn: sqlite3.Connection) -> None:
|
||||
("source", "TEXT", "NULL"),
|
||||
("source_url", "TEXT", "NULL"),
|
||||
("source_json", "TEXT", "NULL"),
|
||||
("config_hash", "TEXT", "NULL"),
|
||||
("visibility", "TEXT", "'public'"),
|
||||
("moderation_status", "TEXT", "'pending'"),
|
||||
("moderation_note", "TEXT", "NULL"),
|
||||
@@ -512,6 +514,7 @@ def migrate_db(conn: sqlite3.Connection) -> None:
|
||||
try:
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_tools_owner ON tools(owner)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_tools_moderation ON tools(moderation_status, visibility)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_tools_hash ON tools(config_hash)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_publishers_role ON publishers(role)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_publishers_banned ON publishers(banned)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_audit_log_target ON audit_log(target_type, target_id)")
|
||||
|
||||
Reference in New Issue
Block a user