# KarmaDue as a pre-action guard for AG2

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

AG2 1.x runs tool calls through middleware. `KarmaDueMiddleware` checks every listed tool call with KarmaDue before the tool runs and obeys `enforce`:

- `block`: the action does not run. The model gets `BLOCKED by KarmaDue: ...` with the request id.
- `require_human_approval`, `unknown`, or no answer: by default the action does not run, and the model is told to ask its person.
- `proceed_with_care`: the action runs.

One middleware covers every tool on the agent; add a line to `KD_TARGETS` per tool that acts.

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

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

```python
"""KarmaDue pre-action guard for AG2 1.x (a tool middleware; 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-ag2")

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}"

# ---- AG2 adapter: one middleware guards every listed tool on the agent ----
import asyncio
from ag2.events import ToolResultEvent
from ag2.middleware import BaseMiddleware, Middleware

# tool name -> builds the KarmaDue target from the call's arguments. Add your own.
KD_TARGETS = {
    "install_package": lambda a: pkg_target(a["package"], a.get("ecosystem", "pypi")),
}

class KarmaDueMiddleware(BaseMiddleware):
    def __init__(self, event, context, targets=KD_TARGETS, action="install", block_on_warn=True):
        super().__init__(event, context)
        self.targets, self.action, self.block_on_warn = targets, action, block_on_warn

    async def on_tool_execution(self, call_next, event, context):
        to_target = self.targets.get(event.name)
        if to_target:
            ok, msg = await asyncio.to_thread(kd_gate, to_target(event.serialized_arguments), self.action, self.block_on_warn)
            if not ok:  # the tool never runs; the model gets the message as the tool result
                return ToolResultEvent.from_call(event, msg)
        return await call_next(event, context)

karmadue = Middleware(KarmaDueMiddleware)   # Agent(..., middleware=[karmadue])
```

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

## Use it

```python
from ag2 import Agent, tool
from karmadue_ag2 import karmadue, KD_TARGETS

@tool
def install_package(package: str, ecosystem: str = "pypi") -> str:
    """Install a package."""
    ...

agent = Agent("builder", config=config, tools=[install_package], middleware=[karmadue])
```

## 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. [d61cacc7-17f6-4d35-8114-694c59f830b6] Do not retry; tell your person.

requests==2.31.0 -> 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. [adaed63f-9bab-422d-8795-2e6d4bfd6107] 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). [b4fea64a-405a-4659-9729-042928764025] Do not retry; tell your person.

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

How it ran: an AG2 `Agent` with AG2's own `ag2.testing.TestConfig` scripted to emit one `install_package` call per target; the lines are the `ToolResultEvent`s from the agent's history. No model key. The tool body is a dry run.

## Notes for this framework

- Older AG2 0.x / pyautogen code (`ConversableAgent`, `register_function`) has no middleware: put the same `kd_gate` check at the top of the registered function instead.

## 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).
