Fix M8 foundation: dry-run exception handling, GUI close race, registry API, contract serialization, semver parser
This commit is contained in:
parent
f6bd640865
commit
188e167c1c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -433,26 +433,27 @@ def _cmd_registry_publish(args):
|
|||
config_yaml, readme=readme, defaults=defaults,
|
||||
dry_run=True,
|
||||
)
|
||||
errors = result.get("errors") or []
|
||||
warnings = result.get("warnings") or []
|
||||
if errors:
|
||||
print(f"Preflight errors ({len(errors)}):")
|
||||
for err in errors:
|
||||
print(f" ERROR: {err}")
|
||||
if warnings:
|
||||
print(f"Preflight warnings ({len(warnings)}):")
|
||||
for warn in warnings:
|
||||
print(f" WARN: {warn}")
|
||||
if not errors and not warnings:
|
||||
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
|
||||
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:
|
||||
print(f" ERROR: {err}")
|
||||
if warnings:
|
||||
print(f"Preflight warnings ({len(warnings)}):")
|
||||
for warn in warnings:
|
||||
print(f" WARN: {warn}")
|
||||
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
|
||||
|
||||
# Check for token
|
||||
config = load_config()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 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")
|
||||
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(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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue