Fix M8 foundation: dry-run exception handling, GUI close race, registry API, contract serialization, semver parser

This commit is contained in:
rob 2026-07-20 11:46:58 -03:00
parent f6bd640865
commit 188e167c1c
5 changed files with 57 additions and 40 deletions

View File

@ -527,7 +527,7 @@ def cmd_inspect(args):
tool = load_tool(args.name)
if not tool:
print(f"Error: Tool '{args.name}' not found.", file=open("/dev/stderr", "w"))
print(f"Error: Tool '{args.name}' not found.", file=sys.stderr)
return 1
report = analyze_tool(tool)

View File

@ -433,8 +433,12 @@ def _cmd_registry_publish(args):
config_yaml, readme=readme, defaults=defaults,
dry_run=True,
)
except Exception as e:
print(f"Registry preflight error: {e}", file=sys.stderr)
return 1
errors = result.get("errors") or []
warnings = result.get("warnings") or []
suggestions = result.get("suggestions") or []
if errors:
print(f"Preflight errors ({len(errors)}):")
for err in errors:
@ -443,16 +447,13 @@ def _cmd_registry_publish(args):
print(f"Preflight warnings ({len(warnings)}):")
for warn in warnings:
print(f" WARN: {warn}")
if not errors and not warnings:
if suggestions:
print(f"Suggestions ({len(suggestions)}):")
for sug in suggestions:
print(f" HINT: {sug}")
if not errors and not warnings and not suggestions:
print("Registry preflight passed.")
return 0 if not errors else 1
except Exception as e:
print(f"Registry preflight unavailable: {e}")
print("Proceeding with local validation only.")
else:
print("No registry token configured — local validation only.")
print("Configure a token to get registry-side preflight checks.")
return 0
# Check for token
config = load_config()

View File

@ -1094,7 +1094,6 @@ class ToolBuilderPage(QWidget):
clicked = msg.clickedButton()
if clicked is btn_open:
self.main_window.open_tool_builder(name)
self.main_window.close_tool_builder()
return
elif clicked is btn_copy:
suffix = 2

View File

@ -1,10 +1,10 @@
"""Shared preflight analysis engine.
Produces a PreflightReport used by the GUI, CLI, and registry workflow.
All checks are deterministic and evidence-based. Recommendations never
automate away human judgment.
All checks are deterministic and evidence-based.
"""
import sys
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@ -62,19 +62,33 @@ def _check_config_integrity(tool: Tool, report: PreflightReport):
report.errors.append("Tool name is required")
if tool.version and not _is_semver(tool.version):
report.errors.append(f"Version '{tool.version}' is not valid semver")
if not tool.steps:
report.warnings.append("Tool has no steps — output is a no-op passthrough")
if not tool.steps and not tool.arguments:
report.warnings.append(
"Tool has no steps and no arguments — output template is passthrough"
)
def _check_contracts(tool: Tool, report: PreflightReport):
if tool.output_schema:
schema = tool.output_schema
if tool.output_schema is not None:
_validate_schema(tool.output_schema, "output_schema", report)
if tool.input_schema is not None:
_validate_schema(tool.input_schema, "input_schema", report)
if tool.output_schema is None and tool.input_schema is None:
report.suggestions.append(
"Add input_schema and output_schema to enable automated verification"
)
def _validate_schema(schema: Any, label: str, report: PreflightReport):
if not isinstance(schema, dict):
report.errors.append("output_schema must be a JSON Schema object")
elif schema.get("type") != "object":
report.warnings.append("output_schema root should have type: object")
else:
report.suggestions.append("Add an output_schema to enable automated verification")
report.errors.append(f"{label} must be a JSON Schema object")
return
schema_type = schema.get("type")
if schema_type and not isinstance(schema_type, (str, list)):
report.errors.append(f"{label} 'type' must be a string or array")
required = schema.get("required")
if required is not None and not isinstance(required, list):
report.errors.append(f"{label} 'required' must be an array")
def _check_secrets(tool: Tool, report: PreflightReport):
@ -100,17 +114,20 @@ def _check_dependencies(tool: Tool, report: PreflightReport):
def _check_similar_tools(tool: Tool, report: PreflightReport, client):
try:
results = client.search_tools(tool.name, limit=5)
for item in results:
results = client.search_tools(tool.name, per_page=5)
items = results.data if hasattr(results, "data") else results
for item in items:
report.similar_tools.append({
"name": item.get("owner") + "/" + item.get("name"),
"name": (item.get("owner") or "") + "/" + item.get("name", ""),
"description": item.get("description", ""),
"downloads": item.get("downloads", 0),
})
if not items:
report.suggestions.append("No similar tools found on the registry")
except Exception:
report.warnings.append("Could not search registry for similar tools")
def _is_semver(version: str) -> bool:
import re
return bool(re.match(r"^\d+\.\d+\.\d+", version))
from .semver import Version
return Version.parse(version) is not None

View File

@ -502,9 +502,9 @@ class Tool:
# Only include visibility if it's not the default
if self.visibility and self.visibility != "public":
d["visibility"] = self.visibility
if self.input_schema:
if self.input_schema is not None:
d["input_schema"] = self.input_schema
if self.output_schema:
if self.output_schema is not None:
d["output_schema"] = self.output_schema
# Include source attribution if present
if self.source: