AI agent security

Microsoft Agent Framework security guide

Secure Microsoft Agent Framework for Go with inbound and local-tool policy checks. Cover dynamic tools, trusted identity, human approval, outages, and sessions, while accounting for provider-hosted capabilities.

11 min read
In short: Secure Microsoft Agent Framework for Go with inbound and local-tool policy checks. Cover dynamic tools, trusted identity, human approval, outages, and sessions, while accounting for provider-hosted capabilities.

How do you secure Microsoft Agent Framework for Go?

To secure Microsoft Agent Framework for Go, authenticate the caller, screen inbound text, and authorize each local tool before execution. Use Arcjet Guard wrappers or middleware at those boundaries. Provider-hosted tools execute outside Go and remain outside the adapter's enforcement coverage; restrict them at the provider or service that executes them.

Microsoft Agent Framework for Go connects model providers to local function tools, Model Context Protocol (MCP) tools, sessions, and human approval. Each tool gives the model some of your application's authority. A malicious support ticket or retrieved document can try to redirect that authority toward another customer's data or an unauthorized action.

This guide covers the Go integration listed in Arcjet's supported frameworks. It does not describe the separate Microsoft Python or .NET runtimes. The Go framework and Arcjet adapter are public previews; the examples target agentframework/v0.1.0 and the framework version selected by that module.

Prompt screening cannot grant access to an order, and a human approval cannot substitute for a tenant check. Arcjet Guard supplies runtime policy checks at the inbound and local-tool boundaries.

How do you install and maintain the Go integration?

The Microsoft Agent Framework adapter is a separate module that requires Go 1.26. The root Arcjet SDK supports Go 1.25. Install it explicitly and commit both go.mod and go.sum:

Terminal window
go get github.com/arcjet/arcjet-go/agentframework@v0.1.0
go mod verify
go list -m all
govulncheck ./...

The import path is github.com/arcjet/arcjet-go/agentframework; there is no /v0 suffix. Check the adapter's versioned go.mod when upgrading. Go module requirements are minimums: another dependency can select a newer framework, so test the resolved graph, as well as the version you requested.

Review new tool packages, MCP servers, provider adapters, and their network permissions. Run dependency and secret scans in CI. Keep the model provider key and ARCJET_KEY on the server, and give each tool only the credentials it needs. Never put credentials in model instructions, tool arguments, serialized sessions, or traces.

Which execution paths can Guard protect?

  • Use GuardTool for an authored tool.FuncTool, including functions created by functool.New and agent-as-tool values from agenttool.New.
  • Use GuardTools to wrap selected function tools in a list, such as tools returned by mcptool.ListTools. Audit the selector: a tool it declines remains unguarded.
  • Use GuardMiddleware on agent.Config.Middlewares to screen inbound user messages and wrap the tools visible at the start of a run.
  • Use the root SDK's arcjet.GuardAction for application functions outside the framework.

For an MCP tool list, follow the GuardTools selector example. It passes tools from mcptool.ListTools through a selector that maps tool names to application-owned policies. Pass the returned list to the agent. Returning false leaves a tool unguarded; it does not remove the tool from the list.

Provider-hosted tools such as hostedtool.WebSearch and hostedtool.MCPServer execute outside Go. The adapter passes them through unchanged. A locally connected MCP function tool and a provider-hosted MCP server are different enforcement boundaries. Remove unsupported capabilities from a sensitive agent, restrict them at the provider, or enforce policy inside an MCP server you operate. Do not claim the Go middleware protects a hosted tool.

Where do identity and authorization come from?

Authenticate the HTTP request, job, or channel before creating the agent's request context. Resolve its tenant, user, and permitted resources from your application. Tool arguments are model-controlled, including fields named userId, tenantId, and orderNumber; none is proof of authority.

Use a trusted caller identifier for a per-user budget. Use a separate resource identifier to select the record, then check that the caller can access that record inside the tool's service or database layer. Keep action names application-owned, such as order.looked-up. When using published Guard policies, supply the actor and declared inputs from these trusted sources and verify the policy is active for that action.

The following factory captures a caller already authenticated by the application. Its lookup dependency must authorize access to the requested order for that caller. A rate limit bounds usage; it does not perform that authorization.

How do you guard a local function tool?

Use agentframework.GuardTool to evaluate policy before a local function tool executes. Pass the wrapped tool to agent.Config.Tools. The following package injects an order service through lookup; that service must check authorization before accessing customer data:

