agent-catalog-new 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_catalog/__init__.py +33 -0
- agent_catalog/cli/__init__.py +108 -0
- agent_catalog/cli/__main__.py +3 -0
- agent_catalog/cli/aux_commands.py +139 -0
- agent_catalog/cli/crud.py +192 -0
- agent_catalog/cli/diff_commands.py +165 -0
- agent_catalog/cli/discover_commands.py +190 -0
- agent_catalog/client.py +340 -0
- agent_catalog/config.py +120 -0
- agent_catalog/decorators.py +549 -0
- agent_catalog/diff.py +156 -0
- agent_catalog/discovery.py +177 -0
- agent_catalog/graph.py +87 -0
- agent_catalog/loader.py +161 -0
- agent_catalog/mcp_server.py +279 -0
- agent_catalog/py.typed +0 -0
- agent_catalog/schema.py +252 -0
- agent_catalog/security.py +101 -0
- agent_catalog/serve.py +397 -0
- agent_catalog/storage.py +280 -0
- agent_catalog_new-0.2.0.dist-info/METADATA +201 -0
- agent_catalog_new-0.2.0.dist-info/RECORD +26 -0
- agent_catalog_new-0.2.0.dist-info/WHEEL +5 -0
- agent_catalog_new-0.2.0.dist-info/entry_points.txt +2 -0
- agent_catalog_new-0.2.0.dist-info/licenses/LICENSE +21 -0
- agent_catalog_new-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Agent Catalog — Declarative agent registry.
|
|
2
|
+
|
|
3
|
+
Discover, diff, and manage agent capabilities across environments.
|
|
4
|
+
|
|
5
|
+
Decorator API (new in 0.2.0):
|
|
6
|
+
|
|
7
|
+
from agent_catalog import agent, capability, tool, interface, dependency
|
|
8
|
+
from agent_catalog.decorators import build_manifest
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from agent_catalog.decorators import (
|
|
14
|
+
agent,
|
|
15
|
+
build_manifest,
|
|
16
|
+
capability,
|
|
17
|
+
dependency,
|
|
18
|
+
interface,
|
|
19
|
+
prompt_ref,
|
|
20
|
+
tool,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"agent",
|
|
25
|
+
"build_manifest",
|
|
26
|
+
"capability",
|
|
27
|
+
"dependency",
|
|
28
|
+
"interface",
|
|
29
|
+
"prompt_ref",
|
|
30
|
+
"tool",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Agent Catalog CLI — declarative agent registry.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
agent-catalog register ./agent.yaml
|
|
5
|
+
agent-catalog list
|
|
6
|
+
agent-catalog get agentic-inbox
|
|
7
|
+
agent-catalog search --capability send_email
|
|
8
|
+
agent-catalog diff agentic-inbox --right staging
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from agent_catalog import __version__
|
|
21
|
+
from agent_catalog.storage import CatalogStore
|
|
22
|
+
|
|
23
|
+
# ── App ────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="agent-catalog",
|
|
27
|
+
help="Declarative agent registry — catalog, discover, and diff agent capabilities.",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Typer adds --install-completion and --show-completion automatically.
|
|
32
|
+
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Version flag ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _version_callback(value: bool) -> None:
|
|
40
|
+
if value:
|
|
41
|
+
console.print(f"agent-catalog {__version__}")
|
|
42
|
+
raise typer.Exit()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.callback()
|
|
46
|
+
def _main_options(
|
|
47
|
+
version: bool = typer.Option(
|
|
48
|
+
False,
|
|
49
|
+
"--version",
|
|
50
|
+
help="Show version and exit.",
|
|
51
|
+
callback=_version_callback,
|
|
52
|
+
is_eager=True,
|
|
53
|
+
),
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Agent Catalog — declarative agent registry."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Shared helpers ─────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_store() -> CatalogStore:
|
|
62
|
+
"""Resolve the catalog store, respecting AGENT_CATALOG_DIR env var."""
|
|
63
|
+
root = os.environ.get("AGENT_CATALOG_DIR")
|
|
64
|
+
return CatalogStore(root=root) if root else CatalogStore()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render_manifest(m) -> Panel:
|
|
68
|
+
"""Pretty-print an agent manifest as a Rich Panel."""
|
|
69
|
+
rows: list[str] = []
|
|
70
|
+
rows.append(f"[bold]Name:[/] {m.name}")
|
|
71
|
+
rows.append(f"[bold]Slug:[/] {m.slug}")
|
|
72
|
+
rows.append(f"[bold]Version:[/] {m.version}")
|
|
73
|
+
rows.append(f"[bold]Environment:[/] {m.environment}")
|
|
74
|
+
rows.append(f"[bold]Status:[/] {m.status}")
|
|
75
|
+
rows.append(f"[bold]Description:[/] {m.description}")
|
|
76
|
+
if m.model:
|
|
77
|
+
rows.append(f"[bold]Model:[/] {m.model.provider}/{m.model.name}")
|
|
78
|
+
if m.capabilities:
|
|
79
|
+
rows.append(f"[bold]Capabilities:[/] {', '.join(c.id for c in m.capabilities)}")
|
|
80
|
+
if m.tools:
|
|
81
|
+
rows.append(f"[bold]Tools:[/] {', '.join(t.name for t in m.tools)}")
|
|
82
|
+
if m.prompt:
|
|
83
|
+
versions = ", ".join(p.version for p in m.prompt)
|
|
84
|
+
rows.append(f"[bold]Prompts:[/] {versions}")
|
|
85
|
+
if m.dependencies:
|
|
86
|
+
deps = ", ".join(d.name for d in m.dependencies)
|
|
87
|
+
rows.append(f"[bold]Dependencies:[/] {deps}")
|
|
88
|
+
if m.eval_contract:
|
|
89
|
+
suites = ", ".join(m.eval_contract.suites)
|
|
90
|
+
rows.append(f"[bold]Eval Suites:[/] {suites}")
|
|
91
|
+
text = "\n".join(rows)
|
|
92
|
+
return Panel(Text.from_markup(text), title=m.environment_tag())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── Entry point ────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main() -> None:
|
|
99
|
+
"""Entry point for the 'agent-catalog' command."""
|
|
100
|
+
app()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|
|
105
|
+
|
|
106
|
+
# ── Register subcommand modules ────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
from . import aux_commands, crud, diff_commands, discover_commands # noqa: E402,F401
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Auxiliary commands: security-audit, graph, serve, run, doctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from agent_catalog.cli import _get_store, app, console
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def security_audit(
|
|
14
|
+
output_format: str = typer.Option("table", "--format", "-f", help="Output: table, json"),
|
|
15
|
+
):
|
|
16
|
+
"""Audit all registered agents for security gaps."""
|
|
17
|
+
from agent_catalog.security import audit_catalog
|
|
18
|
+
|
|
19
|
+
findings = audit_catalog(_get_store())
|
|
20
|
+
if not findings:
|
|
21
|
+
console.print("[green]No security issues found[/]")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
if output_format == "json":
|
|
25
|
+
console.print(
|
|
26
|
+
json.dumps(
|
|
27
|
+
[
|
|
28
|
+
{"severity": f.severity, "agent": f.agent, "title": f.title, "detail": f.detail}
|
|
29
|
+
for f in findings
|
|
30
|
+
],
|
|
31
|
+
indent=2,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
from rich.table import Table
|
|
37
|
+
|
|
38
|
+
table = Table(title="Security Audit")
|
|
39
|
+
table.add_column("Severity", style="bold")
|
|
40
|
+
table.add_column("Agent")
|
|
41
|
+
table.add_column("Issue")
|
|
42
|
+
table.add_column("Detail", style="dim")
|
|
43
|
+
|
|
44
|
+
for f in findings:
|
|
45
|
+
color = {"critical": "red", "high": "yellow", "medium": "dim", "low": "dim"}[f.severity]
|
|
46
|
+
table.add_row(f"[{color}]{f.severity}[/]", f.agent, f.title, f.detail)
|
|
47
|
+
|
|
48
|
+
console.print(table)
|
|
49
|
+
crit = sum(1 for f in findings if f.severity == "critical")
|
|
50
|
+
high = sum(1 for f in findings if f.severity == "high")
|
|
51
|
+
console.print(f"[bold]Summary:[/] {len(findings)} findings ({crit} critical, {high} high)")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def graph(
|
|
56
|
+
output_format: str = typer.Option("mermaid", "--format", "-f", help="Output: mermaid, json"),
|
|
57
|
+
):
|
|
58
|
+
"""Show agent dependency graph."""
|
|
59
|
+
from agent_catalog.graph import build_graph, to_mermaid
|
|
60
|
+
|
|
61
|
+
store = _get_store()
|
|
62
|
+
if output_format == "json":
|
|
63
|
+
console.print_json(json.dumps(build_graph(store), default=str))
|
|
64
|
+
else:
|
|
65
|
+
console.print(to_mermaid(store))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command()
|
|
69
|
+
def serve(
|
|
70
|
+
port: int = typer.Option(8420, "--port", "-p", help="HTTP port"),
|
|
71
|
+
mcp: bool = typer.Option(False, "--mcp", help="Run MCP server instead of HTTP (stdio transport)"),
|
|
72
|
+
):
|
|
73
|
+
"""Start the Agent Marketplace web dashboard.
|
|
74
|
+
|
|
75
|
+
By default starts an HTTP server. Pass --mcp to run as an MCP
|
|
76
|
+
server over stdio (for use with MCP clients like Claude Code).
|
|
77
|
+
"""
|
|
78
|
+
if mcp:
|
|
79
|
+
from agent_catalog.mcp_server import run_server
|
|
80
|
+
|
|
81
|
+
run_server(store=_get_store())
|
|
82
|
+
else:
|
|
83
|
+
from agent_catalog.serve import serve as run_serve
|
|
84
|
+
|
|
85
|
+
run_serve(port=port, store=_get_store())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def run(
|
|
90
|
+
slug: str = typer.Argument(..., help="Agent slug"),
|
|
91
|
+
capability: str = typer.Argument(..., help="Capability ID to invoke"),
|
|
92
|
+
params: str | None = typer.Option(None, "--params", "-p", help="JSON string of parameters"),
|
|
93
|
+
):
|
|
94
|
+
"""Load an agent and invoke a capability at runtime.
|
|
95
|
+
|
|
96
|
+
Uses metadata.python_module / metadata.python_class to find and
|
|
97
|
+
import the agent class, then calls the capability method.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
agent-catalog run my-agent greet
|
|
101
|
+
agent-catalog run my-agent greet --params '{"name": "World"}'
|
|
102
|
+
"""
|
|
103
|
+
from agent_catalog.loader import invoke_capability
|
|
104
|
+
|
|
105
|
+
kwargs: dict = {}
|
|
106
|
+
if params:
|
|
107
|
+
try:
|
|
108
|
+
kwargs = json.loads(params)
|
|
109
|
+
except json.JSONDecodeError as e:
|
|
110
|
+
console.print(f"[red]\u2717[/] Invalid JSON in --params: {e}")
|
|
111
|
+
raise typer.Exit(1) from e
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
result = invoke_capability(slug, capability, store=_get_store(), **kwargs)
|
|
115
|
+
console.print(result)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
console.print(f"[red]\u2717[/] {e}")
|
|
118
|
+
raise typer.Exit(1) from e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command()
|
|
122
|
+
def doctor():
|
|
123
|
+
"""Check catalog consistency (orphaned files, missing manifests).
|
|
124
|
+
|
|
125
|
+
Scans the catalog directory and verifies every indexed agent
|
|
126
|
+
has a manifest file on disk, and every YAML file is indexed.
|
|
127
|
+
"""
|
|
128
|
+
store = _get_store()
|
|
129
|
+
issues = store.check_consistency()
|
|
130
|
+
|
|
131
|
+
if not issues:
|
|
132
|
+
console.print("[green]\u2713[/] Catalog is consistent")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
console.print(f"[yellow]Found {len(issues)} issue(s):[/]")
|
|
136
|
+
for issue in issues:
|
|
137
|
+
prefix = "[red]MISSING[/]" if issue.startswith("MISSING") else "[yellow]ORPHAN[/]"
|
|
138
|
+
console.print(f" {prefix} {issue}")
|
|
139
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""CRUD commands: register, list, get, search, validate, unregister, update."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from agent_catalog.cli import _get_store, _render_manifest, app, console
|
|
12
|
+
from agent_catalog.schema import AgentManifest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def register(
|
|
17
|
+
manifest: str = typer.Argument(..., help="Path to agent manifest YAML file"),
|
|
18
|
+
):
|
|
19
|
+
"""Register an agent from a manifest file."""
|
|
20
|
+
try:
|
|
21
|
+
agent = _get_store().register(Path(manifest))
|
|
22
|
+
console.print(f"[green]\u2713[/] Registered [bold]{agent.slug}[/] @ {agent.environment}")
|
|
23
|
+
console.print(_render_manifest(agent))
|
|
24
|
+
except Exception as e:
|
|
25
|
+
console.print(f"[red]\u2717[/] Failed to register: {e}")
|
|
26
|
+
raise typer.Exit(code=1) from e
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def list(
|
|
31
|
+
environment: str | None = typer.Option(None, "--env", "-e", help="Filter by environment"),
|
|
32
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
|
|
33
|
+
):
|
|
34
|
+
"""List all registered agents."""
|
|
35
|
+
import json as _json
|
|
36
|
+
|
|
37
|
+
agents = _get_store().list_all()
|
|
38
|
+
if environment:
|
|
39
|
+
agents = [a for a in agents if a.environment == environment]
|
|
40
|
+
|
|
41
|
+
if not agents:
|
|
42
|
+
if output == "json":
|
|
43
|
+
console.print(_json.dumps({"agents": []}))
|
|
44
|
+
else:
|
|
45
|
+
console.print("[yellow]No agents registered.[/]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if output == "json":
|
|
49
|
+
data = [
|
|
50
|
+
{
|
|
51
|
+
"slug": a.slug,
|
|
52
|
+
"name": a.name,
|
|
53
|
+
"version": a.version,
|
|
54
|
+
"environment": a.environment,
|
|
55
|
+
"status": a.status,
|
|
56
|
+
"capabilities": [c.id for c in a.capabilities],
|
|
57
|
+
}
|
|
58
|
+
for a in sorted(agents, key=lambda x: x.slug)
|
|
59
|
+
]
|
|
60
|
+
console.print(_json.dumps({"agents": data}, indent=2))
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
table = Table(title="Agent Catalog")
|
|
64
|
+
table.add_column("Slug", style="cyan")
|
|
65
|
+
table.add_column("Name", style="bold")
|
|
66
|
+
table.add_column("Version")
|
|
67
|
+
table.add_column("Environment")
|
|
68
|
+
table.add_column("Capabilities")
|
|
69
|
+
table.add_column("Status")
|
|
70
|
+
|
|
71
|
+
for a in sorted(agents, key=lambda x: x.slug):
|
|
72
|
+
table.add_row(
|
|
73
|
+
a.slug,
|
|
74
|
+
a.name,
|
|
75
|
+
a.version,
|
|
76
|
+
a.environment,
|
|
77
|
+
", ".join(c.id for c in a.capabilities[:3])
|
|
78
|
+
+ ("..." if len(a.capabilities) > 3 else ""),
|
|
79
|
+
a.status,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.print(table)
|
|
83
|
+
console.print(f"[dim]{len(agents)} agent(s)[/]")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def get(
|
|
88
|
+
slug: str = typer.Argument(..., help="Agent slug to retrieve"),
|
|
89
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
|
|
90
|
+
):
|
|
91
|
+
"""Show full details for an agent."""
|
|
92
|
+
import json as _json
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
agent = _get_store().get(slug)
|
|
96
|
+
if output == "json":
|
|
97
|
+
console.print(_json.dumps(agent.model_dump(mode="json", exclude_none=True), indent=2))
|
|
98
|
+
else:
|
|
99
|
+
console.print(_render_manifest(agent))
|
|
100
|
+
except (KeyError, FileNotFoundError) as e:
|
|
101
|
+
console.print(f"[red]\u2717[/] {e}")
|
|
102
|
+
raise typer.Exit(code=1) from e
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def search(
|
|
107
|
+
capability: str | None = typer.Option(
|
|
108
|
+
None, "--capability", "-c", help="Search by capability ID"
|
|
109
|
+
),
|
|
110
|
+
tool: str | None = typer.Option(None, "--tool", "-t", help="Search by tool name"),
|
|
111
|
+
surface: str | None = typer.Option(None, "--surface", "-s", help="Search by interface surface"),
|
|
112
|
+
environment: str | None = typer.Option(None, "--env", "-e", help="Filter by environment"),
|
|
113
|
+
):
|
|
114
|
+
"""Search agents by capability, tool, surface, or environment."""
|
|
115
|
+
results = _get_store().search(
|
|
116
|
+
capability=capability,
|
|
117
|
+
tool=tool,
|
|
118
|
+
surface=surface,
|
|
119
|
+
environment=environment,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if not results:
|
|
123
|
+
console.print("[yellow]No matching agents.[/]")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
table = Table(title=f"Search Results ({len(results)})")
|
|
127
|
+
table.add_column("Slug", style="cyan")
|
|
128
|
+
table.add_column("Name")
|
|
129
|
+
table.add_column("Environment")
|
|
130
|
+
table.add_column("Capabilities")
|
|
131
|
+
table.add_column("Tools")
|
|
132
|
+
table.add_column("Surfaces")
|
|
133
|
+
|
|
134
|
+
for a in sorted(results, key=lambda x: x.slug):
|
|
135
|
+
table.add_row(
|
|
136
|
+
a.slug,
|
|
137
|
+
a.name,
|
|
138
|
+
a.environment,
|
|
139
|
+
", ".join(c.id for c in a.capabilities),
|
|
140
|
+
", ".join(t.name for t in a.tools),
|
|
141
|
+
", ".join(s.type.value for s in a.interfaces),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
console.print(table)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def validate(
|
|
149
|
+
manifest: str = typer.Argument(..., help="Path to manifest YAML to validate"),
|
|
150
|
+
):
|
|
151
|
+
"""Validate a manifest file without registering it."""
|
|
152
|
+
try:
|
|
153
|
+
raw = yaml.safe_load(Path(manifest).read_text())
|
|
154
|
+
if raw is None:
|
|
155
|
+
console.print("[red]\u2717[/] Empty or invalid YAML.")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
m = AgentManifest.model_validate(raw)
|
|
158
|
+
console.print(f"[green]\u2713[/] Valid: [bold]{m.slug}[/] ({m.name})")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
console.print(f"[red]\u2717[/] Validation failed: {e}")
|
|
161
|
+
raise typer.Exit(1) from e
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command()
|
|
165
|
+
def unregister(
|
|
166
|
+
slug: str = typer.Argument(..., help="Agent slug to remove"),
|
|
167
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
168
|
+
):
|
|
169
|
+
"""Remove an agent from the catalog."""
|
|
170
|
+
if not force:
|
|
171
|
+
typer.confirm(f"Remove '{slug}' from the catalog?", abort=True)
|
|
172
|
+
if _get_store().unregister(slug):
|
|
173
|
+
console.print(f"[green]\u2713[/] Unregistered [bold]{slug}[/]")
|
|
174
|
+
else:
|
|
175
|
+
console.print(f"[yellow]Agent '{slug}' not found.[/]")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def update(
|
|
180
|
+
slug: str = typer.Argument(..., help="Agent slug to update"),
|
|
181
|
+
manifest: str = typer.Argument(..., help="Path to updated manifest YAML"),
|
|
182
|
+
):
|
|
183
|
+
"""Update an existing agent's manifest."""
|
|
184
|
+
try:
|
|
185
|
+
raw = yaml.safe_load(Path(manifest).read_text())
|
|
186
|
+
updated = AgentManifest.model_validate(raw)
|
|
187
|
+
result = _get_store().update(slug, updated)
|
|
188
|
+
console.print(f"[green]\u2713[/] Updated [bold]{result.slug}[/]")
|
|
189
|
+
console.print(_render_manifest(result))
|
|
190
|
+
except Exception as e:
|
|
191
|
+
console.print(f"[red]\u2717[/] Update failed: {e}")
|
|
192
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Diff commands: diff, export_contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from agent_catalog.cli import _get_store, app, console
|
|
11
|
+
from agent_catalog.diff import diff_manifests
|
|
12
|
+
from agent_catalog.schema import AgentManifest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def diff(
|
|
17
|
+
slug: str = typer.Argument(..., help="Slug of the primary agent"),
|
|
18
|
+
right: str | None = typer.Option(
|
|
19
|
+
None,
|
|
20
|
+
"--right",
|
|
21
|
+
"-r",
|
|
22
|
+
help="Path to another manifest YAML to diff against",
|
|
23
|
+
),
|
|
24
|
+
left_env: str | None = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--left-env",
|
|
27
|
+
help="Environment for the left side (from metadata.environments snapshots)",
|
|
28
|
+
),
|
|
29
|
+
right_env: str | None = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--right-env",
|
|
32
|
+
help="Environment for the right side. Use with a second slug via --slug2 for cross-agent env diff",
|
|
33
|
+
),
|
|
34
|
+
slug2: str | None = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--slug2",
|
|
37
|
+
help="Second agent slug to diff against (for cross-agent comparison)",
|
|
38
|
+
),
|
|
39
|
+
):
|
|
40
|
+
"""Show structured diff between two manifests, environments, or agents.
|
|
41
|
+
|
|
42
|
+
Modes:
|
|
43
|
+
agent-catalog diff agentic-inbox --right-env staging
|
|
44
|
+
Compare agentic-inbox's production manifest against its staging snapshot
|
|
45
|
+
|
|
46
|
+
agent-catalog diff agentic-inbox --left-env staging --right-env production
|
|
47
|
+
Compare staging and production snapshots from metadata.environments
|
|
48
|
+
|
|
49
|
+
agent-catalog diff agentic-inbox --slug2 nexusgate
|
|
50
|
+
Cross-agent comparison: agentic-inbox vs nexusgate
|
|
51
|
+
|
|
52
|
+
agent-catalog diff agentic-inbox --right ./external.yaml
|
|
53
|
+
Compare registered agent against an external manifest file
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
left = _get_store().get(slug)
|
|
57
|
+
except KeyError:
|
|
58
|
+
console.print(f"[red]\u2717[/] Agent '{slug}' not registered.")
|
|
59
|
+
raise typer.Exit(1) from None
|
|
60
|
+
|
|
61
|
+
# Resolve left manifest (with optional env overlay)
|
|
62
|
+
left_manifest = left
|
|
63
|
+
if left_env:
|
|
64
|
+
envs = left.metadata.get("environments", {})
|
|
65
|
+
if left_env not in envs:
|
|
66
|
+
console.print(f"[red]\u2717[/] No '{left_env}' snapshot in {slug} metadata.")
|
|
67
|
+
raise typer.Exit(1)
|
|
68
|
+
left_data = envs[left_env]
|
|
69
|
+
left_manifest = left.model_copy(update=left_data)
|
|
70
|
+
left_manifest.environment = left_env
|
|
71
|
+
|
|
72
|
+
# Resolve right manifest
|
|
73
|
+
if right:
|
|
74
|
+
# External file
|
|
75
|
+
right_manifest = AgentManifest.model_validate(
|
|
76
|
+
yaml.safe_load(Path(right).read_text())
|
|
77
|
+
)
|
|
78
|
+
right_manifest.environment = right_manifest.environment or "external"
|
|
79
|
+
elif slug2:
|
|
80
|
+
# Cross-agent comparison
|
|
81
|
+
try:
|
|
82
|
+
right_manifest = _get_store().get(slug2)
|
|
83
|
+
except KeyError:
|
|
84
|
+
console.print(f"[red]\u2717[/] Agent '{slug2}' not registered.")
|
|
85
|
+
raise typer.Exit(1) from None
|
|
86
|
+
if right_env:
|
|
87
|
+
envs = right_manifest.metadata.get("environments", {})
|
|
88
|
+
if right_env not in envs:
|
|
89
|
+
console.print(f"[red]\u2717[/] No '{right_env}' snapshot in {slug2} metadata.")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
right_data = envs[right_env]
|
|
92
|
+
right_manifest = right_manifest.model_copy(update=right_data)
|
|
93
|
+
right_manifest.environment = right_env
|
|
94
|
+
elif right_env:
|
|
95
|
+
# Same agent, env snapshot from metadata
|
|
96
|
+
envs = left.metadata.get("environments", {})
|
|
97
|
+
if right_env not in envs:
|
|
98
|
+
console.print(f"[red]\u2717[/] No '{right_env}' snapshot in {slug} metadata.")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
right_data = envs[right_env]
|
|
101
|
+
right_manifest = left.model_copy(update=right_data)
|
|
102
|
+
right_manifest.environment = right_env
|
|
103
|
+
else:
|
|
104
|
+
# Auto-find: try staging environment, then fall back to any second registered agent
|
|
105
|
+
second = _get_store().search(environment="staging")
|
|
106
|
+
if second:
|
|
107
|
+
right_manifest = second[0]
|
|
108
|
+
else:
|
|
109
|
+
all_agents = _get_store().list_all()
|
|
110
|
+
others = [a for a in all_agents if a.slug != slug]
|
|
111
|
+
if others:
|
|
112
|
+
right_manifest = others[0]
|
|
113
|
+
console.print(
|
|
114
|
+
f"[dim]Comparing against '{right_manifest.slug}' (no staging found)[/]"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
console.print(
|
|
118
|
+
"[yellow]No second manifest to diff against. "
|
|
119
|
+
"Try --right, --slug2, or --right-env.[/]"
|
|
120
|
+
)
|
|
121
|
+
raise typer.Exit(0)
|
|
122
|
+
|
|
123
|
+
report = diff_manifests(left_manifest, right_manifest)
|
|
124
|
+
console.print(report.summary)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def export_contract(
|
|
129
|
+
slug: str = typer.Argument(..., help="Agent slug to export contract for"),
|
|
130
|
+
output: str | None = typer.Option(
|
|
131
|
+
None, "--output", "-o", help="Output file path (default: stdout)"
|
|
132
|
+
),
|
|
133
|
+
):
|
|
134
|
+
"""Export a manifest's eval contract as eval-harness compatible YAML."""
|
|
135
|
+
try:
|
|
136
|
+
agent = _get_store().get(slug)
|
|
137
|
+
except KeyError:
|
|
138
|
+
console.print(f"[red]\u2717[/] Agent '{slug}' not registered.")
|
|
139
|
+
raise typer.Exit(1) from None
|
|
140
|
+
|
|
141
|
+
ec = agent.eval_contract
|
|
142
|
+
if not ec or not ec.suites:
|
|
143
|
+
console.print(f"[yellow]No eval contract defined for '{slug}'.[/]")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
contract_yaml = {
|
|
147
|
+
"project": ec.project or agent.slug,
|
|
148
|
+
"coverage_required": ec.coverage_required,
|
|
149
|
+
"suites": ec.suites,
|
|
150
|
+
"agent": {
|
|
151
|
+
"slug": agent.slug,
|
|
152
|
+
"version": agent.version,
|
|
153
|
+
"environment": agent.environment,
|
|
154
|
+
"capabilities": [c.id for c in agent.capabilities],
|
|
155
|
+
"model": f"{agent.model.provider}/{agent.model.name}" if agent.model else None,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
yaml_text = yaml.dump(contract_yaml, sort_keys=False, default_flow_style=False)
|
|
160
|
+
|
|
161
|
+
if output:
|
|
162
|
+
Path(output).write_text(yaml_text)
|
|
163
|
+
console.print(f"[green]\u2713[/] Contract exported to [bold]{output}[/]")
|
|
164
|
+
else:
|
|
165
|
+
console.print(yaml_text)
|