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) tool = load_tool(args.name)
if not tool: 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 return 1
report = analyze_tool(tool) report = analyze_tool(tool)

View File

@ -433,8 +433,12 @@ def _cmd_registry_publish(args):
config_yaml, readme=readme, defaults=defaults, config_yaml, readme=readme, defaults=defaults,
dry_run=True, dry_run=True,
) )
except Exception as e:
print(f"Registry preflight error: {e}", file=sys.stderr)
return 1
errors = result.get("errors") or [] errors = result.get("errors") or []
warnings = result.get("warnings") or [] warnings = result.get("warnings") or []
suggestions = result.get("suggestions") or []
if errors: if errors:
print(f"Preflight errors ({len(errors)}):") print(f"Preflight errors ({len(errors)}):")
for err in errors: for err in errors:
@ -443,16 +447,13 @@ def _cmd_registry_publish(args):
print(f"Preflight warnings ({len(warnings)}):") print(f"Preflight warnings ({len(warnings)}):")
for warn in warnings: for warn in warnings:
print(f" WARN: {warn}") 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.") print("Registry preflight passed.")
return 0 if not errors else 1 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 # Check for token
config = load_config() config = load_config()

View File

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

View File

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