Multi-Tenant AI Agents — FabrCore's New ACL Permissioning System

Eric Brasher March 10, 2026 at 9:00 AM 6 min read

When multiple users share an agent cluster, the first question is always the same: who can talk to whose agents? FabrCore's ACL system answers that with a lightweight, rule-based permissioning layer that sits between the client and the agent grain — no extra middleware, no external policy engine. Every cross-owner message is evaluated before it reaches the target agent.

How Handle Ownership Works

FabrCore uses the handle format owner:agentAlias to scope every agent to an owner. When a client sends a message to "assistant" (a bare alias), the framework auto-prefixes the caller's owner, turning it into "user1:assistant". A fully-qualified handle like "user2:assistant" is used as-is — and that is where ACL evaluation kicks in.

The core rule is simple: own-agent access is always allowed. If the caller's owner matches the target agent's owner, the message passes through with full permissions and zero overhead. ACL rules only matter for cross-owner requests.

ScenarioACL Check?Result
user1 sends to user1:assistantNoAllowed (own agent)
user1 sends to user2:assistantYesDepends on rules
user1 sends to system:shared-agentYesDepends on rules

AclRule and the Permission Model

Every ACL rule is an AclRule object with four fields: OwnerPattern (which owner's agents this rule covers), AgentPattern (which agent aliases match), CallerPattern (who is allowed), and Permission (what they can do). Patterns support wildcards, prefix matching, and group references.

C# — AclRule Structure
public class AclRule
{
    public string OwnerPattern { get; set; }   // Target agent's owner
    public string AgentPattern { get; set; }   // Target agent's alias
    public string CallerPattern { get; set; }  // Who is allowed
    public AclPermission Permission { get; set; }
}

Permissions are flags, so they can be combined:

C# — AclPermission Flags
[Flags]
public enum AclPermission
{
    None      = 0,
    Message   = 1,   // Send messages
    Configure = 2,   // Create/reconfigure agents
    Read      = 4,   // Read threads, state, health
    Admin     = 8,   // Modify ACL rules
    All       = Message | Configure | Read | Admin
}

Pattern matching supports four forms:

PatternMatchesExample
"*"AnythingAll owners, agents, or callers
"prefix*"Starts-with"automation_*" matches "automation_agent-123"
"group:name"Group members (CallerPattern only)"group:admins"
"exact"Case-insensitive literal"system"

Evaluation is first-match-wins. If no rule matches a cross-owner request, the message is denied.

Configuring ACL Rules

Rules and groups are defined in fabrcore.json under the Acl section. The following example opens all system-owned agents for messaging and reading by anyone, and restricts shared-owned analytics agents to the premium group:

JSON — fabrcore.json ACL Configuration
{
  "Acl": {
    "Rules": [
      {
        "OwnerPattern": "system",
        "AgentPattern": "*",
        "CallerPattern": "*",
        "Permission": "Message,Read"
      },
      {
        "OwnerPattern": "shared",
        "AgentPattern": "analytics_*",
        "CallerPattern": "group:premium",
        "Permission": "Message,Read"
      }
    ],
    "Groups": {
      "admins": ["alice", "bob"],
      "premium": ["alice", "charlie"]
    }
  }
}

If no rules are configured at all, FabrCore seeds a default rule: system:* -> * -> Message,Read. This means system-owned agents are always reachable out of the box, but every other cross-owner path is locked down until you explicitly open it.

Rules can also be managed at runtime through the IAclProvider interface. The built-in InMemoryAclProvider loads rules from configuration on startup and supports runtime additions — though changes are not persisted across restarts:

C# — Runtime Rule Management
var aclProvider = serviceProvider.GetRequiredService<IAclProvider>();

// Grant premium group access to premium agents
await aclProvider.AddRuleAsync(new AclRule
{
    OwnerPattern = "system",
    AgentPattern = "premium_*",
    CallerPattern = "group:premium",
    Permission = AclPermission.Message | AclPermission.Read
});

// Add a user to the premium group
await aclProvider.AddToGroupAsync("premium", "newuser123");

Custom Providers and Enforcement

For production systems that need durable rules, implement the IAclProvider interface and back it with a database. The interface covers rule evaluation, rule management, and group membership:

C# — IAclProvider Interface
public interface IAclProvider
{
    Task<AclEvaluationResult> EvaluateAsync(
        string callerOwner, string targetOwner,
        string agentAlias, AclPermission required);

    Task<List<AclRule>> GetRulesAsync();
    Task AddRuleAsync(AclRule rule);
    Task RemoveRuleAsync(AclRule rule);

    Task<Dictionary<string, HashSet<string>>> GetGroupsAsync();
    Task AddToGroupAsync(string groupName, string member);
    Task RemoveFromGroupAsync(string groupName, string member);
}

Register it in your server configuration:

C# — Registering a Custom ACL Provider
builder.AddFabrCoreServer(options =>
{
    options.UseAclProvider<SqlAclProvider>();
});

ACL enforcement happens at the ClientGrain level. Both SendMessage and SendAndReceiveMessage require the Message permission, while CreateAgent requires Configure for cross-owner operations. Importantly, agent-to-agent communication within the cluster is trusted and bypasses ACL entirely — only client-initiated requests are gated.

ChatDock integrates seamlessly with the ACL system. When ChatDock builds a message, it uses HandleUtilities.EnsurePrefix to construct the fully-qualified FromHandle. If a user attempts to message an agent they do not have permission for, the request is denied at the ClientGrain before it ever reaches the target agent.


Built with FabrCore on .NET 10.


Eric Brasher

Builder of FabrCore and OpenCaddis.