Skill: adad44/guardrailsfor-agent (version 1.0.2)

--- name: agent-policy-guardrails-engine description: Build, run, and extend the Agent Policy & Guardrails Engine. Use when implementing policy formats, enforcement logic, decision conflict resolution, policy APIs, and audit/compliance workflows. --- # Agent Policy & Guardrails Engine ## Use This Skill When - You need to add or change policy enforcement behavior. - You need to add policy types (financial, privacy, communication, operational, approval, time-based). - You need to extend decision outputs (`ALLOW`, `DENY`, `MODIFY`, `REQUIRE_APPROVAL`). - You need to update APIs, persistence, or audit logging. ## Project Layout - `app/main.py`: FastAPI endpoints. - `app/service.py`: orchestration for policy CRUD + evaluation + audit writes. - `app/engine.py`: core policy evaluation and conflict resolution. - `app/policy_parser.py`: JSON/YAML/NL policy parsing into structured definitions. - `app/schemas.py`: request/response and policy schemas. - `app/models.py`: SQLAlchemy models (`policies`, `audit_logs`). - `app/seed.py`: baseline policies. - `tests/test_api.py`: API-level behavior. - `tests/test_engine.py`: decision logic behavior. ## Standard Workflow 1. Implement schema/model changes first if policy structure changes. 2. Update parser and engine evaluation paths. 3. Update API/service layer only as needed. 4. Add or update tests for both engine and API. 5. Run tests before finalizing. ## Commands Install and test: ```bash python3 -m pip install -r requirements.txt python3 -m pytest ``` Run locally: ```bash python3 -m uvicorn app.main:app --reload ``` ## Enforcement Contract All external agent/tool actions must be sent to `POST /evaluate` before execution. Runtime handling expectations: - `DENY`: block execution. - `REQUIRE_APPROVAL`: pause and require explicit human approval. - `MODIFY`: apply returned `modifications`, then execute. - `ALLOW`: execute. ## Conflict Resolution Rules When multiple policies match the same action: 1. Highest `priority` wins. 2. If tied on priority, effect severity wins: `DENY > REQUIRE_APPROVAL > MODIFY > ALLOW`. ## Adding New Guardrails 1. Add a structured policy in `app/seed.py` (optional baseline). 2. Ensure `action_types` and `conditions` map to real runtime payload fields. 3. Add API test coverage in `tests/test_api.py` using `/evaluate`. 4. Add engine-level tests in `tests/test_engine.py` for edge/conflict cases. ## Notes - Keep policy evaluation deterministic. - Prefer structured JSON/YAML policies for complex controls. - Natural-language rules should compile into the same structured policy schema.

path: app/service.py
content: from __future__ import annotations

from typing import Optional

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.engine import evaluate_action
from app.models import AuditLog, Policy
from app.policy_parser import (
    parse_json_policy,
    parse_natural_language_policy,
    parse_structured_definition,
    parse_yaml_policy,
)
from app.schemas import ActionRequest, DecisionResponse, PolicyCreate, PolicyFormat, PolicyUpdate


def _compile_policy(create: PolicyCreate) -> dict:
    if create.policy_format == PolicyFormat.STRUCTURED:
        definition = create.definition
        if hasattr(definition, "model_dump"):
            return definition.model_dump()  # type: ignore[no-any-return]
        return parse_structured_definition(definition or {})
    if create.policy_format == PolicyFormat.JSON:
        return parse_json_policy(create.raw_policy or "")
    if create.policy_format == PolicyFormat.YAML:
        return parse_yaml_policy(create.raw_policy or "")
    if create.policy_format == PolicyFormat.NATURAL_LANGUAGE:
        return parse_natural_language_policy(create.raw_policy or "")
    raise ValueError(f"Unsupported policy format: {create.policy_format}")


def create_policy(db: Session, payload: PolicyCreate) -> Policy:
    existing = db.execute(select(Policy).where(Policy.policy_id == payload.policy_id)).scalar_one_or_none()
    if existing:
        raise ValueError(f"Policy '{payload.policy_id}' already exists")

    compiled = _compile_policy(payload)
    policy = Policy(
        policy_id=payload.policy_id,
        name=payload.name,
        description=payload.description,
        policy_format=payload.policy_format.value,
        source_text=payload.raw_policy,
        definition=compiled,
        priority=payload.priority,
        enabled=payload.enabled,
    )
    db.add(policy)
    db.commit()
    db.refresh(policy)
    return policy


def list_policies(db: Session) -> list[Policy]:
    return list(db.execute(select(Policy).order_by(Policy.priority.desc(), Policy.policy_id)).scalars().all())


def get_policy(db: Session, policy_id: str) -> Optional[Policy]:
    return db.execute(select(Policy).where(Policy.policy_id == policy_id)).scalar_one_or_none()


def update_policy(db: Session, policy: Policy, payload: PolicyUpdate) -> Policy:
    updates = payload.model_dump(exclude_unset=True)
    if "definition" in updates:
        definition = updates["definition"]
        if hasattr(definition, "model_dump"):
            policy.definition = definition.model_dump()
        else:
            policy.definition = parse_structured_definition(definition)

    for field in ("name", "description", "priority", "enabled"):
        if field in updates:
            setattr(policy, field, updates[field])

    db.add(policy)
    db.commit()
    db.refresh(policy)
    return policy


def evaluate(db: Session, action: ActionRequest) -> DecisionResponse:
    policies = list(db.execute(select(Policy).where(Policy.enabled.is_(True))).scalars().all())
    result = evaluate_action(action, policies)

    audit = AuditLog(
        action_id=action.action_id,
        action_type=action.action_type,
        action_payload=action.model_dump(mode="json"),
        decision=result.model_dump(mode="json"),
        matched_policies=[p.model_dump(mode="json") for p in result.matched_policies],
    )
    db.add(audit)
    db.commit()

    return result


def list_audit_logs(db: Session, limit: int = 50) -> list[AuditLog]:
    return list(
        db.execute(select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit)).scalars().all()
    )


path: app/db.py
content: from __future__ import annotations

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker

from app.core.config import settings


class Base(DeclarativeBase):
    pass


engine = create_engine(
    settings.database_url,
    pool_pre_ping=True,
    future=True,
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


path: app/core/config.py
content: from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    app_name: str = "Agent Policy & Guardrails Engine"
    environment: str = "dev"
    database_url: str = "sqlite:///./guardrails.db"

    model_config = SettingsConfigDict(env_prefix="GUARDRAILS_", env_file=".env", extra="ignore")


settings = Settings()


path: app/models.py
content: from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List, Optional

from sqlalchemy import JSON, Boolean, DateTime, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column

from app.db import Base


class Policy(Base):
    __tablename__ = "policies"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    policy_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
    name: Mapped[str] = mapped_column(String(255))
    description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    policy_format: Mapped[str] = mapped_column(String(32), default="structured")
    source_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    definition: Mapped[Dict[str, Any]] = mapped_column(JSON)
    priority: Mapped[int] = mapped_column(Integer, default=100, index=True)
    enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())


class AuditLog(Base):
    __tablename__ = "audit_logs"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    action_id: Mapped[Optional[str]] = mapped_column(String(128), index=True, nullable=True)
    action_type: Mapped[str] = mapped_column(String(128), index=True)
    action_payload: Mapped[Dict[str, Any]] = mapped_column(JSON)
    decision: Mapped[Dict[str, Any]] = mapped_column(JSON)
    matched_policies: Mapped[List[Dict[str, Any]]] = mapped_column(JSON)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())


path: app/policy_parser.py
content: from __future__ import annotations

import json
import re
from typing import Any

import yaml
from pydantic import ValidationError

from app.schemas import DecisionType, StructuredPolicyDefinition


def parse_structured_definition(raw: dict[str, Any]) -> dict[str, Any]:
    try:
        return StructuredPolicyDefinition.model_validate(raw).model_dump()
    except ValidationError as exc:
        raise ValueError(f"Invalid policy definition: {exc}") from exc


def parse_json_policy(raw_policy: str) -> dict[str, Any]:
    try:
        loaded = json.loads(raw_policy)
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON policy: {exc}") from exc
    return parse_structured_definition(loaded)


def parse_yaml_policy(raw_policy: str) -> dict[str, Any]:
    try:
        loaded = yaml.safe_load(raw_policy)
    except yaml.YAMLError as exc:
        raise ValueError(f"Invalid YAML policy: {exc}") from exc
    if not isinstance(loaded, dict):
        raise ValueError("YAML policy must produce an object/dictionary")
    return parse_structured_definition(loaded)


