# KarmaDue as a pre-action guard for the Vercel AI SDK

Tested: 2026-11-12, 07:23–07:24 UTC (2:23 AM ET), with ai 7.0.170 and zod on Node 24. No account or API key.

Wrap any AI SDK `tool()` that installs, runs or connects something. Before `execute` runs, the wrapper makes one HTTP call to KarmaDue's public preflight 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. Pass `{ blockOnWarn: false }` to run anyway with the warning attached.

The model gets one short headline instead of registry and advisory dumps (token cost: https://karmadue.expo.app/docs/token-cost.md).

## Drop-in file: `karmadue-guard-vercel-ai.mjs`

Raw file: https://karmadue.expo.app/recipes/karmadue-guard-vercel-ai.mjs

```js
// KarmaDue pre-action guard for the Vercel AI SDK (no account, no dependencies beyond `ai`).
const KD_PREFLIGHT = "https://ogogoizwsfaduzehkshb.supabase.co/functions/v1/public-api/v1/preflight";
const KD_VIA = process.env.KARMADUE_VIA || "recipe-vercel-ai";

export async function kdCheck(target, action = "install") {
  try {
    const r = await fetch(KD_PREFLIGHT, {
      method: "POST",
      headers: { "content-type": "application/json", "x-kd-via": KD_VIA },
      body: JSON.stringify({ target, action }),
      signal: AbortSignal.timeout(15000),
    });
    const b = await r.json();
    return { ...(b.data || {}), request_id: b.request_id };
  } catch (e) { // no answer is not clearance
    return { decision: "unknown", enforce: "require_human_approval", headline: `KarmaDue check failed (${e}); not assessed.` };
  }
}

// Wrap a tool's execute. targetFromInput(input) -> "npm:name@ver" | "pypi:name==ver" | URL
export function kdGuard(t, targetFromInput, { action = "install", blockOnWarn = true } = {}) {
  const execute = t.execute;
  return {
    ...t,
    execute: async (input, options) => {
      const d = await kdCheck(targetFromInput(input), action);
      const ref = d.receipt_id || d.request_id || "";
      if (d.enforce === "block")
        return `BLOCKED by KarmaDue: ${d.headline} [${ref}] Do not retry; tell your person.`;
      if (blockOnWarn && (d.enforce !== "proceed_with_care" || d.decision === "unknown"))
        return `NEEDS APPROVAL (KarmaDue): ${d.headline} [${ref}] Ask your person before retrying.`;
      const out = await execute(input, options);
      return d.enforce === "proceed_with_care" && !(d.decision === "unknown") ? out : `KarmaDue: ${d.headline} [${ref}]\n${out}`;
    },
  };
}
```

## Use it

```js
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";
import { kdGuard } from "./karmadue-guard-vercel-ai.mjs";

const installPackage = kdGuard(
  tool({
    description: "Install a package. package: 'requests==2.33.0' (pypi) or 'left-pad@1.3.0' (npm).",
    inputSchema: z.object({ package: z.string(), ecosystem: z.enum(["pypi", "npm"]).default("pypi") }),
    execute: async ({ package: p, ecosystem }) => `(dry run) would install ${ecosystem}:${p}`, // your real installer here
  }),
  ({ package: p, ecosystem = "pypi" }) => `${ecosystem}:${p}`,   // input -> KarmaDue target
);

await generateText({ model, tools: { installPackage }, prompt, stopWhen: stepCountIs(5) });
```

For an MCP-connect or shell tool, map its input to the server's package or URL and pass `{ action: "run" }` or `{ action: "connect" }`. `streamText` and `ToolLoopAgent` call the same `execute`, so they are guarded the same way.

## 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. [8254f414-0a77-4169-b78e-482edcf15fd2] 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. [c11cebb7-aa8f-496d-915e-c2146a83ef0c] 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). [88c06207-b1c6-4a0d-ac95-aa1df4f3c239] Do not retry; tell your person.

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

How it ran: `generateText` with the AI SDK's own `MockLanguageModelV4` (from `ai/test`) scripted to emit one `installPackage` tool call per target, so the real AI SDK tool loop ran the guarded `execute`. No model key. The tool body is a dry run.

## Notes for this framework

- The AI SDK also has per-tool `needsApproval`. You can use `needsApproval: async (input) => (await kdCheck(target(input))).enforce !== "proceed_with_care"` to route warnings to your approval UI; keep the `kdGuard` wrapper too so `block` can never be approved through.

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