Initial commit
This commit is contained in:
310
skills/policy.enforce/SKILL.md
Normal file
310
skills/policy.enforce/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: policy.enforce
|
||||
description: Enforces policy rules for skill and agent manifests
|
||||
---
|
||||
|
||||
# policy.enforce
|
||||
|
||||
Enforces policy rules for skill and agent manifests including naming conventions, semantic versioning, permissions validation, and status lifecycle checks.
|
||||
|
||||
## Overview
|
||||
|
||||
The `policy.enforce` skill validates skill and agent manifests against Betty Framework policy rules to ensure consistency and compliance across the framework. It supports both single-file validation and batch mode for scanning all manifests.
|
||||
|
||||
## Policy Rules
|
||||
|
||||
### 1. Naming Convention
|
||||
- Names must be **lowercase only**
|
||||
- Can use **dots** for namespacing (e.g., `api.define`, `workflow.compose`)
|
||||
- Must start with a lowercase letter
|
||||
- Must end with a letter or number
|
||||
- **No spaces, underscores, or hyphens** in the name field
|
||||
|
||||
**Valid Examples:**
|
||||
- `api.define`
|
||||
- `policy.enforce`
|
||||
- `skill.create`
|
||||
|
||||
**Invalid Examples:**
|
||||
- `API.define` (uppercase)
|
||||
- `api_define` (underscore)
|
||||
- `api define` (space)
|
||||
- `api-define` (hyphen)
|
||||
|
||||
### 2. Semantic Versioning
|
||||
- Must follow semantic versioning format: `MAJOR.MINOR.PATCH`
|
||||
- Optional pre-release suffix allowed (e.g., `1.0.0-alpha`)
|
||||
|
||||
**Valid Examples:**
|
||||
- `0.1.0`
|
||||
- `1.0.0`
|
||||
- `2.3.1-beta`
|
||||
|
||||
**Invalid Examples:**
|
||||
- `1.0` (missing patch)
|
||||
- `v1.0.0` (prefix not allowed)
|
||||
- `1.0.0.0` (too many segments)
|
||||
|
||||
### 3. Permissions
|
||||
Only the following permissions are allowed:
|
||||
- `filesystem` (or scoped: `filesystem:read`, `filesystem:write`)
|
||||
- `network` (or scoped: `network:http`, `network:https`)
|
||||
- `read`
|
||||
- `write`
|
||||
|
||||
Any other permissions will trigger a violation.
|
||||
|
||||
### 4. Status
|
||||
Must be one of:
|
||||
- `draft` - Under development
|
||||
- `active` - Production ready
|
||||
- `deprecated` - Still works but not recommended
|
||||
- `archived` - No longer maintained
|
||||
|
||||
## Usage
|
||||
|
||||
### Single File Validation
|
||||
|
||||
Validate a single skill or agent manifest:
|
||||
|
||||
```bash
|
||||
python3 skills/policy.enforce/policy_enforce.py path/to/skill.yaml
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
python3 skills/policy.enforce/policy_enforce.py skills/api.define/skill.yaml
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
Validate all manifests in `skills/` and `agents/` directories:
|
||||
|
||||
```bash
|
||||
python3 skills/policy.enforce/policy_enforce.py --batch
|
||||
```
|
||||
|
||||
Or simply run without arguments:
|
||||
```bash
|
||||
python3 skills/policy.enforce/policy_enforce.py
|
||||
```
|
||||
|
||||
### Strict Mode
|
||||
|
||||
Treat warnings as errors (if warnings are implemented in future):
|
||||
|
||||
```bash
|
||||
python3 skills/policy.enforce/policy_enforce.py --batch --strict
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
The skill outputs JSON with the following structure:
|
||||
|
||||
### Single File Success
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"path": "skills/api.define/skill.yaml",
|
||||
"manifest_type": "skill",
|
||||
"violations": [],
|
||||
"violation_count": 0,
|
||||
"message": "All policy checks passed"
|
||||
}
|
||||
```
|
||||
|
||||
### Single File Failure
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"path": "skills/example/skill.yaml",
|
||||
"manifest_type": "skill",
|
||||
"violations": [
|
||||
{
|
||||
"field": "name",
|
||||
"rule": "naming_convention",
|
||||
"message": "Name contains uppercase letters: 'API.define'. Names must be lowercase.",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"field": "version",
|
||||
"rule": "semantic_versioning",
|
||||
"message": "Invalid version: '1.0'. Must follow semantic versioning (e.g., 1.0.0, 0.1.0-alpha)",
|
||||
"severity": "error"
|
||||
}
|
||||
],
|
||||
"violation_count": 2,
|
||||
"message": "Found 2 policy violation(s)"
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"mode": "batch",
|
||||
"message": "Validated 15 manifest(s): 15 passed, 0 failed",
|
||||
"total_manifests": 15,
|
||||
"passed": 15,
|
||||
"failed": 0,
|
||||
"results": [
|
||||
{
|
||||
"success": true,
|
||||
"path": "skills/api.define/skill.yaml",
|
||||
"manifest_type": "skill",
|
||||
"violations": [],
|
||||
"violation_count": 0,
|
||||
"message": "All policy checks passed"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
### Exit Codes
|
||||
- `0` - All policy checks passed
|
||||
- `1` - One or more policy violations found
|
||||
|
||||
### Violation Severity
|
||||
- `error` - Blocking violation that must be fixed
|
||||
- `warning` - Non-blocking issue (recommended to fix)
|
||||
|
||||
### Common Violations
|
||||
|
||||
**Naming Convention Violations:**
|
||||
- Use lowercase: `API.define` → `api.define`
|
||||
- Use dots instead of underscores: `api_define` → `api.define`
|
||||
- Remove spaces: `api define` → `api.define`
|
||||
|
||||
**Version Violations:**
|
||||
- Add patch version: `1.0` → `1.0.0`
|
||||
- Remove prefix: `v1.0.0` → `1.0.0`
|
||||
|
||||
**Permission Violations:**
|
||||
- Use only allowed permissions
|
||||
- Replace `http` with `network:http`
|
||||
- Replace `file` with `filesystem`
|
||||
|
||||
**Status Violations:**
|
||||
- Use lowercase: `Active` → `active`
|
||||
- Use valid values: `production` → `active`
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
Add to your CI pipeline to enforce policies on all manifests:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/policy-check.yml
|
||||
name: Policy Enforcement
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
policy-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run Policy Enforcement
|
||||
run: |
|
||||
python3 skills/policy.enforce/policy_enforce.py --batch
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `manifest_path` | string | No | - | Path to a single manifest file to validate |
|
||||
| `--batch` | boolean | No | false | Enable batch mode to validate all manifests |
|
||||
| `--strict` | boolean | No | false | Treat warnings as errors |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `success` | boolean | Whether all policy checks passed |
|
||||
| `violations` | array | List of policy violations found |
|
||||
| `violation_count` | integer | Total number of violations |
|
||||
| `message` | string | Summary message |
|
||||
| `path` | string | Path to the validated manifest (single mode) |
|
||||
| `manifest_type` | string | Type of manifest: "skill" or "agent" (single mode) |
|
||||
| `total_manifests` | integer | Total number of manifests checked (batch mode) |
|
||||
| `passed` | integer | Number of manifests that passed (batch mode) |
|
||||
| `failed` | integer | Number of manifests that failed (batch mode) |
|
||||
| `results` | array | Individual results for each manifest (batch mode) |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `betty.config` - Configuration and paths
|
||||
- `betty.validation` - Validation utilities
|
||||
- `betty.logging_utils` - Logging infrastructure
|
||||
- PyYAML - YAML parsing
|
||||
|
||||
## Status
|
||||
|
||||
**Active** - Production ready
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Validate a Single Skill
|
||||
|
||||
```bash
|
||||
$ python3 skills/policy.enforce/policy_enforce.py skills/api.define/skill.yaml
|
||||
{
|
||||
"success": true,
|
||||
"path": "skills/api.define/skill.yaml",
|
||||
"manifest_type": "skill",
|
||||
"violations": [],
|
||||
"violation_count": 0,
|
||||
"message": "All policy checks passed"
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Batch Validation
|
||||
|
||||
```bash
|
||||
$ python3 skills/policy.enforce/policy_enforce.py --batch
|
||||
{
|
||||
"success": true,
|
||||
"mode": "batch",
|
||||
"message": "Validated 15 manifest(s): 15 passed, 0 failed",
|
||||
"total_manifests": 15,
|
||||
"passed": 15,
|
||||
"failed": 0,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Failed Validation
|
||||
|
||||
```bash
|
||||
$ python3 skills/policy.enforce/policy_enforce.py skills/bad-example/skill.yaml
|
||||
{
|
||||
"success": false,
|
||||
"path": "skills/bad-example/skill.yaml",
|
||||
"manifest_type": "skill",
|
||||
"violations": [
|
||||
{
|
||||
"field": "name",
|
||||
"rule": "naming_convention",
|
||||
"message": "Name contains uppercase letters: 'Bad-Example'. Names must be lowercase.",
|
||||
"severity": "error"
|
||||
}
|
||||
],
|
||||
"violation_count": 1,
|
||||
"message": "Found 1 policy violation(s)"
|
||||
}
|
||||
$ echo $?
|
||||
1
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Custom policy rule definitions via YAML files
|
||||
- Support for warning-level violations
|
||||
- Auto-fix capability for common violations
|
||||
- Policy exemptions and overrides
|
||||
- Historical violation tracking
|
||||
1
skills/policy.enforce/__init__.py
Normal file
1
skills/policy.enforce/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
449
skills/policy.enforce/policy_enforce.py
Executable file
449
skills/policy.enforce/policy_enforce.py
Executable file
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
policy_enforce.py - Implementation of the policy.enforce Skill
|
||||
|
||||
Enforces policy rules for skill and agent manifests including:
|
||||
- Naming conventions (lowercase, dot-separated, no spaces)
|
||||
- Semantic versioning
|
||||
- Permissions validation (only filesystem, network, read, write)
|
||||
- Status lifecycle checks (draft, active, deprecated, archived)
|
||||
|
||||
Supports both single-file validation and batch mode.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
import re
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from betty.config import BASE_DIR, SKILLS_DIR, AGENTS_DIR
|
||||
from betty.validation import validate_path, validate_version, ValidationError
|
||||
from betty.logging_utils import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# Policy definitions
|
||||
ALLOWED_PERMISSIONS = {"filesystem", "network", "read", "write"}
|
||||
ALLOWED_STATUSES = {"draft", "active", "deprecated", "archived"}
|
||||
VALID_NAME_PATTERN = r"^[a-z][a-z0-9.]*[a-z0-9]$" # lowercase, dots allowed, no spaces
|
||||
|
||||
|
||||
class PolicyViolation:
|
||||
"""Represents a single policy violation."""
|
||||
|
||||
def __init__(self, field: str, rule: str, message: str, severity: str = "error"):
|
||||
self.field = field
|
||||
self.rule = rule
|
||||
self.message = message
|
||||
self.severity = severity # "error" or "warning"
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
"field": self.field,
|
||||
"rule": self.rule,
|
||||
"message": self.message,
|
||||
"severity": self.severity
|
||||
}
|
||||
|
||||
|
||||
def load_manifest(path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load and parse a manifest from YAML file.
|
||||
|
||||
Args:
|
||||
path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
Parsed manifest dictionary
|
||||
|
||||
Raises:
|
||||
Exception: If manifest cannot be loaded or parsed
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
manifest = yaml.safe_load(f)
|
||||
return manifest
|
||||
except FileNotFoundError:
|
||||
raise Exception(f"Manifest file not found: {path}")
|
||||
except yaml.YAMLError as e:
|
||||
raise Exception(f"Failed to parse YAML: {e}")
|
||||
|
||||
|
||||
def validate_name_format(name: str) -> Optional[PolicyViolation]:
|
||||
"""
|
||||
Validate that name follows naming convention:
|
||||
- lowercase letters and numbers only
|
||||
- dots allowed for namespacing
|
||||
- no spaces, underscores, or hyphens (except in skill directory names for backwards compatibility)
|
||||
|
||||
Args:
|
||||
name: Name to validate
|
||||
|
||||
Returns:
|
||||
PolicyViolation if invalid, None if valid
|
||||
"""
|
||||
if not name:
|
||||
return PolicyViolation(
|
||||
field="name",
|
||||
rule="naming_convention",
|
||||
message="Name cannot be empty"
|
||||
)
|
||||
|
||||
# Check for spaces
|
||||
if ' ' in name:
|
||||
return PolicyViolation(
|
||||
field="name",
|
||||
rule="naming_convention",
|
||||
message=f"Name contains spaces: '{name}'. Names must not contain spaces."
|
||||
)
|
||||
|
||||
# Check for uppercase
|
||||
if name != name.lower():
|
||||
return PolicyViolation(
|
||||
field="name",
|
||||
rule="naming_convention",
|
||||
message=f"Name contains uppercase letters: '{name}'. Names must be lowercase."
|
||||
)
|
||||
|
||||
# Check format: lowercase, dots allowed, must start with letter
|
||||
if not re.match(VALID_NAME_PATTERN, name):
|
||||
return PolicyViolation(
|
||||
field="name",
|
||||
rule="naming_convention",
|
||||
message=f"Invalid name format: '{name}'. "
|
||||
"Names must start with a lowercase letter, contain only lowercase letters, "
|
||||
"numbers, and dots for namespacing, and end with a letter or number."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_version_format(version: str) -> Optional[PolicyViolation]:
|
||||
"""
|
||||
Validate that version follows semantic versioning.
|
||||
|
||||
Args:
|
||||
version: Version string to validate
|
||||
|
||||
Returns:
|
||||
PolicyViolation if invalid, None if valid
|
||||
"""
|
||||
try:
|
||||
validate_version(version)
|
||||
return None
|
||||
except ValidationError as e:
|
||||
return PolicyViolation(
|
||||
field="version",
|
||||
rule="semantic_versioning",
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
|
||||
def validate_permissions(manifest: Dict[str, Any], manifest_type: str) -> List[PolicyViolation]:
|
||||
"""
|
||||
Validate that all permissions are in the allowed set.
|
||||
|
||||
Args:
|
||||
manifest: Manifest dictionary
|
||||
manifest_type: "skill" or "agent"
|
||||
|
||||
Returns:
|
||||
List of PolicyViolations for invalid permissions
|
||||
"""
|
||||
violations = []
|
||||
|
||||
# For skills, check entrypoints permissions
|
||||
if manifest_type == "skill":
|
||||
entrypoints = manifest.get("entrypoints", [])
|
||||
for idx, entrypoint in enumerate(entrypoints):
|
||||
permissions = entrypoint.get("permissions", [])
|
||||
for perm in permissions:
|
||||
# Handle both simple permissions and scoped permissions (e.g., "filesystem:read")
|
||||
base_perm = perm.split(':')[0] if ':' in perm else perm
|
||||
|
||||
if base_perm not in ALLOWED_PERMISSIONS:
|
||||
violations.append(PolicyViolation(
|
||||
field=f"entrypoints[{idx}].permissions",
|
||||
rule="allowed_permissions",
|
||||
message=f"Invalid permission: '{perm}'. "
|
||||
f"Only {', '.join(sorted(ALLOWED_PERMISSIONS))} are allowed."
|
||||
))
|
||||
|
||||
# For agents, permissions might be in a different location
|
||||
# (checking if there's a permissions field at the top level)
|
||||
elif manifest_type == "agent":
|
||||
permissions = manifest.get("permissions", [])
|
||||
for perm in permissions:
|
||||
base_perm = perm.split(':')[0] if ':' in perm else perm
|
||||
|
||||
if base_perm not in ALLOWED_PERMISSIONS:
|
||||
violations.append(PolicyViolation(
|
||||
field="permissions",
|
||||
rule="allowed_permissions",
|
||||
message=f"Invalid permission: '{perm}'. "
|
||||
f"Only {', '.join(sorted(ALLOWED_PERMISSIONS))} are allowed."
|
||||
))
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def validate_status(status: str) -> Optional[PolicyViolation]:
|
||||
"""
|
||||
Validate that status is one of the allowed values.
|
||||
|
||||
Args:
|
||||
status: Status string to validate
|
||||
|
||||
Returns:
|
||||
PolicyViolation if invalid, None if valid
|
||||
"""
|
||||
if not status:
|
||||
return PolicyViolation(
|
||||
field="status",
|
||||
rule="allowed_status",
|
||||
message="Status field is required"
|
||||
)
|
||||
|
||||
if status not in ALLOWED_STATUSES:
|
||||
return PolicyViolation(
|
||||
field="status",
|
||||
rule="allowed_status",
|
||||
message=f"Invalid status: '{status}'. "
|
||||
f"Must be one of: {', '.join(sorted(ALLOWED_STATUSES))}"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_manifest_policies(path: str, manifest_type: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate a single manifest against all policy rules.
|
||||
|
||||
Args:
|
||||
path: Path to manifest file
|
||||
manifest_type: "skill" or "agent" (auto-detected if None)
|
||||
|
||||
Returns:
|
||||
Dictionary with validation results
|
||||
"""
|
||||
violations = []
|
||||
|
||||
try:
|
||||
# Validate path
|
||||
validate_path(path, must_exist=True)
|
||||
|
||||
# Load manifest
|
||||
manifest = load_manifest(path)
|
||||
|
||||
# Auto-detect manifest type if not specified
|
||||
if manifest_type is None:
|
||||
if "skill.yaml" in path or path.endswith(".yaml") and "skills/" in path:
|
||||
manifest_type = "skill"
|
||||
elif "agent.yaml" in path or path.endswith(".yaml") and "agents/" in path:
|
||||
manifest_type = "agent"
|
||||
else:
|
||||
# Try to detect from content
|
||||
if "entrypoints" in manifest or "inputs" in manifest:
|
||||
manifest_type = "skill"
|
||||
elif "capabilities" in manifest or "reasoning_mode" in manifest:
|
||||
manifest_type = "agent"
|
||||
else:
|
||||
manifest_type = "unknown"
|
||||
|
||||
# Validate name
|
||||
name = manifest.get("name")
|
||||
if name:
|
||||
violation = validate_name_format(name)
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
else:
|
||||
violations.append(PolicyViolation(
|
||||
field="name",
|
||||
rule="required_field",
|
||||
message="Name field is required"
|
||||
))
|
||||
|
||||
# Validate version
|
||||
version = manifest.get("version")
|
||||
if version:
|
||||
violation = validate_version_format(version)
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
else:
|
||||
violations.append(PolicyViolation(
|
||||
field="version",
|
||||
rule="required_field",
|
||||
message="Version field is required"
|
||||
))
|
||||
|
||||
# Validate permissions
|
||||
perm_violations = validate_permissions(manifest, manifest_type)
|
||||
violations.extend(perm_violations)
|
||||
|
||||
# Validate status
|
||||
status = manifest.get("status")
|
||||
violation = validate_status(status)
|
||||
if violation:
|
||||
violations.append(violation)
|
||||
|
||||
# Build result
|
||||
success = len(violations) == 0
|
||||
|
||||
result = {
|
||||
"success": success,
|
||||
"path": path,
|
||||
"manifest_type": manifest_type,
|
||||
"violations": [v.to_dict() for v in violations],
|
||||
"violation_count": len(violations)
|
||||
}
|
||||
|
||||
if success:
|
||||
result["message"] = "All policy checks passed"
|
||||
else:
|
||||
result["message"] = f"Found {len(violations)} policy violation(s)"
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating manifest {path}: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"path": path,
|
||||
"error": str(e),
|
||||
"violations": [],
|
||||
"violation_count": 0
|
||||
}
|
||||
|
||||
|
||||
def find_all_manifests() -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Find all skill and agent manifests in the repository.
|
||||
|
||||
Returns:
|
||||
List of tuples (path, type) where type is "skill" or "agent"
|
||||
"""
|
||||
manifests = []
|
||||
|
||||
# Find skill manifests
|
||||
skills_dir = Path(SKILLS_DIR)
|
||||
if skills_dir.exists():
|
||||
for skill_yaml in skills_dir.glob("*/skill.yaml"):
|
||||
manifests.append((str(skill_yaml), "skill"))
|
||||
|
||||
# Find agent manifests
|
||||
agents_dir = Path(AGENTS_DIR)
|
||||
if agents_dir.exists():
|
||||
for agent_yaml in agents_dir.glob("*/agent.yaml"):
|
||||
manifests.append((str(agent_yaml), "agent"))
|
||||
|
||||
return manifests
|
||||
|
||||
|
||||
def validate_batch(strict: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate all manifests in batch mode.
|
||||
|
||||
Args:
|
||||
strict: If True, treat warnings as errors
|
||||
|
||||
Returns:
|
||||
Dictionary with batch validation results
|
||||
"""
|
||||
manifests = find_all_manifests()
|
||||
|
||||
if not manifests:
|
||||
return {
|
||||
"success": True,
|
||||
"mode": "batch",
|
||||
"message": "No manifests found to validate",
|
||||
"total_manifests": 0,
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
results = []
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for path, manifest_type in manifests:
|
||||
logger.info(f"Validating {manifest_type}: {path}")
|
||||
result = validate_manifest_policies(path, manifest_type)
|
||||
results.append(result)
|
||||
|
||||
if result.get("success"):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
overall_success = failed == 0
|
||||
|
||||
return {
|
||||
"success": overall_success,
|
||||
"mode": "batch",
|
||||
"message": f"Validated {len(manifests)} manifest(s): {passed} passed, {failed} failed",
|
||||
"total_manifests": len(manifests),
|
||||
"passed": passed,
|
||||
"failed": failed,
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Enforce policy rules for skill and agent manifests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"manifest_path",
|
||||
nargs="?",
|
||||
help="Path to manifest file to validate (omit for batch mode)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch",
|
||||
action="store_true",
|
||||
help="Validate all manifests in skills/ and agents/ directories"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Treat warnings as errors"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Batch mode
|
||||
if args.batch or not args.manifest_path:
|
||||
logger.info("Running in batch mode")
|
||||
result = validate_batch(strict=args.strict)
|
||||
else:
|
||||
# Single file mode
|
||||
logger.info(f"Validating single manifest: {args.manifest_path}")
|
||||
result = validate_manifest_policies(args.manifest_path)
|
||||
|
||||
# Output JSON result
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if result.get("success") else 1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
error_result = {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "Unexpected error during policy enforcement"
|
||||
}
|
||||
print(json.dumps(error_result, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
78
skills/policy.enforce/skill.yaml
Normal file
78
skills/policy.enforce/skill.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
name: policy.enforce
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Enforces policy rules for skill and agent manifests including naming conventions,
|
||||
semantic versioning, permissions validation, and status lifecycle checks. Supports
|
||||
both single-file validation and batch mode for scanning all manifests in skills/
|
||||
and agents/ directories.
|
||||
|
||||
inputs:
|
||||
- name: manifest_path
|
||||
type: string
|
||||
required: false
|
||||
description: Path to a single skill.yaml or agent.yaml manifest file to validate
|
||||
|
||||
- name: batch
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: Enable batch mode to scan all manifests in skills/ and agents/ directories
|
||||
|
||||
- name: strict
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: Enable strict mode where warnings become errors
|
||||
|
||||
outputs:
|
||||
- name: validation_report
|
||||
type: object
|
||||
description: Detailed validation results including violations, warnings, and success status
|
||||
|
||||
- name: violations
|
||||
type: array
|
||||
description: List of policy violations found in the manifest(s)
|
||||
|
||||
- name: success
|
||||
type: boolean
|
||||
description: Whether the manifest(s) passed all policy checks
|
||||
|
||||
dependencies:
|
||||
- context.schema
|
||||
|
||||
status: active
|
||||
|
||||
entrypoints:
|
||||
- command: /policy/enforce
|
||||
handler: policy_enforce.py
|
||||
runtime: python
|
||||
description: >
|
||||
Validate skill or agent manifest against policy rules for naming conventions,
|
||||
semantic versioning, permissions, and status values.
|
||||
parameters:
|
||||
- name: manifest_path
|
||||
type: string
|
||||
required: false
|
||||
description: Path to the manifest file to validate (omit for batch mode)
|
||||
|
||||
- name: --batch
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: Enable batch mode to validate all manifests
|
||||
|
||||
- name: --strict
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: Treat warnings as errors
|
||||
permissions:
|
||||
- filesystem
|
||||
- read
|
||||
|
||||
tags:
|
||||
- governance
|
||||
- policy
|
||||
- validation
|
||||
- naming
|
||||
- versioning
|
||||
Reference in New Issue
Block a user