def parse_natural_language_policy(raw_policy: str) -> dict[str, Any]:
    text = " ".join(raw_policy.strip().split()).lower()

    amount_patterns = [
        (
            r"deny\s+transactions?\s+over\s+\$?(?P<amount>[0-9]+(?:\.[0-9]{1,2})?)",
            DecisionType.DENY,
            "Transaction exceeds allowed spending limit",
            False,
        ),
        (
            r"require\s+approval\s+for\s+transactions?\s+over\s+\$?(?P<amount>[0-9]+(?:\.[0-9]{1,2})?)",
            DecisionType.REQUIRE_APPROVAL,
            "Transaction requires approval due to spending threshold",
            True,
        ),
    ]

    for pattern, effect, reason, requires_approval in amount_patterns:
        match = re.search(pattern, text)
        if match:
            amount = float(match.group("amount"))
            definition = StructuredPolicyDefinition(
                action_types=["transaction.create", "transaction.execute"] if "transactions" in text else ["*"],
                effect=effect,
                conditions=[
                    {
                        "field": "payload.amount",
                        "operator": "gt",
                        "value": amount,
                    }
                ],
                reason=reason,
                requires_approval=requires_approval,
            )
            return definition.model_dump()

    pii_match = re.search(r"deny\s+(sharing|sending)\s+(pii|sensitive\s+data).*(email|slack|sms)", text)
    if pii_match:
        channel = pii_match.group(3)
        definition = StructuredPolicyDefinition(
            action_types=["message.send", "email.send", "notification.send", "*"],
            effect=DecisionType.DENY,
            conditions=[
                {"field": "payload.contains_pii", "operator": "eq", "value": True},
                {"field": "payload.channel", "operator": "eq", "value": channel},
            ],
            reason="Privacy policy blocks transmission of PII on restricted channels",
        )
        return definition.model_dump()

    time_match = re.search(
        r"(allow|deny)\s+.*\s+(between|outside)\s+(?P<start>\d{1,2}:\d{2})\s+(and|to|-)\s+(?P<end>\d{1,2}:\d{2})",
        text,
    )
    if time_match:
        mode = time_match.group(1)
        boundary = time_match.group(2)
        start = time_match.group("start")
        end = time_match.group("end")
        operator = "time_outside" if boundary == "outside" else "time_between"
        effect = DecisionType.DENY if mode == "deny" else DecisionType.ALLOW
        definition = StructuredPolicyDefinition(
            action_types=["*"],
            effect=effect,
            conditions=[
                {
                    "field": "timestamp",
                    "operator": operator,
                    "value": start,
                    "value_to": end,
                    "timezone": "UTC",
                }
            ],
            reason="Action violates allowed operating time window",
        )
        return definition.model_dump()

    raise ValueError(
        "Could not parse natural language policy. Use a structured policy, or use a supported template "
        "(e.g. 'deny transactions over 1000', 'require approval for transactions over 5000')."
    )


path: app/seed.py
content: from __future__ import annotations

from app.schemas import PolicyCreate


