Skip to content

Access Control

FabrCore 2.0 · Release and package availability

These guides track the current 2.0 source. Stable 2.0.0 publication is pending; package commands show the release target. Until it is published, follow the source quick start or use a matching available prerelease set. Release migration · Runtime modes

2.0 mode boundary: relational ACL is available in SQL mode. Standalone is a trusted workspace and bypasses cross-principal ACL.

Access Control (ACL)

FabrCore's access-control platform governs everything that crosses a principal boundary: who may message whose agents, who may create agents (and for whom), who may read monitor data, and who may administer the policy itself. It is built on four entities — principals, roles, groups, and permission grants — evaluated by a synchronous, snapshot-backed engine that adds no I/O to the message hot path, and observed end-to-end by a pluggable security audit provider.

SQL mode: the registry initializes built-in principals, groups and grants. Manage application ACL through the API. Standalone does not initialize or enforce relational ACL.

The Model

Every decision answers one question: may this subject perform this action on this resource?

EntityWhat it is
AclPrincipalAn identity (user, tenant, service, system). The handle is the same string that keys PrincipalGrain. Only needs an ACL record when it carries direct role assignments.
AclRoleA named set of permission grants. Assign to principals directly or to every member of a group.
AclGroupA named set of members (principals and/or agents). Groups may also carry roles inherited by all members. Two built-in dynamic groups have computed membership: all-principals and all-agents.
PermissionGrantTies a subject + permission + resource scope together, with an allow or deny effect.

Permission Notation

Permissions are three lowercase dot-separated segments: entity.behavior.effect. The effect is always allow or deny — and deny overrides allow for evaluated subjects. Disabled enforcement and the system principal bypass evaluation.

PermissionGoverns
agent.message.allow / .denySending messages and events to an agent (principal-initiated and agent-to-agent)
agent.create.allow / .denyCreating agents — the resource's principal segment expresses for whom
agent.reconfigure.allow / .denyResetting/reconfiguring an existing agent
agent.destroy.allow / .denyUntracking/evicting an agent
agent.read.allow / .denyReading monitor data (messages, events, LLM calls) for an agent
acl.manage.allow / .denyAdministering ACL entities via the management API
acl.read.allow / .denyReading ACL entities and running evaluate/check queries

The vocabulary is open: applications define their own permissions in non-reserved entity space (e.g. surface.adminview.allow) — see application-defined permissions. Reserved entities: agent, principal, acl, system, fabrcore.

Resource Patterns

Grant resources match a full "principal:agent" handle:

PatternMatches
p2:agent3Exactly that agent
p2:*All agents of principal p2
*:agent5The agent named agent5 under any principal
*:*Everything (also the convention for resource-less app permissions)
tenant-*:*Prefix wildcard, per segment
group:tenants:*Agents of any principal in group tenants (group refs valid on the principal segment)

Cross-Talk Grants

The three canonical shapes for letting agents talk across principal boundaries:

JSON — PermissionGrant shapes
// P1's agent1 may message exactly P2's agent3
{ "Subject": { "Kind": "Agent", "Selector": "p1:agent1" },
  "Permission": "agent.message.allow", "Resource": "p2:agent3" }

// P1 (any of its agents) may message all of P2's agents
{ "Subject": { "Kind": "Principal", "Selector": "p1" },
  "Permission": "agent.message.allow", "Resource": "p2:*" }

// P1 may message any principal's agent named agent5
{ "Subject": { "Kind": "Principal", "Selector": "p1" },
  "Permission": "agent.message.allow", "Resource": "*:agent5" }

// Ban a principal from creating agents entirely (deny overrides ordinary allows)
{ "Subject": { "Kind": "Principal", "Selector": "p1" },
  "Permission": "agent.create.deny", "Resource": "*:*" }

Evaluation Order

  1. Disabled mode — everything allowed (no evaluation)
  2. System principal — the built-in System identity bypasses all checks
  3. Explicit deny — any matching .deny grant denies (deny overrides allow)
  4. Same principal — subject and target share a principal → implicitly allowed
  5. Explicit allow — any matching .allow grant allows
  6. Default deny — no match → denied
Deny sits above the implicit self-allow on purpose.

That's how agent.create.deny on *:* disables agent creation for a subject entirely — even under its own principal. SQL bootstrap has no user-defined deny grants.

Subject matching resolves the caller's full identity set per evaluation: the principal, the acting agent handle (agent-to-agent only), stored group memberships, the dynamic groups, and effective roles (direct + via groups). Evaluation is synchronous against an immutable in-memory snapshot — no I/O, no locks, no grain calls on the message hot path.

Enforcement

BoundaryPermissionWhere
Principal → agent messaging & eventsagent.messagePrincipalGrain (WebSocket and grain clients)
Agent → agent, cross-principalagent.messageAgentGrain — checked sender-side on every cross-principal send
Agent creation / reset / untrackagent.create / agent.reconfigure / agent.destroyPrincipalGrain
Monitor data readsagent.readMonitor REST + SSE endpoints (filtering)
ACL administrationacl.manage / acl.readACL & Audit REST endpoints

Enforcement identity always derives from the sending grain's own key — FromHandle is spoofable routing metadata and is never trusted for authorization. Unauthorized operations throw AclDeniedException (an UnauthorizedAccessException). Response-kind and system messages (_status/_error) are exempt from the agent-to-agent check so authorized request/reply round-trips can't be broken.

Enforcement Modes

ModeBehavior
Enforce (default)Denials block the operation and are audited
AuditOnlyWould-be denials log a warning and an audit event, then proceed — ideal while rolling policy out. Monitor read filtering still filters.
DisabledNo evaluation; everything allowed

Set via FabrCore:Acl:Mode in host configuration, or change at runtime through the management API (the override persists and wins over configuration).

Cross-Principal Fan-Out & the Breadcrumb

Granting P1 access to P2's agent means that agent may, while handling P1's request, contact other agents P1 was never granted. Those transitive hops run as P2 and are not blocked — they are made visible instead:

  • The first cross-principal send stamps AgentMessage.CrossPrincipalOrigin (the originating principal) and increments CrossPrincipalHops.
  • The receiving host propagates the breadcrumb automatically into messages the agent composes while processing — agent authors do nothing.
  • If a tagged chain crosses a second principal boundary, the host logs a warning and emits a BoundaryCrossing audit event naming origin, sender, target, and hop count.
Warn, don't break.

The first hop is permissioned; downstream fan-out is the target principal's business, surfaced through the audit trail rather than blocked mid-flight. Watch the BoundaryCrossing audit category to see exactly where cross-principal chains travel.

Built-Ins: System Principal & Dynamic Groups

  • System principal — unrestricted; bypasses every check. Its handle is set in appsettings.json (FabrCore:Acl:SystemPrincipal, default "system"). Treat the handle like a root credential. It cannot be deleted.
  • all-principals — dynamic group containing every principal, stored nowhere, computed at evaluation time. The right subject for "everyone may…" grants.
  • all-agents — dynamic group containing every agent (matches agent subjects only).
  • acl-admin — built-in role granting acl.manage.allow + acl.read.allow; assigned to System at bootstrap; protected from deletion.

Dynamic group names are configurable (AllPrincipalsGroupId, AllAgentsGroupId). Membership edits on dynamic groups are rejected — it's computed.

SQL configuration and ACL migration

Enable SQL mode with ConnectionStrings:FabrCore. ACL entities and their version live in relational acl tables, independent of Orleans storage. The registry serializes transactional writes; each silo evaluates immutable cached snapshots with notifications and TTL refresh.

Configure policy under FabrCore:Acl: Mode, SystemPrincipal, CacheTtlSeconds and SeedDefaultSystemAgentAccess. SQL startup initializes built-in entities and configured System-agent grants. Manage user principals, roles, groups and grants through the API.

Normal startup rejects FabrCore:Acl:Seed, legacy Acl:Seed and FabrCore:Acl:Rules. Export the old ACL before upgrading and import into a built-in-only SQL target. The import preserves IDs, validates references and refuses to overwrite existing user-defined data. See the migration guide and utility.

Without a feature database, standalone bypasses cross-principal ACL and ACL administration is unavailable. Authentication and storage/session ownership checks remain. The grant/evaluation rules on this page describe SQL mode.

Management API & SDK