package agentsecurity
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/arcjet/arcjet-go"
"github.com/arcjet/arcjet-go/agentframework"
"github.com/microsoft/agent-framework-go/tool"
"github.com/microsoft/agent-framework-go/tool/functool"
)
type LookupArgs struct {
OrderNumber string `json:"orderNumber"`
}
func OrderTool(
client *arcjet.GuardClient,
callerID string,
lookup func(context.Context, string, string) (string, error),
) (tool.FuncTool, error) {
if callerID == "" || lookup == nil {
return nil, fmt.Errorf("authenticated caller and order service required")
}
limit, err := arcjet.GuardTokenBucket(arcjet.GuardTokenBucketOptions{
Mode: arcjet.ModeLive, RefillRate: 10,
Interval: time.Minute, Capacity: 10, Bucket: "order-lookups-per-user",
})
if err != nil {
return nil, err
}
original, err := functool.New(
functool.Config{Name: "lookup_order", Description: "Look up an authorized order"},
func(ctx context.Context, in LookupArgs) (string, error) {
// The service checks callerID's access to this order.
return lookup(ctx, callerID, in.OrderNumber)
},
)
if err != nil {
return nil, err
}
return agentframework.GuardTool(client, original, agentframework.ToolPolicy{
Action: "order.looked-up",
Actor: func(context.Context, json.RawMessage) (string, error) {
return callerID, nil
},
Rules: agentframework.Args(func(_ context.Context, _ LookupArgs) ([]arcjet.GuardRuleInput, error) {
return []arcjet.GuardRuleInput{limit.Key(callerID, 1)}, nil
}),
})
}

Reuse one GuardClient across requests, but create each tool closure for its authenticated caller. Do not reuse another tenant's closure.

Create the client with arcjet.NewGuardClient(arcjet.GuardConfig{}), which reads ARCJET_KEY, and handle initialization errors at startup. Before process exit, call Close with a shutdown context to drain pending capture work.

agentframework.Args decodes model JSON for policy evaluation. Keep its argument type aligned with the function tool. Do not depend on schema defaults being applied during that decode. Treat malformed input and resolver failures as blocked calls rather than retrying the original unwrapped function.

How do you screen text before the model runs?

Use GuardMiddleware with an InboundPolicy to screen user text before Microsoft Agent Framework calls the model provider. Register the middleware in agent.Config.Middlewares. The following helper can live alongside the preceding tool package, with an additional import of github.com/microsoft/agent-framework-go/agent:

func InboundMiddleware(client *arcjet.GuardClient) (agent.Middleware, error) {
scan, err := arcjet.GuardPromptInjection(arcjet.GuardPromptInjectionOptions{
Mode: arcjet.ModeLive,
})
if err != nil {
return nil, err
}
return agentframework.GuardMiddleware(client, agentframework.MiddlewareConfig{
Inbound: &agentframework.InboundPolicy{
Action: "message.received",
Rules: func(_ context.Context, text string) ([]arcjet.GuardRuleInput, error) {
return []arcjet.GuardRuleInput{scan.Text(text)}, nil
},
},
})
}

The inbound middleware evaluates concatenated user-role message text. A denial returns assistant text and stops the run before the provider is invoked. It does not automatically screen every retrieved document, tool result, or later context-provider addition. Treat those as untrusted content and retain the tool authorization boundary even when the initial prompt passes.

Both inbound and tool policies require an action. Handle helper-construction errors before serving traffic; ignoring an invalid configuration can leave the application running without its intended middleware.

How do you cover dynamic tools and human approval?

GuardMiddleware can see agent.Config.Tools and tools supplied through agent.WithTool. It cannot see tools injected later by a context provider's Invoking hook or toolautocall.Config.AdditionalTools. Wrap those tools with GuardTool where they are created, before passing them to the framework. Include them in your test inventory.

GuardTools and GuardMiddleware normally skip tools that already carry the GuardTool marker. Wrapper order matters for approval-required tools:

guarded, err := agentframework.GuardTool(
client,
tool.ApprovalRequiredFunc(original),
policy,
)

Keep GuardTool outermost. Putting ApprovalRequiredFunc around an already guarded tool hides the adapter's marker, so middleware can guard it again and spend two evaluations and two rate-limit tokens. The supported order preserves the approval requirement. A policy ALLOW still does not provide the person's answer to toolapproval.

What happens on denial or an outage?

