# Mastra: Per-User Auth, Policy & Audit for Agents

Add per-user policy, audit logging, and PII detection to Mastra agents. One npm install, one governed() wrapper, every tool call audited and policy-checked.

# Per-user auth, policy & audit for Mastra agents

**TL;DR.** `npm i @agenticcontrolplane/governance`, wrap each tool's `execute` with `governed(...)`, bind the end user's identity via `withContext` — and every Mastra tool call gets per-user identity, policy enforcement (allow / deny / redact), PII detection, and an audit trail. Same control model as Claude Code.

[Mastra](https://mastra.ai) is a TypeScript framework for building agents with first-class tools, workflows, and multi-provider model routing. Out of the box, a production deployment shares one backend API key across every end user's request — no per-user policy enforcement, no per-user audit trail, no way to tell downstream systems which human triggered which action.

`@agenticcontrolplane/governance` closes that gap. Wrap each tool's `execute` callback with `governed(...)`; bind the end user's identity per request via `withContext`. Same control model as Claude Code — same `/govern/tool-use` endpoint, same workspace policies.

> **Starter · 5-minute install.** No framework-specific adapter needed — the base `governed()` from `@agenticcontrolplane/governance` composes cleanly with Mastra's `createTool()`. See [the runnable starter](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/starters/mastra), [how a call is decided](/docs/governance-model), or the [frameworks index](/frameworks).

## Install

```bash
npm install @agenticcontrolplane/governance @mastra/core zod
```

## Minimal policy-enforced agent

```typescript
import { Mastra } from "@mastra/core";
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import {
  configure,
  governed,
  withContext,
} from "@agenticcontrolplane/governance";

configure({ baseUrl: "https://api.agenticcontrolplane.com" });

// Wrap the execute callback with governed(name, fn). Mastra calls the
// wrapped version transparently; governance runs on every dispatch.
const lookupRecord = createTool({
  id: "lookup_record",
  description: "Look up a record by ID.",
  inputSchema: z.object({ id: z.string() }),
  outputSchema: z.object({ id: z.string(), data: z.any() }),
  execute: governed("lookup_record", async ({ context }) => {
    return { id: context.id, data: await db.records.findOne({ id: context.id }) };
  }),
});

const agent = new Agent({
  id: "my-mastra-agent",
  name: "My Mastra Agent",
  instructions: "You are an ACP-governed agent. Use the tools available.",
  model: "openai/gpt-4o-mini",
  tools: { lookupRecord },
});

const mastra = new Mastra({ agents: { agent } });

app.post("/run", async (req, res) => {
  const userToken = req.header("authorization")!.replace(/^Bearer /, "").trim();
  await withContext(
    { userToken, agentName: "my-mastra-agent", agentTier: "interactive" },
    async () => {
      const result = await mastra.getAgentById("my-mastra-agent")!.generate(req.body.prompt);
      res.json({ result: result.text });
    },
  );
});
```

## What `governed` does

Wraps any async function with ACP's pre/post hook protocol:

1. POSTs to `/govern/tool-use` with the tool name, input, and the user JWT bound by `withContext`.
2. If ACP denies, returns `"tool_error: <reason>"` — the model sees this as a tool result and adapts.
3. If ACP allows, runs your function.
4. POSTs the output to `/govern/tool-output` for audit logging and PII scanning.
5. If ACP redacts, replaces the output with the redacted version.
6. If ACP blocks the output post-hoc (a leaked secret pattern, for example), returns `"tool_error: <reason>"`.

Mastra's `createTool({...execute})` receives the wrapped function — control is invisible to Mastra.

## Per-tier policy

`withContext` binds an `agentTier` to the request scope:

- **`interactive`** — human at the keyboard, permissive default.
- **`subagent`** — invoked by another agent, no human in the immediate loop.
- **`background`** — autonomous, no human anywhere — most restrictive.
- **`api`** — programmatic call from your backend.

A destructive tool denied in `background` can be allowed in `interactive`. Match the tier to actual deployment reality.

## Mastra-specific notes

- **No framework-specific ACP adapter needed.** The base `@agenticcontrolplane/governance` package composes with Mastra directly. No `acp-mastra` shim required.
- **Mastra's `requireApproval: true`** on a tool gates with a stream-level approval event — orthogonal to ACP. Use for human-in-the-loop on sensitive tools; complementary to per-call ACP policy.
- **Mastra processors** (`inputProcessors` / `outputProcessors`) handle message-content guardrails (PII detection, prompt-injection scanning, moderation) — complementary to tool-layer control, not a replacement.
- **No tool-dispatch middleware in `@mastra/core` 1.28**. Inline `governed(execute)` is the documented way to add per-tool control.

## Adding more tools

```typescript
const sendEmail = createTool({
  id: "send_email",
  description: "Send an email.",
  inputSchema: z.object({
    to: z.string(),
    subject: z.string(),
    body: z.string(),
  }),
  execute: governed("send_email", async ({ context }) => {
    return await mailer.send({ to: context.to, subject: context.subject, body: context.body });
  }),
});

const agent = new Agent({
  ...,
  tools: { lookupRecord, sendEmail },
});
```

Wrap each `execute` with `governed("...")`. Tools outside this pattern bypass control.

## Price and meter the model calls

`governed` is the **interception plane** — what your agent *does*. The **proxy
plane** covers what it *spends*. Both run against the same gateway, and `init()`
wires them together:

```ts
import { init } from "@agenticcontrolplane/governance";

init();                          // call before you construct any model client
```

Model calls now land on the same trail as tool calls, carrying real cost,
tokens, and cache economics. Constructing clients explicitly instead? Pass the
options rather than calling `init()`:

```ts
import { modelClientOptions } from "@agenticcontrolplane/governance";
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(modelClientOptions("anthropic"));
```

Shapes are `"anthropic"`, `"openai"` (chat completions), and
`"openai-responses"`. The last two are **not** interchangeable — `/v1` serves
chat completions, `/openai/v1` serves responses.

`init()` is all-or-nothing per provider: it sets the base URL *and* the key, or
it leaves that provider completely alone and tells you which one it skipped. If
`OPENAI_BASE_URL` already points at your own gateway, ACP won't silently
reroute you.

**Your coverage:** interception ✓ (decorated tools) · proxy ✓ (model calls).

## Limitations

- **Only tools wrapped with `governed` are covered.** Plain `execute` callbacks bypass control.
- **The two planes are wired separately.** The decorator covers tool calls; the proxy covers model calls. Both ship, but each is its own edit — see [Price and meter the model calls](#price-and-meter-the-model-calls) above. Decorating tools without repointing the model client gives you control with no cost data.
- **Pre-release.** `@agenticcontrolplane/governance` is on 0.x. Pin exact versions.

## Related

- [Mastra docs — agents](https://mastra.ai/docs/agents/overview)
- [Mastra docs — tools](https://mastra.ai/docs/agents/using-tools)
- [`@agenticcontrolplane/governance` (core SDK)](https://www.npmjs.com/package/@agenticcontrolplane/governance)
- [Anthropic Agent SDK integration](/integrations/anthropic-agent-sdk)
- [LangChain / LangGraph integration](/integrations/langgraph)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Govern Mastra with Agentic Control Plane",
  "totalTime": "PT5M",
  "step": [
    {"@type": "HowToStep", "name": "Install dependencies", "text": "npm install @agenticcontrolplane/governance @mastra/core zod"},
    {"@type": "HowToStep", "name": "Wrap each tool's execute callback", "text": "Pass execute through governed(name, fn) inside createTool({...})."},
    {"@type": "HowToStep", "name": "Bind the user's JWT per request", "text": "Wrap your request handler body with withContext({ userToken }, async () => { ... })."}
  ]
}
</script>

