Control who can use your agents. Access is enforced via owner vs. non-owner distinction; role labels are stored for future enforcement.
GoClaw's permission system ensures agents stay in the right hands. The core concept:
- Owner owns the agent (full control, can delete, share)
- Default agents are readable by all users (good for shared utilities)
- Shares grant others access with a stored role label
Access is checked in a 4-step pipeline: Does the agent exist? → Is it default? → Are you the owner? → Is it shared with you?
When you share an agent, a record is created in the agent_shares table:
CREATE TABLE agent_shares (
id UUID PRIMARY KEY,
agent_id UUID NOT NULL REFERENCES agents(id),
user_id VARCHAR NOT NULL,
role VARCHAR NOT NULL, -- stored label: "admin", "operator", "viewer", "user", etc.
granted_by VARCHAR NOT NULL, -- who granted this share
created_at TIMESTAMP NOT NULL
);Each row represents one user's access to one agent.
Important: Role labels are stored in
agent_sharesbut not currently enforced at runtime. The only distinction enforced today is owner vs. non-owner. Role-based permission checks are planned for a future release.
| Role | Planned Permissions | Status |
|---|---|---|
| admin | Full control: read, write, delete, reshare, manage team | Planned |
| operator | Read + write: run agent, edit context files, but NOT delete/reshare | Planned |
| viewer | Read-only: run agent, view files, but NOT edit | Planned |
| user | Basic access (default when no role specified) | Stored only |
What IS enforced today:
- Owner can share, revoke, and list shares; non-owners cannot
- Any user with a share row can access the agent (regardless of role value)
- Default agents (
is_default = true) are accessible by everyone
What is NOT enforced today:
- Role-based write/delete restrictions for shared users
- Preventing "viewer" role holders from editing
- "admin" role does not grant resharing ability
When sharing without specifying a role, the default is "user":
POST /v1/agents/:id/shares
{ "user_id": "alice@example.com" }
→ role stored as "user"
When you try to access an agent, GoClaw checks in this order:
1. Does the agent exist?
→ No: access denied
2. Is it marked is_default = true?
→ Yes (and exists): allow (you get "user" role)
→ No: proceed to step 3
3. Are you the owner (owner_id = your_id)?
→ Yes: allow (you get "owner" role)
→ No: proceed to step 4
4. Is there an agent_shares row for (agent_id, your_id)?
→ Yes: allow (you get the role stored in that row)
→ No: access denied
Result: Each access check returns (allowed: bool, role: string). The role string is returned but downstream handlers currently do not restrict behavior based on it.
Predefined agents can also be accessible through channel_instances. If a predefined agent has an enabled channel instance whose allow_from list includes your user ID, you can access that agent even without a direct share or default flag.
Use POST /v1/agents/:id/shares to share an agent. Only the owner (or a gateway owner-level user) can share.
Request:
POST /v1/agents/550e8400-e29b-41d4-a716-446655440000/shares
Content-Type: application/json
Authorization: Bearer <token>
{
"user_id": "alice@example.com",
"role": "operator"
}Response (201 Created):
{ "ok": "true" }If role is omitted, it defaults to "user".
Use DELETE /v1/agents/:id/shares/:userID to remove a share immediately.
Request:
DELETE /v1/agents/550e8400-e29b-41d4-a716-446655440000/shares/alice@example.com
Authorization: Bearer <token>Response (200 OK):
{ "ok": "true" }Use GET /v1/agents/:id/shares to see who has access. Only the owner can list shares.
Response:
{
"shares": [
{ "id": "...", "agent_id": "...", "user_id": "alice@example.com", "role": "operator", "granted_by": "owner@example.com", "created_at": "..." },
{ "id": "...", "agent_id": "...", "user_id": "bob@example.com", "role": "viewer", "granted_by": "owner@example.com", "created_at": "..." }
]
}Go store method:
shares, err := agentStore.ListShares(ctx, agentID)The Dashboard provides a UI for sharing:
- Open Agents → select your agent
- Click Sharing or Team tab
- Enter a user ID (email, Telegram handle, etc.)
- Select a role label (note: not enforced at runtime yet)
- Click Share
- To revoke: find the user in the list, click Remove
Changes take effect immediately.
- Owner creates
customer-summaryagent (default: not shared) - Owner shares with
alice— she gains access (role stored as "operator") - Alice accesses the agent and refines settings
- Owner marks agent default → all users can now use it
- Owner revokes alice's share (no longer needed)
- Owner creates
research-agent - Shares with team members — they can all access and run the agent
- Shares with manager as "viewer" — manager can access (role enforcement planned)
- Team iterates; owner controls sharing and deletion
- Owner creates
web-searchagent - Marks it default (no explicit shares needed)
- All users can use it; owner can still edit it
- If owner unmarks default, only owner can use it again
When a user loads their agent list, GoClaw returns only agents they can access:
agents, err := agentStore.ListAccessible(ctx, userID)
// Returns:
// - All agents owned by userID
// - All default agents
// - All agents explicitly shared with userID
// - Predefined agents accessible via channel_instancesThis powers the "My Agents" list in the Dashboard.
| Practice | Why |
|---|---|
| Share by explicit user ID | Clear audit trail of who has access |
| Revoke shares when no longer needed | Reduces clutter; tightens security |
| Use default sparingly | Good for utilities (web search, memory); bad for sensitive agents |
| Keep track of shares via ListShares | Especially for multi-team agents; prevents confusion |
| Problem | Solution |
|---|---|
| User can't see the agent | Check: (1) agent exists, (2) user has a share row, or (3) agent is default |
| Revoked but user still has access | Maybe the agent is default; unmark it first, then revoke |
| Forgot who has access | Use GET /v1/agents/:id/shares or Dashboard → Sharing tab to audit |
| Role restrictions not working | Role-based enforcement is planned, not yet implemented — all shared users have equal access today |
GoClaw caches hot permission lookups in memory to reduce database pressure on high-traffic deployments. The PermissionCache (in internal/cache/permission_cache.go) maintains three short-TTL caches:
| Cache | Key | TTL |
|---|---|---|
| Tenant role | tenantID:userID |
30 seconds |
| Agent access | agentID:userID |
30 seconds |
| Team access | teamID:userID |
30 seconds |
The cache is invalidated via pubsub events:
CacheKindTenantUsers— clears all tenant role entries (user-level change)CacheKindAgentAccess— deletes all entries for the changed agent (prefix match onagentID:)CacheKindTeamAccess— deletes all entries for the changed team (prefix match onteamID:)
Session IDOR fix: Prior to v3, a session could retain stale access after a share was revoked within the same 30-second window. The pubsub invalidation path now ensures revocations are reflected immediately across all running sessions.