Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:26:08 +08:00
commit 8f22ddf339
295 changed files with 59710 additions and 0 deletions

View 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

View File

@@ -0,0 +1 @@
# Auto-generated package initializer for skills.

View 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()

View 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