Skip to content
Agentic Control Plane

Microsoft Agent Framework + ACP — Policy & Audit Install Guide

Microsoft Agent Framework (MAF) is Microsoft’s unified successor to AutoGen and Semantic Kernel — GA since April 2026 — for building agents and multi-agent workflows in Python and .NET. It ships three middleware scopes (agent run, chat, function); the function scope wraps every tool invocation with a call_next delegate. That’s exactly the seam ACP needs.

acp-governance plugs into it with one class. A single FunctionMiddleware passed at agent construction routes every tool call through ACP: policy check before execution, audit + PII scan after, the end user’s identity on every row. Same /govern/tool-use endpoint, same workspace policies as Claude Code.

Starter · 5-minute install. pip install acp-governance agent-framework-core agent-framework-anthropic, add one middleware, bind the JWT per request. See the runnable starter, the policy model, or the frameworks index.

Install

pip install acp-governance agent-framework-core agent-framework-anthropic

Minimal agent, fully covered

import json
from typing import Any, Awaitable, Callable

from pydantic import BaseModel

from acp_governance import configure, post_tool_output, pre_tool_use, set_context
from agent_framework import Agent, FunctionInvocationContext, FunctionMiddleware
from agent_framework.anthropic import AnthropicClient

configure(base_url="https://api.agenticcontrolplane.com")


class ACPFunctionMiddleware(FunctionMiddleware):
    """One instance governs every tool the agent owns."""

    async def process(
        self,
        context: FunctionInvocationContext,
        call_next: Callable[[], Awaitable[None]],
    ) -> None:
        tool_name = context.function.name
        args = context.arguments
        tool_input = args.model_dump() if isinstance(args, BaseModel) else dict(args or {})

        allowed, reason = pre_tool_use(tool_name, tool_input)
        if not allowed:
            # Never call call_next(): the tool function does not execute.
            context.result = f"tool_error: {reason or 'denied by policy'}"
            return

        await call_next()

        output = context.result
        serialized = output if isinstance(output, str) else json.dumps(output, default=str)
        verdict = post_tool_output(tool_name, tool_input, serialized)
        if verdict:
            if verdict.get("action") == "redact":
                context.result = verdict.get("modified_output", "[ACP] Redacted")
            elif verdict.get("action") == "block":
                context.result = f"[ACP] Blocked: {verdict.get('reason', 'policy')}"


def lookup_record(id: str) -> str:
    """Look up a record by ID."""
    return json.dumps(db.records.find_one({"id": id}))


def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return mailer.send(to=to, subject=subject, body=body)


agent = Agent(
    client=AnthropicClient(model_id="claude-sonnet-4-6"),
    instructions="You are an ACP-governed agent. Use the tools available.",
    tools=[lookup_record, send_email],
    middleware=ACPFunctionMiddleware(),
)

Per request, bind the end user’s identity before the agent runs:

@app.post("/run")
async def run(prompt: str, authorization: str = Header(...)):
    set_context(
        user_token=authorization.removeprefix("Bearer ").strip(),
        agent_name="my-maf-agent",
        agent_tier="interactive",
    )
    response = await agent.run(prompt)
    return {"result": response.text}

What happens on every tool call

  1. MAF resolves the model’s function call and enters the middleware chain; ACP POSTs to /govern/tool-use with the tool name, validated input, and the user JWT bound by set_context.
  2. Deny → the middleware skips call_next(), so your function never runs. The model receives tool_error: <reason> as the tool output and adapts.
  3. Allowcall_next() executes your function.
  4. Post-audit: ACP scans the output for PII / secrets. redact swaps in the redacted version; block replaces the output entirely. Audit row written, rooted in the end user’s identity.

One middleware vs. per-tool decorators

In decorator-based frameworks, coverage is opt-in per function — forget the decorator on tool #7 and it runs unchecked. MAF’s function middleware inverts that: coverage is a property of the agent, not of each tool. Add tools freely — plain functions, @tool-decorated functions, dynamically added tools — the middleware wraps all of them. You can also pass the middleware per-run (agent.run(..., middleware=...)) to scope it to specific invocations.

Per-tier policy

set_context(agent_tier="...") controls the policy tier:

  • 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.

MAF-specific notes

  • Package layout. agent-framework on PyPI is a meta-package that pulls every connector; agent-framework-core covers agents + tools + middleware. Provider chat clients install separately (agent-framework-anthropic, agent-framework-openai, agent-framework-azure-ai, …) and load under agent_framework.<provider> namespaces — swapping providers doesn’t touch the middleware.
  • Native approvals compose. MAF ships approval_mode on tools and ToolApprovalMiddleware for in-process human sign-off. ACP doesn’t replace that UX — it adds the workspace layer: standing rules, per-user policy, and an audit trail that spans every framework your fleet runs.
  • AutoGen / Semantic Kernel migrations. If you’re consolidating AutoGen or SK agents onto MAF, this is the moment to standardize control in one place — the middleware travels with the agent definition, not with each tool you port.
  • Exception semantics. An ordinary exception raised in function middleware becomes a tool-error result and the run continues; MiddlewareFailure aborts fail-closed. ACP’s middleware uses the context.result override so denials stay legible to the model.

Limitations

  • Tool-layer, not token-layer. LLM calls go direct to your provider with your own key. Route model traffic through the ACP proxy separately if you want per-user cost metering.
  • Python first. This guide covers MAF’s Python package; the .NET middleware surface is equivalent (IFunctionMiddleware) and an example is on the roadmap.
  • Pre-release SDK. acp-governance is on 0.x. A framework-specific acp-maf helper package may follow; the class above is ~30 lines and yours to own either way.