Multi-Tenant AI Agents — FabrCore's New ACL Permissioning System
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.
| Scenario | ACL Check? | Result |
|---|---|---|
user1 sends to user1:assistant | No | Allowed (own agent) |
user1 sends to user2:assistant | Yes | Depends on rules |
user1 sends to system:shared-agent | Yes | Depends 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.
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:
[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:
| Pattern | Matches | Example |
|---|---|---|
"*" | Anything | All 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:
{
"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:
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:
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:
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.
Builder of FabrCore and OpenCaddis.