Summary
Add optional per-sender role-based access control so a single ZeroClaw instance can serve multiple user classes (customers, operators, developers) with isolated workspaces, tool sets, rate limits, and system prompts.
Problem statement
ZeroClaw currently runs with a single security policy for all users. When deploying an agent accessible via channels (Telegram, Discord, etc.), every sender gets the same tools, model, rate limits, and workspace. This makes multi-tenant deployments impractical:
- A customer-facing sales agent should have read-only product tools and a friendly prompt, but should not access shell, file writes, or internal docs.
- An operator/owner should see all conversations, manage the system, and use a more capable model.
- A developer should have full access with debug tools.
Today, achieving this requires running separate ZeroClaw instances per role, which multiplies infrastructure cost and prevents cross-role features like deferred task delivery.
The existing security scaffolding (IamPolicy, NevisIdentity, WorkspaceManager) provides types and tests but is not yet wired into the agent loop, channel processing, or tool execution. This proposal fills that gap with a lightweight, config-driven approach that can bridge to the existing security types when they are connected.
Proposed solution
A new optional crate zeroclaw-rbac (~120 lines) plus minimal integration points:
Core concepts:
- RoleResolver — maps channel sender ID to a named role via config assignments
- RoleDefinition — per-role: autonomy level, allowed_tools, workspace, model_hint, rag_access, rate_limit_per_minute, cost_daily_limit_usd, memory_isolation
- WorkspaceSelector — maps role to workspace directory + per-sender memory DB path (SHA256-based isolation when
memory_isolation = true)
- Each workspace has its own
AGENTS.md serving as the system prompt for that role
Enforcement via decorator pattern (no upstream signature changes):
RateLimitedProvider wraps Box<dyn Provider> — intercepts chat() for per-role LLM rate limiting
RateLimitedTool wraps Box<dyn Tool> — intercepts execute() for per-role action rate limiting
- Tool filtering applied before
run_tool_call_loop — only allowed tools passed in
- Multimodal path canonicalization applied before
run_tool_call_loop
HookRunner::with_role_context() embeds role info without changing hook signatures
Integration pattern:
// In RBAC branch (channel orchestrator, CLI loop, dispatch executor):
if let Some(ref rbac) = config.rbac {
let resolver = RoleResolver::from_config(rbac);
let selector = WorkspaceSelector::new(&base_dir, rbac);
let role_ctx = resolver.resolve(&sender_id);
let workspace_dir = selector.workspace_dir(&role_ctx);
let memory_db = selector.memory_db_path(&role_ctx, &sender_id);
let provider = RateLimitedProvider::wrap(provider, &role_ctx);
let tools = filter_and_wrap_tools(tools, &role_ctx);
let hooks = hooks.with_role_context(role_ctx.clone());
// ... call run_tool_call_loop with upstream-identical signature
}
Config example (inside existing config.toml):
[rbac]
default_role = "buyer"
[rbac.roles.buyer]
autonomy = "readonly"
allowed_tools = ["product_search", "product_info", "web_search"]
workspace = "shared"
memory_isolation = true
rag_access = ["public"]
rate_limit_per_minute = 10
[rbac.roles.owner]
autonomy = "supervised"
allowed_tools = ["*"]
workspace = "owner"
memory_isolation = true
rag_access = ["public", "internal"]
[rbac.roles.developer]
autonomy = "full"
allowed_tools = ["*"]
workspace = "dev"
memory_isolation = false
rag_access = ["*"]
[rbac.workspaces.shared]
path = "shared"
[rbac.workspaces.owner]
path = "owner"
[rbac.workspaces.dev]
path = "dev"
[[rbac.assignments]]
sender = "${OWNER_TELEGRAM_ID}"
role = "owner"
[[rbac.assignments]]
sender = "${DEV_TELEGRAM_ID}"
role = "developer"
Unmatched senders fall back to default_role.
Key design decision: All RBAC code lives in if let Some(rbac) branches. The non-RBAC path is identical to upstream. The decorator pattern means run_tool_call_loop, execute_one_tool, and other upstream function signatures remain unchanged.
Non-goals / out of scope
- Replacing or modifying existing
IamPolicy/NevisIdentity/WorkspaceManager types
- OAuth2/JWT authentication (Nevis SSO) — orthogonal, can coexist
- UI/dashboard for role management
- Cross-instance federation
- Agent output verification/quality checks (future work)
Alternatives considered
-
Extend upstream WorkspaceProfile with RBAC fields — rejected because WorkspaceProfile is filesystem-driven (profile.toml per workspace) while RBAC needs a centralized config mapping senders to roles. Adding sender-mapping to WorkspaceProfile would change its semantics.
-
Use IamPolicy with synthetic NevisIdentity — rejected because IamPolicy expects OAuth2 identity objects. Constructing fake NevisIdentity from channel sender IDs is a hack that misrepresents the auth model.
-
Multiple ZeroClaw instances — rejected because it prevents cross-role features (dispatch), multiplies infra cost, and can't share config.
Acceptance criteria
- Single config file defines roles, tool sets, rate limits, workspaces
- Channel messages are resolved to roles by sender ID
- Each role gets isolated workspace directory and memory DB
- Per-workspace
AGENTS.md serves as system prompt for the role
- Tool filtering enforced before agent loop
- LLM and tool rate limits enforced per role via decorators
- Non-RBAC deployments are unaffected (feature is opt-in via
[rbac] section)
run_tool_call_loop signature unchanged from upstream
- All enforcement testable in isolation (decorator unit tests)
Architecture impact
- New crate:
zeroclaw-rbac (leaf dependency, ~120 lines)
- New types in
zeroclaw-api: RbacConfig, RoleDefinition, RoleContext, SenderRoleAssignment, WorkspaceDefinition
- Integration:
zeroclaw-runtime/agent/loop_.rs, zeroclaw-channels/orchestrator/mod.rs (additive if let branches)
- Config: one
Option<RbacConfig> field in Config struct
Risk and rollback
Risk: RBAC branches in agent loop and channel orchestrator could affect non-RBAC paths if not properly isolated.
Rollback: Remove [rbac] section from config — all RBAC branches become no-ops. Or revert the crate addition (single workspace member line).
Breaking change?
No
Relationship to existing security modules
This proposal is designed to be forward-compatible with the existing security scaffolding:
- When
IamPolicy is wired into the runtime, RoleResolver can accept NevisIdentity as an additional identity source alongside channel sender IDs
- When
WorkspaceManager is connected to the agent loop, WorkspaceSelector can delegate to it instead of doing its own directory resolution
- The decorator pattern (
RateLimitedProvider, RateLimitedTool) is independent of identity source — it only needs a role name and limits
Implementation plan
This RFC covers the overall design. Implementation will be split into separate PRs:
- RBAC types in
zeroclaw-api + zeroclaw-rbac crate
- Decorator-based enforcement:
RateLimitedProvider, RateLimitedTool
- Integration points in agent loop and channel orchestrator
- Dispatch system for deferred agent-to-agent tasks (complements existing cron scheduler)
Summary
Add optional per-sender role-based access control so a single ZeroClaw instance can serve multiple user classes (customers, operators, developers) with isolated workspaces, tool sets, rate limits, and system prompts.
Problem statement
ZeroClaw currently runs with a single security policy for all users. When deploying an agent accessible via channels (Telegram, Discord, etc.), every sender gets the same tools, model, rate limits, and workspace. This makes multi-tenant deployments impractical:
Today, achieving this requires running separate ZeroClaw instances per role, which multiplies infrastructure cost and prevents cross-role features like deferred task delivery.
The existing security scaffolding (
IamPolicy,NevisIdentity,WorkspaceManager) provides types and tests but is not yet wired into the agent loop, channel processing, or tool execution. This proposal fills that gap with a lightweight, config-driven approach that can bridge to the existing security types when they are connected.Proposed solution
A new optional crate
zeroclaw-rbac(~120 lines) plus minimal integration points:Core concepts:
memory_isolation = true)AGENTS.mdserving as the system prompt for that roleEnforcement via decorator pattern (no upstream signature changes):
RateLimitedProviderwrapsBox<dyn Provider>— interceptschat()for per-role LLM rate limitingRateLimitedToolwrapsBox<dyn Tool>— interceptsexecute()for per-role action rate limitingrun_tool_call_loop— only allowed tools passed inrun_tool_call_loopHookRunner::with_role_context()embeds role info without changing hook signaturesIntegration pattern:
Config example (inside existing config.toml):
Unmatched senders fall back to
default_role.Key design decision: All RBAC code lives in
if let Some(rbac)branches. The non-RBAC path is identical to upstream. The decorator pattern meansrun_tool_call_loop,execute_one_tool, and other upstream function signatures remain unchanged.Non-goals / out of scope
IamPolicy/NevisIdentity/WorkspaceManagertypesAlternatives considered
Extend upstream
WorkspaceProfilewith RBAC fields — rejected because WorkspaceProfile is filesystem-driven (profile.tomlper workspace) while RBAC needs a centralized config mapping senders to roles. Adding sender-mapping to WorkspaceProfile would change its semantics.Use
IamPolicywith syntheticNevisIdentity— rejected because IamPolicy expects OAuth2 identity objects. Constructing fake NevisIdentity from channel sender IDs is a hack that misrepresents the auth model.Multiple ZeroClaw instances — rejected because it prevents cross-role features (dispatch), multiplies infra cost, and can't share config.
Acceptance criteria
AGENTS.mdserves as system prompt for the role[rbac]section)run_tool_call_loopsignature unchanged from upstreamArchitecture impact
zeroclaw-rbac(leaf dependency, ~120 lines)zeroclaw-api:RbacConfig,RoleDefinition,RoleContext,SenderRoleAssignment,WorkspaceDefinitionzeroclaw-runtime/agent/loop_.rs,zeroclaw-channels/orchestrator/mod.rs(additiveif letbranches)Option<RbacConfig>field inConfigstructRisk and rollback
Risk: RBAC branches in agent loop and channel orchestrator could affect non-RBAC paths if not properly isolated.
Rollback: Remove
[rbac]section from config — all RBAC branches become no-ops. Or revert the crate addition (single workspace member line).Breaking change?
No
Relationship to existing security modules
This proposal is designed to be forward-compatible with the existing security scaffolding:
IamPolicyis wired into the runtime,RoleResolvercan acceptNevisIdentityas an additional identity source alongside channel sender IDsWorkspaceManageris connected to the agent loop,WorkspaceSelectorcan delegate to it instead of doing its own directory resolutionRateLimitedProvider,RateLimitedTool) is independent of identity source — it only needs a role name and limitsImplementation plan
This RFC covers the overall design. Implementation will be split into separate PRs:
zeroclaw-api+zeroclaw-rbaccrateRateLimitedProvider,RateLimitedTool