Access Control (Groups & RBAC)
Who can do what, resolved on every request
MUXI controls access with three pieces that work together:
groups/- YAML files defining permission sets (which agents, triggers, SOPs, and MCP tools a group can reach).middleware:- the single source of group membership: an MCP server you supply that tags each inbound request with the caller's groups.rbac:- the switch that decides whether membership is enforced and what happens to a request with no groups.
Permissions resolve once per request (behind a membership + resolution cache)
and filter agent routing, direct agent addressing, workflow planning/execution,
trigger firing (403), SOP matching, and each agent's per-server MCP tool surface.
A denied agent behaves exactly like an unknown one - no information leak. A
formation with no groups/ directory is unaffected.
The pre-1.0 server.auth key and the user_groups membership table have been
removed. Membership now comes only from the middleware: server; user-level
gating is rbac.fallback: false plus a middleware that returns no groups for
unknown users. A formation still carrying server.auth fails to load.
Group files
A groups/ directory activates permission filtering. Each file is one group;
its id is the filename stem:
# groups/support.yaml
inherits: [base] # optional, cycle-detected
agents: # a plain list is an allow-list
- support-* # fnmatch globs supported
- faq
triggers:
- zendesk-*
sops:
- customer-onboarding
native_apps:
allow: ["support-console"]
deny: ["admin-*"]
memory:
write: # grants for writing shared memory scopes
- "formation"
Across a user's groups, allows are unioned and any deny wins.
Cascading tool overrides
One tools: {allow, deny} structure applies at four levels; the most specific
wins, and deny is applied after allow:
- MCP registry catalog (
mcp.servers[].tools) - Agent attachment (
{id, tools}on an agent'smcp_servers) - Group per-server
- Group per-agent-per-server
tools: {deny: "*"} hides a server from a group entirely.
# groups/support.yaml
mcp_servers:
web-search:
tools:
allow: ["search"]
deny: ["admin_*"]
Per-agent overrides live inside that agent's allow-list entry:
agents:
- support:
web-search:
tools:
allow: ["search", "read-page"]
The separate mcp: group key is reserved for the watch_job quota:
mcp:
watch:
max_concurrent: 4
allow / deny is the canonical vocabulary at every level; whitelist /
blacklist remain accepted aliases.
Membership middleware
Membership comes from a middleware: MCP server you run. It exposes exactly one
tool named middleware, receives the full request payload (user_id,
message, attachments, metadata, route_class - never groups inbound),
and returns the same-shaped payload, optionally attaching groups, rewriting
identity, or applying payload policy.
middleware:
command: python # stdio server
args: ["middleware.py"]
timeout: 5
# or an http server:
# url: https://rbac.internal/mcp
# headers: { Authorization: "Bearer ${{ secrets.RBAC_TOKEN }}" }
The pipeline runs after client-key auth, before any processing, on all
authenticated inbound traffic (chat, audiochat, triggers, memory routes) and on
internally-originated requests (heartbeat and scheduler jobs synthesize the same
payload with route_class: "heartbeat" / "scheduler"). It is fail-closed:
an error, timeout, or malformed response rejects the request with 403. There is
no runtime-side caching of the middleware result - timeout is the only knob.
A shipped one-file stdio template (contributing/templates/middleware.py in the
runtime repo) resolves groups from a static map and doubles as a starting point.
The rbac: switch
rbac:
active: auto # auto = on iff groups/ has files; true = force on; false = kill switch
fallback: false # reject no-group requests, or name a group whose permissions apply
active: truewithout anygroups/files fails the load.active: falseis a loud kill switch.fallback:must reference an existing group.- Dead config (
activeon +fallback: false+ nomiddleware) fails the load.
Shared memory grants
Writing formation- or group-scoped memory requires a
memory.write grant in the caller's group YAML (403 without one; globs
supported). Conversation-derived extraction is always user-scoped.
Learn More
- Multi-Tenancy - per-user isolation this builds on
- Memory System - memory scopes and shared-write grants
- Tools & MCP - the tool cascade in context