Skip to content
Agentic Control Plane

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/governance composes cleanly with Mastra’s createTool(). 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:

  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 — 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/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 governance, not a replacement.
  • No tool-dispatch middleware in @mastra/core 1.28. Inline governed(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 governed are covered. Plain execute callbacks 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/governance is on 0.x. Pin exact versions.