Mastra + ACP — Governance Install Guide
Mastra 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 governance 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/governancecomposes cleanly with Mastra’screateTool(). See the runnable starter, the governance model, or the frameworks index.
Install
npm install @agenticcontrolplane/governance @mastra/core zod
Minimal governed agent
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:
- POSTs to
/govern/tool-usewith the tool name, input, and the user JWT bound bywithContext. - If ACP denies, returns
"tool_error: <reason>"— the model sees this as a tool result and adapts. - If ACP allows, runs your function.
- POSTs the output to
/govern/tool-outputfor audit logging and PII scanning. - If ACP redacts, replaces the output with the redacted version.
- 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 — governance 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/governancepackage composes with Mastra directly. Noacp-mastrashim required. - Mastra’s
requireApproval: trueon 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 governance, not a replacement. - No tool-dispatch middleware in
@mastra/core1.28. Inlinegoverned(execute)is the documented way to add per-tool governance.
Adding more tools
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 governance.
Limitations
- Only tools wrapped with
governedare covered. Plainexecutecallbacks bypass governance. - LLM calls go direct to your provider. ACP governs tools, not tokens. For per-user LLM cost attribution, pair with Portkey or LiteLLM virtual keys.
- Pre-release.
@agenticcontrolplane/governanceis on 0.x. Pin exact versions.