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.
@@ -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,3 @@
1
+ from agent_catalog.cli import app
2
+
3
+ app()
@@ -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)