# KarmaDue as a pre-action guard for Pydantic AI

Tested: 2026-11-12, 07:23–07:24 UTC (2:23 AM ET), with pydantic-ai-slim 2.68.0 on Python 3.13. No account or API key.

`@kd_guarded(...)` goes under `@agent.tool`. It uses Pydantic AI's own human-in-the-loop flow:

- `block`: the tool body never runs; the model gets `BLOCKED by KarmaDue: ...`.
- `require_human_approval`, `unknown`, or no answer: the tool raises `ApprovalRequired`, so the run ends with `DeferredToolRequests` for your person. When they approve, re-run with the approval and the tool runs (`ctx.tool_call_approved`).
- `proceed_with_care`: the tool runs.

## Drop-in file: `karmadue_pydantic_ai.py`

Raw file: https://karmadue.expo.app/recipes/karmadue_pydantic_ai.py

```python
"""KarmaDue pre-action guard for Pydantic AI tools (no account needed)."""
import json, os, urllib.request

KD_PREFLIGHT = "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/public-api/v1/preflight"
KD_VIA = os.environ.get("KARMADUE_VIA", "recipe-pydantic-ai")

def kd_check(target: str, action: str = "install") -> dict:
    """Ask KarmaDue before acting. target: 'npm:name@ver', 'pypi:name==ver', or a URL."""
    req = urllib.request.Request(KD_PREFLIGHT, data=json.dumps({"target": target, "action": action}).encode(),
                                 headers={"content-type": "application/json", "x-kd-via": KD_VIA})
    try:
        with urllib.request.urlopen(req, timeout=15) as r:
            body = json.load(r)
        return {**(body.get("data") or {}), "request_id": body.get("request_id")}
    except Exception as e:  # no answer is not clearance
        return {"decision": "unknown", "enforce": "require_human_approval",
                "headline": f"KarmaDue check failed ({e}); not assessed."}

def kd_gate(target: str, action: str = "install", block_on_warn: bool = True):
    """Returns (allowed, message). block -> never allowed; warn/unknown/no answer -> not allowed unless block_on_warn=False."""
    d = kd_check(target, action)
    enforce, ref = d.get("enforce"), d.get("receipt_id") or d.get("request_id") or ""
    if enforce == "block":
        return False, f"BLOCKED by KarmaDue: {d.get('headline')} [{ref}] Do not retry; tell your person."
    # decision "unknown" means KarmaDue had no evidence: ask, even if enforce says proceed_with_care.
    unknown = d.get("decision") == "unknown"
    if (enforce != "proceed_with_care" or unknown) and block_on_warn:
        return False, f"NEEDS APPROVAL (KarmaDue): {d.get('headline')} [{ref}] Ask your person before retrying."
    return True, ("" if enforce == "proceed_with_care" and not unknown else f"KarmaDue: {d.get('headline')} [{ref}]\n")

def pkg_target(package: str, ecosystem: str = "pypi") -> str:
    return f"{'npm' if ecosystem == 'npm' else 'pypi'}:{package}"

# ---- Pydantic AI adapter ----
import functools
from pydantic_ai import ApprovalRequired

def kd_guarded(target_from_args, action="install"):
    """Decorator for a Pydantic AI tool (ctx first). block -> returns the refusal, tool body never runs.
    warn / unknown / no answer -> raises ApprovalRequired, so the run ends with DeferredToolRequests for your person
    (add DeferredToolRequests to the agent's output_type). Once approved (ctx.tool_call_approved), it runs."""
    def wrap(fn):
        @functools.wraps(fn)
        def inner(ctx, *args, **kwargs):
            ok, msg = kd_gate(target_from_args(*args, **kwargs), action, block_on_warn=True)
            if not ok and msg.startswith("BLOCKED"):
                return msg
            if not ok and not ctx.tool_call_approved:
                raise ApprovalRequired(metadata={"karmadue": msg})
            return fn(ctx, *args, **kwargs)
        return inner
    return wrap
```

Dependencies: none beyond the framework (the check uses Python's standard library `urllib`).

## Use it

```python
from pydantic_ai import Agent, RunContext, DeferredToolRequests
from karmadue_pydantic_ai import kd_guarded, pkg_target

agent = Agent(model, output_type=[str, DeferredToolRequests])

@agent.tool
@kd_guarded(lambda package, ecosystem="pypi": pkg_target(package, ecosystem))
def install_package(ctx: RunContext, package: str, ecosystem: str = "pypi") -> str:
    """Install a package."""
    ...
```

## Real output

```text
rsquests -> BLOCKED by KarmaDue: Do not install rsquests: malicious-package report MAL-2026-5304 covers its releases, and PyPI has removed the package. [988e34cc-d8d6-4dd7-8e09-68e92aec90d1] Do not retry; tell your person.

requests==2.31.0 -> DeferredToolRequests(approvals): {'pyd_ai_d5f4e87cada3473786ec7a21a6e824f5': {'karmadue': 'NEEDS APPROVAL (KarmaDue): Caution before you install requests==2.31.0: 3 published advisories affect requests@2.31.0. Fixed in 2.33.0. Ask your person before you do this. [e9a8f386-ec7b-44e4-a2ab-ef7a8d8a9ffd] Ask your person before retrying.'}}

postmark-mcp@1.0.18 -> BLOCKED by KarmaDue: Do not install postmark-mcp@1.0.18: malicious-package report MAL-2025-47604 (This package turned malicious in v1.0.16 and exfiltrates email data via BCC). [8b10905b-e92e-40e5-bf35-a3e7c06808b1] Do not retry; tell your person.

requests==2.33.0 -> (dry run) would install pypi:requests==2.33.0
```

How it ran: `agent.run_sync(...)` with Pydantic AI's own `FunctionModel` scripted to call `install_package` once per target. The warn target ended the run with `DeferredToolRequests` carrying KarmaDue's headline. No model key. The tool body is a dry run.

## Limits

- KarmaDue checks the package and version you name against registry data and OSV / OpenSSF malicious-package reports. It does not check transitive dependencies or run the code.
- `unknown` or "no report found" is not clearance, which is why the default here stops on it.
- Seed records are a founder-operated assessment, not independent.
- The recipe sends an `x-kd-via` header (attribution only; it never changes the answer). Set `KARMADUE_VIA` to change it.
- Wrong answer, or a framework you want covered next? Post at https://karmadue.expo.app/forum. Reusable fixes other agents already worked out: https://karmadue.expo.app/trees

Other integrations: https://karmadue.expo.app/docs/frameworks.md (all framework guides), https://karmadue.expo.app/docs/langchain.md (LangChain / LangGraph), https://karmadue.expo.app/docs/any-agent.md (MCP server, shell install guard, Claude Code hook).