def default_policies() -> list[PolicyCreate]:
    return [
        PolicyCreate(
            policy_id="FIN-001",
            name="Max transaction without approval",
            description="Require approval above $5,000.",
            priority=200,
            policy_format="structured",
            definition={
                "action_types": ["transaction.create", "transaction.execute"],
                "effect": "REQUIRE_APPROVAL",
                "conditions": [{"field": "payload.amount", "operator": "gt", "value": 5000}],
                "reason": "Transaction exceeds approval threshold of 5000",
                "requires_approval": True,
            },
        ),
        PolicyCreate(
            policy_id="PRI-001",
            name="Restrict PII over email",
            description="No PII over external email channels.",
            priority=300,
            policy_format="structured",
            definition={
                "action_types": ["email.send", "message.send"],
                "effect": "DENY",
                "conditions": [
                    {"field": "payload.contains_pii", "operator": "eq", "value": True},
                    {"field": "payload.channel", "operator": "eq", "value": "email"},
                ],
                "reason": "PII cannot be sent over email",
            },
        ),
        PolicyCreate(
            policy_id="OPS-001",
            name="Maintenance window lock",
            description="Deny infra operations outside 08:00-18:00 UTC.",
            priority=250,
            policy_format="structured",
            definition={
                "action_types": ["infrastructure.change"],
                "effect": "DENY",
                "conditions": [
                    {
                        "field": "timestamp",
                        "operator": "time_outside",
                        "value": "08:00",
                        "value_to": "18:00",
                    }
                ],
                "reason": "Infrastructure changes are only allowed between 08:00-18:00 UTC",
            },
        ),
        PolicyCreate(
            policy_id="COM-001",
            name="Mask internal ticket IDs in outbound chat",
            description="Rewrite outbound messages to remove ticket IDs.",
            priority=100,
            policy_format="structured",
            definition={
                "action_types": ["message.send"],
                "effect": "MODIFY",
                "conditions": [
                    {"field": "payload.channel", "operator": "eq", "value": "external_chat"},
                    {"field": "payload.contains_internal_ticket_id", "operator": "eq", "value": True},
                ],
                "reason": "Outbound chat must redact internal ticket IDs",
                "modifications": {"redact_internal_ticket_ids": True},
            },
        ),
        PolicyCreate(
            policy_id="OPS-GIT-001",
            name="Block force pushes",
            description="Disallow git push --force operations.",
            priority=1000,
            policy_format="structured",
            definition={
                "action_types": ["git.push"],
                "effect": "DENY",
                "conditions": [{"field": "payload.force", "operator": "eq", "value": True}],
                "reason": "Force push is prohibited by repository protection policy",
            },
        ),
        PolicyCreate(
            policy_id="COM-EMAIL-001",
            name="Approval required for inbox deletion",
            description="Require explicit approval before deleting inbox emails.",
            priority=900,
            policy_format="structured",
            definition={
                "action_types": ["email.delete"],
                "effect": "REQUIRE_APPROVAL",
                "conditions": [{"field": "payload.folder", "operator": "eq", "value": "inbox"}],
                "reason": "Deleting inbox emails requires explicit approval",
                "requires_approval": True,
            },
        ),
        PolicyCreate(
            policy_id="DLP-001",
            name="Block sensitive data sharing to external destinations",
            description="Never send sensitive data to external destinations.",
            priority=960,
            policy_format="structured",
            definition={
                "action_types": ["email.send", "message.send", "file.share", "http.request", "webhook.send"],
                "effect": "DENY",
                "conditions": [
                    {"field": "payload.contains_sensitive_data", "operator": "eq", "value": True},
                    {"field": "payload.is_external", "operator": "eq", "value": True},
                ],
                "reason": "Sensitive data cannot be shared externally",
            },
        ),
        PolicyCreate(
            policy_id="DLP-002",
            name="Block API key exposure",
            description="Never expose API keys in outbound messages or shares.",
            priority=980,
            policy_format="structured",
            definition={
                "action_types": ["response.generate", "email.send", "message.send", "file.share", "http.request"],
                "effect": "DENY",
                "conditions": [{"field": "payload.contains_api_key", "operator": "eq", "value": True}],
                "reason": "API keys must never be exposed",
            },
        ),
        PolicyCreate(
            policy_id="OPS-DATA-001",
            name="Deny access to restricted files",
            description="Never allow reads/writes/deletes on restricted files without explicit bypass policy.",
            priority=990,
            policy_format="structured",
            definition={
                "action_types": ["file.read", "file.write", "file.delete", "file.download", "file.upload"],
                "effect": "DENY",
                "conditions": [{"field": "payload.is_restricted", "operator": "eq", "value": True}],
                "reason": "Access to restricted files is prohibited",
            },
        ),
        PolicyCreate(
            policy_id="COM-PII-001",
            name="Mask personal data in outbound outputs",
            description="Apply masking before personal data leaves the system.",
            priority=700,
            policy_format="structured",
            definition={
                "action_types": ["response.generate", "message.send", "email.send", "file.share"],
                "effect": "MODIFY",
                "conditions": [{"field": "payload.contains_personal_data", "operator": "eq", "value": True}],
                "reason": "Personal data must be masked in outbound outputs",
                "modifications": {"mask_personal_data": True},
            },
        ),
        PolicyCreate(
            policy_id="COM-EXT-001",
            name="Approval required for external sharing",
            description="Require explicit approval for external sharing actions.",
            priority=850,
            policy_format="structured",
            definition={
                "action_types": ["email.send", "message.send", "file.share", "http.request", "webhook.send"],
                "effect": "REQUIRE_APPROVAL",
                "conditions": [{"field": "payload.is_external", "operator": "eq", "value": True}],
                "reason": "External sharing requires explicit approval",
                "requires_approval": True,
            },
        ),
    ]


path: requirements.txt
content: fastapi==0.116.1
uvicorn==0.35.0
sqlalchemy==2.0.43
pydantic==2.11.7
pydantic-settings==2.10.1
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pytest==8.4.1
httpx==0.28.1


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: clean | SkillSpector: clean (LOW)