Skip to content

Commit 9d2a098

Browse files
feat: add pre-commit hooks for plugin manifest validation (#428)
Add two new pre-commit hooks to .pre-commit-hooks.yaml: - validate-plugin-manifest: Validates plugin.json files against the PluginManifest schema (required fields, format, naming conventions) - evaluate-plugin-policy: Evaluates plugin manifests against a governance policy file using the PolicyEvaluator Implementation in agent_marketplace.hooks module with CLI dispatcher for python -m agent_marketplace.hooks <command>. Closes #428 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6f3c328 commit 9d2a098

File tree

2 files changed

+185
-1
lines changed

2 files changed

+185
-1
lines changed

.pre-commit-hooks.yaml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
# Pre-commit hooks for Agent-OS policy validation.
1+
# Pre-commit hooks for Agent Governance Toolkit.
22
# Add this repo as a pre-commit hook source:
33
#
44
# repos:
55
# - repo: https://github.com/microsoft/agent-governance-toolkit
66
# rev: main
77
# hooks:
88
# - id: validate-policy
9+
# - id: validate-plugin-manifest
10+
# - id: evaluate-plugin-policy
11+
# args: ['--policy', 'policies/marketplace-policy.yaml']
912
#
1013
- id: validate-policy
1114
name: Validate Agent-OS policy files
@@ -16,3 +19,27 @@
1619
additional_dependencies:
1720
- pydantic>=2.0.0
1821
- pyyaml>=6.0.0
22+
23+
- id: validate-plugin-manifest
24+
name: Validate plugin manifest schema
25+
description: Validates plugin manifests (plugin.json) for required fields and schema compliance
26+
language: python
27+
entry: python -m agent_marketplace.hooks validate-manifest
28+
files: '(^|/)plugin\.(json|yaml|yml)$'
29+
types_or: [json, yaml]
30+
additional_dependencies:
31+
- pydantic>=2.0.0
32+
- pyyaml>=6.0.0
33+
34+
- id: evaluate-plugin-policy
35+
name: Evaluate plugin against governance policy
36+
description: Evaluates plugin manifests against a governance policy file
37+
language: python
38+
entry: python -m agent_marketplace.hooks evaluate-policy
39+
files: '(^|/)plugin\.(json|yaml|yml)$'
40+
types_or: [json, yaml]
41+
additional_dependencies:
42+
- pydantic>=2.0.0
43+
- pyyaml>=6.0.0
44+
- agent-os-kernel
45+
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""Pre-commit hooks for plugin manifest validation.
2+
3+
Entry points for ``.pre-commit-hooks.yaml``:
4+
5+
- ``validate-manifest`` — schema validation of plugin manifests
6+
- ``evaluate-policy`` — policy evaluation of plugin manifests
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import json
13+
import sys
14+
from pathlib import Path
15+
16+
import yaml
17+
18+
from agent_marketplace.exceptions import MarketplaceError
19+
from agent_marketplace.manifest import PluginManifest
20+
21+
22+
def _load_manifest_file(path: Path) -> dict:
23+
"""Load a manifest from JSON or YAML."""
24+
text = path.read_text(encoding="utf-8")
25+
if path.suffix == ".json":
26+
return json.loads(text)
27+
return yaml.safe_load(text)
28+
29+
30+
def validate_manifest_cli() -> int:
31+
"""Validate plugin manifest files against the PluginManifest schema.
32+
33+
Returns 0 if all files are valid, 1 if any fail.
34+
"""
35+
parser = argparse.ArgumentParser(
36+
prog="validate-plugin-manifest",
37+
description="Validate plugin manifest schema",
38+
)
39+
parser.add_argument("files", nargs="+", help="Manifest files to validate")
40+
args = parser.parse_args()
41+
42+
failures = 0
43+
for filepath in args.files:
44+
path = Path(filepath)
45+
try:
46+
data = _load_manifest_file(path)
47+
PluginManifest(**data)
48+
print(f" ✓ {path}")
49+
except MarketplaceError as exc:
50+
print(f" ✗ {path}: {exc}", file=sys.stderr)
51+
failures += 1
52+
except (json.JSONDecodeError, yaml.YAMLError) as exc:
53+
print(f" ✗ {path}: invalid format — {exc}", file=sys.stderr)
54+
failures += 1
55+
except Exception as exc: # noqa: BLE001
56+
print(f" ✗ {path}: {exc}", file=sys.stderr)
57+
failures += 1
58+
59+
if failures:
60+
print(f"\n{failures} manifest(s) failed validation.", file=sys.stderr)
61+
return 1
62+
63+
print(f"\n{len(args.files)} manifest(s) valid.")
64+
return 0
65+
66+
67+
def evaluate_policy_cli() -> int:
68+
"""Evaluate plugin manifests against a governance policy.
69+
70+
Returns 0 if all manifests comply, 1 if any violate.
71+
"""
72+
parser = argparse.ArgumentParser(
73+
prog="evaluate-plugin-policy",
74+
description="Evaluate plugin manifests against governance policy",
75+
)
76+
parser.add_argument("files", nargs="+", help="Manifest files to evaluate")
77+
parser.add_argument(
78+
"--policy",
79+
required=True,
80+
help="Path to policy YAML file or directory",
81+
)
82+
args = parser.parse_args()
83+
84+
try:
85+
from agent_os.policies import PolicyEvaluator
86+
except ImportError:
87+
print(
88+
"agent-os-kernel is required for policy evaluation. "
89+
"Install with: pip install agent-os-kernel",
90+
file=sys.stderr,
91+
)
92+
return 1
93+
94+
evaluator = PolicyEvaluator()
95+
policy_path = Path(args.policy)
96+
if policy_path.is_dir():
97+
evaluator.load_policies(str(policy_path))
98+
else:
99+
evaluator.load_policies(str(policy_path.parent))
100+
101+
violations = 0
102+
for filepath in args.files:
103+
path = Path(filepath)
104+
try:
105+
data = _load_manifest_file(path)
106+
manifest = PluginManifest(**data)
107+
108+
context = {
109+
"plugin_name": manifest.name,
110+
"plugin_type": manifest.plugin_type.value,
111+
"capabilities": manifest.capabilities,
112+
"author": manifest.author,
113+
"has_signature": manifest.signature is not None,
114+
}
115+
116+
decision = evaluator.evaluate(context)
117+
if decision.allowed:
118+
print(f" ✓ {path}: {decision.action}{decision.reason}")
119+
else:
120+
print(
121+
f" ✗ {path}: {decision.action}{decision.reason}",
122+
file=sys.stderr,
123+
)
124+
violations += 1
125+
126+
except Exception as exc: # noqa: BLE001
127+
print(f" ✗ {path}: evaluation error — {exc}", file=sys.stderr)
128+
violations += 1
129+
130+
if violations:
131+
print(f"\n{violations} manifest(s) violate policy.", file=sys.stderr)
132+
return 1
133+
134+
print(f"\n{len(args.files)} manifest(s) comply with policy.")
135+
return 0
136+
137+
138+
def main() -> None:
139+
"""CLI dispatcher for ``python -m agent_marketplace.hooks``."""
140+
if len(sys.argv) < 2:
141+
print("Usage: python -m agent_marketplace.hooks <command> [args...]")
142+
print("Commands: validate-manifest, evaluate-policy")
143+
sys.exit(1)
144+
145+
command = sys.argv.pop(1)
146+
147+
if command == "validate-manifest":
148+
sys.exit(validate_manifest_cli())
149+
elif command == "evaluate-policy":
150+
sys.exit(evaluate_policy_cli())
151+
else:
152+
print(f"Unknown command: {command}", file=sys.stderr)
153+
sys.exit(1)
154+
155+
156+
if __name__ == "__main__":
157+
main()

0 commit comments

Comments
 (0)