Skip to content

Multi User Collaboration

scarecr0w12 edited this page Jun 24, 2026 · 1 revision

Multi-User Collaboration

CortexPrism v0.53.0 introduced multi-user collaboration with users, teams, API tokens, and resource scoping across the entire platform. Every agent, session, service, node, channel, and workspace config is now owned by a user and optionally scoped to a team.

Architecture

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│    Users     │    │    Teams     │    │  API Tokens  │
│ PBKDF2 hash  │◄──►│ join policies│◄──►│ SHA-256 hash │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       ▼                   ▼                   ▼
┌─────────────────────────────────────────────────────┐
│              RequestIdentity System                  │
│  extractIdentity() → user / instance / anonymous     │
│  userId · username · teamIds · currentTeamId         │
│  isInstanceAdmin · sessionId                         │
└─────────────────────┬───────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────┐
│              Authorization Guards                    │
│  requireInstanceAdmin · requireTeamAdmin             │
│  requireTeamMember · requireResourceOwner            │
└─────────────────────────────────────────────────────┘

Users

Users are stored in the users table (migration 044) with PBKDF2 password hashing. Each user has:

Field Description
id usr_<uuid> — unique user identifier
username Unique login name
password_hash PBKDF2-SHA-256 derived key (200,000 iterations)
password_salt 16-byte random salt
display_name Optional human-readable name
email Optional email address
disabled_at Timestamp if user is disabled (soft-delete)

Password Hashing

Passwords are hashed using PBKDF2 with SHA-256, 200,000 iterations, and a 16-byte random salt. Verification uses constant-time comparison to prevent timing attacks. Password complexity requires at least 8 characters with at least 2 of: lowercase, uppercase, numbers, symbols.

Instance Admin

The first user created during initial setup is automatically assigned as the instance admin. Instance admins are tracked in the config table under the key instance_admins as a JSON array of user IDs. Instance admins bypass all resource ownership and team membership checks.

Web UI

The Users page (src/server/ui/js/28_users.ts) provides:

  • User list with create/disable/enable actions (instance admin only)
  • Username, email, and disabled status display

CLI

cortex users list              # List all users (instance admin)
cortex users create <username> <password>  # Create a user (instance admin)
cortex users disable <userId>  # Disable a user (instance admin)
cortex users enable <userId>   # Re-enable a disabled user (instance admin)

Teams

Teams are stored in the teams table with join policies. The team_memberships join table assigns users to teams with admin or member roles.

Table Purpose
teams Team identity: id, name, join_policy, created_at
team_memberships User-team assignments: user_id, team_id, role (admin/member)

Team Management API

Method Path Description
GET /api/teams List teams (filtered to user's teams)
POST /api/teams Create a team (instance admin)
GET /api/teams/:id Get team details
PATCH /api/teams/:id Update team (team admin or instance admin)
DELETE /api/teams/:id Delete team (instance admin)
GET /api/teams/:id/members List team members
POST /api/teams/:id/members Add member to team
PATCH /api/teams/:id/members Update member role
DELETE /api/teams/:id/members Remove member from team
GET /api/teams/:id/agents List team-scoped agents
POST /api/teams/:id/agents Create agent scoped to team

CLI

cortex teams list             # List your teams
cortex teams create <name>    # Create a team (instance admin)

Resource Sharing

Users can share resources (agents, sessions, services, etc.) with other users via the resource_shares table (migration 047).

API

Method Path Description
POST /api/shares Share a resource with another user
GET /api/shares/given List shares you've created
GET /api/shares/received List shares shared with you
DELETE /api/shares/:id Revoke a share

Ownership validation is enforced — only the resource owner can create a share.

RequestIdentity System

Every API request is assigned a RequestIdentity via extractIdentity() in src/server/auth.ts:

interface RequestIdentity {
  type: 'user' | 'instance' | 'anonymous';
  userId?: string;           // User ID (when type === 'user')
  username?: string;         // Username
  teamIds?: string[];        // Teams the user belongs to
  currentTeamId?: string;    // Active team (from x-cortex-team header)
  sessionId?: string;        // Session ID (when authenticated via session cookie)
  isInstanceAdmin?: boolean; // Whether the user is an instance admin
}

Identity is extracted from either:

  1. Authorization header (Bearer <token>) — API token or node token
  2. Session cookie (cortex_session) — web UI session

Authorization Guards

Four coarse-grained authorization guards in src/server/guards.ts enforce access control:

Guard Requirements
requireInstanceAdmin(identity) identity.isInstanceAdmin === true
requireTeamAdmin(identity, teamId) User is a member of the team with admin role (instance admins bypass)
requireTeamMember(identity, teamId) User is a member of the team (instance admins bypass)
requireResourceOwner(identity, resourceType, resourceId) User owns the resource (instance admins bypass)

Guards return null if authorization passes, or a Response with the appropriate error (401 Unauthorized or 403 Forbidden) if it fails.

Resource Ownership

requireResourceOwner maps resource types to database tables:

Resource Type Table
agent agents
session sessions
service services
node nodes
channel channels

Agent Scoping

Agents are scoped in a three-layer hierarchy:

User → Team → Instance
  • User-scoped: Agent owned by a specific user, visible only to that user
  • Team-scoped: Agent associated with a team, visible to all team members (validates team membership before creation)
  • Instance-scoped: Global agent visible to all users on this instance

listAgents() in src/agent/manager.ts accepts optional userId and teamIds for scope-aware filtering. Built-in agents are seeded into the agents table as instance-scoped.

Agent Storage

In v0.53.0, agent storage moved from config.json to the agents database table (migration 044):

Field Description
id Unique agent identifier
name Agent name
kind builtin / custom
user_id Owner user (nullable for instance-scoped)
team_id Owning team (nullable for user/instance-scoped)
scope user / team / instance
config_json Full agent configuration as JSON

Legacy agents in config.json are preserved as fallback during the transition.

See Also

Clone this wiki locally