RBAC

The core idea

Humans and agents in Clip share one roles table. A role has permissions (what humans with this role can do in the UI) and skill bindings (what skills agents in this role get loaded with). One mental model, one place to edit.

This is the bit paperclip doesn't unify cleanly. The unified table means: promote a teammate from Engineer to CTO and they can summon CTO-class agents; pin a new skill on the CTO role and the agent named "CTO Bot" inherits it on its next run.

The default role pack

Every workspace gets six roles on creation:

Role Permissions
Owner everything, plus manage_runtimes, install_skills
Admin summon, approve, set budget, edit org, manage members, read audit, install skills
Manager summon, approve, read audit
Engineer summon (own work)
Reviewer read + comment
Viewer read only

Imported companies (from companies.sh) can override or add to this pack.

How permissions are checked

Server-side. Every consequential write goes through a lib/rbac resolver that joins members → roles → permissions. The Postgres function has_workspace_permission(ws, perm) does the same check at the row level so we get defense in depth via RLS.

For agents, "permissions" are about what tools they can invoke during a run. The MCP server set is composed at run start from the role's bindings plus the agent's own additions. Hooks loaded from the connected repo can clamp this further; see the Cursor SDK hooks docs.

Why this works

The traditional split — humans get RBAC, agents get a YAML file — produces drift. New skill, no one updated the role config, the human-side permission was never added. By making one row the source of both views, the only operation is "edit the role." Both sides change together.

Tips and tricks

What's optional

Per-resource ACLs. We don't have them in v1. If you need "this ticket is read-only for these members," compose it from the existing primitives (a private goal, a separate workspace, etc.) and tell us the use case so we can reconsider.