Full CRUD over fabrcoreapi/acl/* — principals, roles, groups (with member endpoints), grants — plus evaluate (dry-run with the deciding grant), check (simplified boolean), effective-membership queries, and runtime enforcement-mode control. Audit queries live at fabrcoreapi/audit/*. The host consumes the x-user-handle header as caller identity. Your authenticated boundary must derive it from trusted identity and reject or replace caller-supplied values; the header is not authentication. mutations require acl.manage.allow, reads require acl.read.allow. Every management call is itself audited.

C# — SDK (IFabrCoreHostApiClient)
// Grant cross-talk (as System or an acl-admin)
await client.UpsertAclGrantAsync("system", new PermissionGrant
{
    Subject = new AclSubject(SubjectKind.Agent, "p1:agent1"),
    Permission = FabrPermissions.AgentMessageAllow,
    Resource = "p2:agent3"
});

// Dry-run a decision
var result = await client.EvaluateAclAsync("system", new AclEvaluationRequest
{
    SubjectPrincipal = "p1", SubjectAgent = "p1:agent1",
    Action = "agent.message", Resource = "p2:agent3"
});
// result.Allowed, result.Outcome, result.Reason, result.DecidingGrant

Building an admin UI? The repo ships a dedicated guide covering every endpoint, screen, and pitfall: docs/acl-management.md.

Application-Defined Permissions

Consuming applications run their own authorization on the same platform. Define permissions in your own entity space, create app-prefixed roles/groups via the same API, and gate features with one call:

C# — Addon feature gating
// Setup once: a role carrying an app permission
await client.UpsertAclRoleAsync("system", new AclRole
{
    Name = "surface:admin",
    Grants = { new PermissionGrant { Permission = "surface.adminview.allow", Resource = "*:*" } }
});

// At runtime: does alice get the admin view?
var check = await client.CheckPermissionAsync("surface-svc", "alice", "surface.adminview");
if (check.Allowed) { /* render admin view */ }

Role-membership checks (IsPrincipalInRoleAsync) and effective-role queries (GetPrincipalRolesAsync) are also available. Grant the addon's service principal acl.read.allow once at setup.

Security Audit

A pluggable IAuditProvider records ACL decisions, management changes, agent-creation checks, boundary crossings, and bootstrap events. SQL mode selects SqlAuditProvider in fabrOps; standalone uses a bounded in-memory buffer. Custom providers remain supported. SQL write failures are logged/counted without a retry spool or in-memory fallback; retention is explicit.

CategoryRecordsDefault level
AclDecisionMessage/read authorization outcomesFailures
AgentCreationAgent-creation checksFailures
AclManagementEvery management API call (who changed what)All
BoundaryCrossingCross-principal chain movement and fan-outAll
BootstrapRegistry bootstrap activityAll

Levels are None / Failures / All, configurable globally and per category under FabrCore:Audit. Events carry the caller, resource, permission, enforcement mode, whether the denial was actually enforced (distinguishing Enforce from AuditOnly), a reason including the deciding grant, and the W3C TraceId for joining OpenTelemetry traces and monitor data. When verifiable execution is enabled, events also link to the signed evidence trail.

Compliance note: standalone audit is lost on restart. SQL audit survives restart but has no automatic retention job or retry spool. Configure retention and failure monitoring for application requirements.

Custom Providers

C# — Provider registration
builder.AddFabrCoreServer(new FabrCoreServerOptions
{
    AdditionalAssemblies = [typeof(MyAgent).Assembly]
}
.UseAclEvaluator<MyAclEvaluator>()          // custom decision engine
.UseAuditProvider<MySiemAuditProvider>());  // durable audit sink
Hot-path contract: custom IAclEvaluator implementations must stay synchronous and snapshot-backed — never perform I/O or grain calls inside Evaluate; it runs inside grain turns on every message.

Best Practices

  • Groups, roles, and wildcards first; individual grants last. Policy entity counts should track policy concepts, not population — "everyone may message system agents" is one grant on all-principals, not thousands of per-principal grants.
  • Reserve deny for hard policy statements ("this subject must never do X"); removing an allow is the normal revocation path.
  • Use AuditOnly to roll policy out against live traffic, watch the would-be denials in the audit feed, then flip to Enforce.
  • Protect the System handle like a root credential — authentication in front of the API is the hosting layer's job.

Complete remote ACL management

Use the 2.0 administration principal directory to combine registered and runtime-discovered principals without dropping inactive registrations. Paged collection and individual APIs cover principals, roles, groups, grants and membership.

Conditional writes send the numeric registry version from /access/metadata in If-Match; it is checked inside the serialized mutation. Protected built-ins, effective grant origins and enforcement reset are supported. The principal directory has its own revision hash. Cached message authorization remains local to the cluster; existing audit functionality is unchanged.