TechFabricTechFabricPlatform
Platform referenceDevelopment patterns

Agents within policy bounds

How AI agents call into the platform under the same governance as humans.

An AI agent in Fabric is not a special user with elevated rights. It is an actor with a credential whose scope is explicit, per-action, checked at submission, and revalidated against the durable command immediately before execution.

The actor flow

action + stable idempotency key authenticate registration and current grant tenant, actor, action and resource scope submitAction + opaque authorizationBindingId persist invocation and validate schema authorizeExecution(original actor, parsed params) current grant pass / deny evaluate declared policies pass / warn / block run handler result and declared events durable invocation result Agent Agent credential authenticated gateway Platform Host Policies Handler

The checks happen at distinct boundaries:

  1. Submission authorization asks whether this authenticated registration may create the invocation.
  2. Execution authorization revalidates the original actor, current grant, expiry, action, and resource scope against the schema-parsed durable parameters.
  3. Domain policy decides whether the authorized action is permissible now under business rules.

The gateway persists an immutable admission and supplies only its opaque, non-secret ID as authorizationBindingId. Credentials and signed proofs never become durable action parameters.

Why the permission check is skipped at invokeAction

invokeAction skips the tenant-Member permission check for actorType: "agent". The reason: an agent does not have a Member row. It has a registration-bound credential and grant. Skipping the human membership lookup does not skip Host authorization. The gateway checks admission and Platform Host 6.x calls authorizeExecution before policies and mutation code for initial execution, approval resume, and recovery.

Bypass paths are explicitly enumerated in packages/api/modules/platform/lib/invoke-action.ts:

const skipPermissionCheck =
  input.actorType === "external_system" ||
  input.actorType === "system" ||
  input.actorType === "agent";
var skipPermissionCheck = input.ActorType is
    ActorType.ExternalSystem or
    ActorType.System or
    ActorType.Agent;

Anything else still goes through Member + requiredPermissions. The agent path is one of three trusted upstream gates, followed by the durable execution-time check.

What policies see

Policies evaluating an agent invocation get a PolicyContext with actionId, parameters, tenantId, spaceId, and mode ("preview" or "execute"). They do not automatically see "this is an agent", by design. A policy that needs to differentiate (e.g. "agents cannot accept offers above $100k") writes that condition directly:

const offerLimitPolicy: PolicyEvaluator = {
  policyId: "orders.agent_amount_limit.v1",
  version: 1,
  evaluate: async (ctx) => {
    // The platform-level audit context (actorType) is available via the action invocation;
    // the policy reads it via db where needed, or via parameters if explicitly threaded.
    // ...
  },
};
public class AgentAmountLimitPolicy : IPolicyEvaluator
{
    public PolicyId PolicyId => new PolicyId("orders", "agent_amount_limit", 1);
    public int Version => 1;

    public async Task<PolicyEvaluationResult> EvaluateAsync(
        IPolicyContext ctx, CancellationToken ct)
    {
        // The platform-level audit context (actorType) is available via the action invocation;
        // the policy reads it via db where needed, or via parameters if explicitly threaded.
        // ...
    }
}

In practice most teams thread agent-specific limits via separate action IDs with their own policy lists, so orders.agent_submit is a different action from orders.submit and carries the agent-specific cap as a routine policy. This avoids "is this an agent?" branching inside one policy.

Designing agent-callable actions

Three rules that make agent operations safe.

1. Explicit, narrow scopes

Don't grant orders.*. Grant ["orders.list", "orders.summarize"]. Read-only by default, with mutating actions added one at a time, each justified.

2. Mutating actions get their own policies

Any action an agent can call should have a policy that captures the human version of "is this OK?". Even if the human-facing UI doesn't enforce it (the human has visual judgment; the agent does not). Examples:

  • orders.agent_send_quote policy: cap dollar amount, cap rate spread, require recent consent.
  • orders.agent_request_consent: rate-limit per external user, cap per day.

3. Approval as a state, not as a hope

For high-stakes actions, use waiting_for_approval. The agent invokes the action; the runtime parks it; a human approves. The audit trail records both the agent's request and the human's approval.

Agent runs

The platform's AgentRun object type lets you bind a sequence of related agent actions to a session. The MCP server typically opens an AgentRun at the start of a conversation and tags every ActionInvocation made during it. This lets compliance ask.

Show me every action this agent took during this user's chat session, in order.

…and get one query, not an archeology project.

Anti-patterns

  • A single shared "agent" credential. Per-agent or per-deployment credentials with their own scope lists. Revocation is cheap that way.
  • Wide scopes "for now." Scopes are forever in audit logs. Start narrow.
  • Branching on actorType inside business logic. If an action behaves differently for agents, it's a different action.
  • Asking the LLM to "be careful." Carefulness is a property of the runtime gates, not the model.

See also

On this page