Skip to content
Agentic Control Plane

Governed Mastra in 3 minutes

David Crowe · 5 min read
mastra typescript governance three-minute-governance

Mastra is the TypeScript agent framework that took the Python ergonomics from CrewAI and LangGraph and made them feel native to the Node ecosystem. Type-safe tools, multi-provider model routing (openai/gpt-4o-mini, anthropic/claude-sonnet-4-6, google/gemini-*), workflows, and processors. It’s clean.

What it doesn’t ship: per-user identity attribution on tool calls, cross-tenant audit, output redaction, or pluggable policy. The framework’s requireApproval: true gates a tool with a stream-level approval event, and processors handle message-content guardrails — but neither is the same as a per-call policy point with verified user identity.

This post is the 3-minute path to closing that gap with Agentic Control Plane. The base @agenticcontrolplane/governance package composes directly with Mastra’s createTool({...execute}) — no framework-specific adapter needed.

The pattern

Tool-layer governance via governed("name", fn). Each tool’s execute callback is wrapped so every call runs preToolUse → execute → postToolOutput against ACP. Identity is bound once per request via withContext. Mastra’s tool registration is unchanged — the wrapped function is invisible to the framework.

Three minutes from blank slate

1. Install

npm install @agenticcontrolplane/governance @mastra/core zod

2. Wrap your tools

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

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-agent",
  name: "My Mastra Agent",
  instructions: "You are an ACP-governed agent.",
  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-agent")!.generate(req.body.prompt);
      res.json({ result: result.text });
    },
  );
});

3. Run it

export ACP_USER_TOKEN=gsk_...
export OPENAI_API_KEY=...
node my-agent.mjs

Open cloud.agenticcontrolplane.com/activity. One row appears for lookup_record — actor, tool, decision, session, latency, input/output preview with redactions applied if policy says so. Three minutes, two integration calls, full audit.

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 token bound by withContext.
  2. Deny → the wrapped function returns "tool_error: <reason>". Mastra delivers it to the model as tool output; the model adapts.
  3. Allow → your function runs.
  4. Post-audit: ACP scans the output for PII / secrets. Redacted version replaces the original if policy says so.

Mastra’s createTool({...execute}) receives the wrapped function — governance is invisible to the framework.

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, most restrictive.
  • api — programmatic call from your backend.

A destructive tool denied in background (a scheduled Mastra workflow) can be allowed in interactive (a human-supervised request). The same tool, the same agent code, different policies — Mastra doesn’t need to know.

Why no framework-specific adapter?

CrewAI has acp-crewai. LangGraph has acp-langchain. Anthropic Agent SDK has governance-anthropic. Mastra doesn’t — and doesn’t need one. The base governed(name, fn) helper takes any async function and returns a governed version with the same signature. Mastra’s createTool({execute}) accepts the wrapped function transparently. The composition is direct.

The same is true for Vercel AI SDK and any future TypeScript framework with the same execute shape. The base package is the right home for tool-layer governance in TypeScript; framework-specific packages exist when there’s framework-specific semantics to add (CrewAI’s inter-agent handoffs, Anthropic’s tool roster shape).

Composing with Mastra’s other governance surfaces

Mastra ships two governance-adjacent features:

  • requireApproval: true on a tool gates with a stream-level human-in-the-loop event. Use for sensitive tools that need a person to confirm before firing. Complementary to per-call ACP policy — they fire at different points.
  • Processors (inputProcessors, outputProcessors) handle message-content guardrails: PII detection on the user’s prompt, prompt-injection scanning, moderation. Complementary to tool-layer governance, not a replacement.

A real-world Mastra deployment often uses all three: processors for input/output content guardrails, requireApproval for high-blast-radius tools that need human confirmation, ACP for per-call policy + audit + identity attribution.

What this unlocks

Mastra is the cleanest TypeScript surface for shipping multi-provider agents in 2026. ACP plugs in directly via the base governance package — three lines of integration, full audit, identity-propagated policy, no framework-specific adapter to learn.

Mastra integration guide → · Three-minute integrations → · Get started →

Get the next post
Agentic governance, AgentGovBench updates, the occasional incident post-mortem. One email per post. No marketing fluff.
Share: Twitter LinkedIn
Related posts

← back to blog