A guarded function returns an arcjet.GuardDenialResult value when policy denies execution. It does not return a Go error for that denial. The denial result gives the model a structured explanation. The framework sanitizes tool error messages by default, so an error would lose that explanation. Errors from the original function still pass through unchanged.

Keep the default fail-closed behavior for sensitive tools. An unavailable Guard evaluation prevents the function from running and returns an ERROR result with a five-second retry hint. Rate-limit results may also carry a retry delay. Bound retries and total run time in application code; a retry hint is not authorization to repeat an irreversible operation.

OnGuardError: arcjet.OnGuardErrorAllow permits execution during an availability failure and records a degraded outcome. It does not override a policy DENY or an unusable configuration such as an invalid action or empty required rule key. Review that choice per action: even a read-only tool can disclose private data.

If you customize OnDeny, preserve a recognizable denial contract. A tool re-exported over MCP may retain a declared return schema that a generic denial object does not satisfy. Use a schema-compatible result or an output contract that accommodates denial. Never turn a blocked call into a success-shaped payload.

How do you correlate and isolate conversations?

Authorize access to the conversation before loading its history. Store sessions under the owning tenant and user, and keep serialized state and tool results out of public storage. Correlation groups decisions; it is not an access-control token.

Pass the same application-owned conversation ID on every turn:

ctx = arcjet.ContextWithCorrelationID(ctx, conversationID)
response, err := a.RunText(ctx, userText).Collect()

For restored sessions, session.Set(agentframework.CorrelationIDStateKey, conversationID) provides a fallback when the context has no ID. An explicit policy CorrelationID takes precedence over context and session state. Do not use agent.Session.ServiceID: providers may rewrite it during a run. Generating a new ID on every turn also splits the conversation's decisions across Sequences.

Record action, decision, caller reference, and correlation ID with appropriate access controls. Redact raw prompts, tool arguments, credentials, and customer data before logs or traces leave the application.

How do you verify the gates before deployment?

To verify Microsoft Agent Framework enforcement, use deterministic Guard responses and a fake order service with a call counter. Test the tool directly and through a real framework run with a fake provider; a detector occasionally recognizing a malicious phrase is not enough to prove enforcement.

  • Return DENY and assert the service counter stays at zero. Return ALLOW for an authorized lookup and assert it becomes one.
  • Simulate timeout, malformed arguments, and resolver failure. Confirm no fallback calls the unwrapped tool, and verify the configured unavailable result.
  • Supply another tenant's order number with an ALLOW decision. Confirm the application service still refuses access.
  • Try the same action through configured tools, agent.WithTool, context-provider tools, additional tools, and local MCP tools. Explicitly inventory hosted tools as outside adapter coverage.
  • Deny inbound text and assert the fake provider was never called. Test allowed text too, so a broken agent that never runs cannot pass every assertion.
  • Exercise approval and resume paths. Confirm one policy evaluation per execution attempt, no duplicated side effects, and the same conversation ID after restoring a session.

Run go test ./..., go vet ./..., dependency checks, and secret scanning in CI. For irreversible actions, use application idempotency keys and transaction boundaries in addition to policy. Review budget exhaustion, outage behavior, and tenant isolation before enabling the agent for customers.

What do you implement next?

Start with one sensitive tool and its inbound entry point, then expand the inventory to every capability the agent can reach. Keep the Microsoft Agent Framework integration reference beside the versioned adapter source when upgrading. Use AI agent runtime security for broader identity and budget controls and securing MCP server tool calls when enforcement must live at the server.

Frequently asked questions

Does this guide cover Python and .NET Microsoft Agent Framework?

No. It covers the supported Go integration, github.com/arcjet/arcjet-go/agentframework, which requires Go 1.26. Python and .NET have different runtime APIs.

Can Go middleware guard provider-hosted MCP tools?

No. Hosted tools execute at the provider and pass through unchanged. Guard locally executed MCP function tools or enforce inside an MCP server you control.

Does Guard replace human approval?

No. Apply GuardTool outside ApprovalRequiredFunc to preserve approval and avoid duplicate policy evaluations. A policy ALLOW does not answer the human approval request.

What happens when Guard is unavailable?

By default the tool does not execute and returns an ERROR denial result with a retry hint. Availability fail-open is an explicit per-policy choice and never overrides a real policy DENY.

AI runtime security in your code

Protect your AI agent workflows with Arcjet

Arcjet guards run inside the tool, so the allow or deny arrives before the side effect rather than after it.