v5 Identity, Workspace, and RBAC Model
June 3, 2026 ยท View on GitHub
Status: planned for v5.0-alpha.1 Issue: #334
Purpose
Veritas Kanban v5 needs a multi-user identity and authorization model that works in three operating modes:
- local single-user desktop mode
- LAN or self-hosted server mode
- mobile or remote clients paired to a trusted server
The model must keep first-run local use simple while giving remote/server mode real user accounts, workspaces, memberships, scoped API keys, agent identities, auditable actor attribution, and route-level plus entity-level permissions.
Design Principles
- Local-first stays frictionless. A fresh local install seeds
localandlocal-user, and the owner can continue using password auth or an admin key. - Remote/server mode never relies on localhost bypass. Every request is tied to a human session, device session, service token, or agent token.
- Human sessions and agent API keys are separate principals. Agents cannot use browser session endpoints and humans cannot inherit agent tool scopes.
- Authorization is workspace-scoped by default. Cross-workspace reads and writes require explicit owner/admin permission.
- Every state change records an actor, auth method, workspace, and request id.
- Existing v4/v5 local auth continues to boot. Migration tightens defaults without invalidating local boards.
Identity Model
Actors
| Actor type | Principal id | Auth method | Typical use |
|---|---|---|---|
user | users.id | password session, future SSO subject, recovery flow | Browser, desktop, or mobile human user. |
device | device_sessions.id | paired device session | Desktop remote, mobile/PWA, browser, and CLI pairing. |
agent | agent_identities.id | scoped agent API token | OpenClaw, local Codex, workflow workers, MCP clients. |
service | api_tokens.id | scoped service API token | Dashboards, webhooks, import/export jobs. |
system | system | internal | Migrations, retention jobs, scheduled cleanup. |
localhost-bypass | localhost-bypass | development bypass only | Local development compatibility, never remote/server. |
req.auth should evolve from a role-only object into an actor context:
interface AuthContext {
actorType: 'user' | 'device' | 'agent' | 'service' | 'system' | 'localhost-bypass';
actorId: string;
displayName: string;
workspaceId: string;
role: WorkspaceRole;
permissions: Permission[];
authMethod: 'session' | 'api-key' | 'device-session' | 'recovery' | 'localhost-bypass' | 'system';
sessionId?: string;
tokenId?: string;
deviceSessionId?: string;
deviceId?: string;
clientId?: string;
clientMode?: string;
capabilities?: string[];
degradedReason?: string | null;
isLocalhost: boolean;
}
Core Tables
The existing SQLite foundation already seeds workspaces, users, and
workspace_memberships. The v5 multi-user migration should extend that
foundation instead of replacing it.
ALTER TABLE users ADD COLUMN handle TEXT;
ALTER TABLE users ADD COLUMN auth_subject TEXT;
ALTER TABLE users ADD COLUMN avatar_url TEXT;
ALTER TABLE users ADD COLUMN disabled_at TEXT;
ALTER TABLE users ADD COLUMN last_seen_at TEXT;
ALTER TABLE workspaces ADD COLUMN mode TEXT NOT NULL DEFAULT 'local';
ALTER TABLE workspaces ADD COLUMN created_by TEXT;
ALTER TABLE workspaces ADD COLUMN archived_at TEXT;
ALTER TABLE workspace_memberships ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
ALTER TABLE workspace_memberships ADD COLUMN invited_by TEXT;
ALTER TABLE workspace_memberships ADD COLUMN joined_at TEXT;
ALTER TABLE workspace_memberships ADD COLUMN disabled_at TEXT;
Role values must expand from the current owner/admin/member/viewer seed check
to the v5 set:
owner, admin, member, reviewer, read-only, agent
viewer is a legacy synonym and must be migrated to read-only.
Additional tables:
CREATE TABLE workspace_invitations (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
email TEXT,
role TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
invited_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
accepted_by TEXT REFERENCES users(id),
accepted_at TEXT,
revoked_at TEXT
);
CREATE TABLE device_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
device_name TEXT,
device_type TEXT NOT NULL DEFAULT 'browser',
session_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
last_seen_at TEXT,
expires_at TEXT NOT NULL,
revoked_at TEXT
);
CREATE TABLE agent_identities (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
default_role TEXT NOT NULL DEFAULT 'agent',
description TEXT,
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
disabled_at TEXT,
UNIQUE (workspace_id, slug)
);
CREATE TABLE api_tokens (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
actor_type TEXT NOT NULL CHECK (actor_type IN ('agent', 'service')),
actor_id TEXT NOT NULL,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
scopes_json TEXT NOT NULL,
allowed_routes_json TEXT,
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
expires_at TEXT,
revoked_at TEXT,
last_used_at TEXT
);
CREATE TABLE permission_overrides (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'agent', 'service')),
subject_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
permission TEXT NOT NULL,
effect TEXT NOT NULL CHECK (effect IN ('allow', 'deny')),
reason TEXT,
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
expires_at TEXT
);
Indexes:
CREATE INDEX idx_workspace_memberships_user_status
ON workspace_memberships(user_id, status);
CREATE INDEX idx_workspace_invitations_workspace_email
ON workspace_invitations(workspace_id, email, revoked_at, accepted_at);
CREATE INDEX idx_device_sessions_user_workspace
ON device_sessions(user_id, workspace_id, revoked_at, expires_at);
CREATE INDEX idx_agent_identities_workspace_enabled
ON agent_identities(workspace_id, disabled_at);
CREATE INDEX idx_api_tokens_workspace_actor
ON api_tokens(workspace_id, actor_type, actor_id, revoked_at);
CREATE INDEX idx_permission_overrides_subject
ON permission_overrides(workspace_id, subject_type, subject_id, resource_type);
Roles
| Role | Human session | Agent token | Scope |
|---|---|---|---|
owner | Yes | No | Full workspace control, ownership transfer, recovery, billing/release settings, destructive admin operations. |
admin | Yes | No | Workspace administration, user invites, settings, integrations, agents, workflows, policies, import/export. Cannot remove last owner. |
member | Yes | No | Board work: create/update tasks, comments, work products, workflow runs, and non-admin automation. |
reviewer | Yes | No | Review work: read board, comment, approve gates, QA tasks, generate reports. No broad task mutation except review fields. |
read-only | Yes | Service token allowed | Read board, reports, metrics, activity, and docs. No writes. |
agent | No | Yes | Scoped automation writes. Can mutate assigned tasks, run allowed workflows, post chat/status, and upload allowed artifacts. No admin settings. |
admin is not a synonym for owner. Operations that can permanently lock out a
workspace require owner.
Permission Vocabulary
Permissions are expressed as resource:action.
Resources:
workspace, user, membership, invitation, session, token,
board, task, comment, work_product, workflow, workflow_run,
agent, git, policy, setting, integration, report, telemetry,
audit, backup, maintenance, admin
Actions:
read, create, update, delete, execute, approve, export, import, manage
Examples:
task:update
workflow_run:execute
token:manage
backup:export
policy:manage
Role Permission Baseline
| Permission group | owner | admin | member | reviewer | read-only | agent |
|---|---|---|---|---|---|---|
| Workspace read | yes | yes | yes | yes | yes | scoped |
| Workspace settings manage | yes | yes | no | no | no | no |
| Users/memberships/invitations manage | yes | yes | no | no | no | no |
| Device sessions revoke | yes | yes | own | own | own | no |
| API tokens create/revoke | yes | yes | no | no | no | no |
| Board/tasks read | yes | yes | yes | yes | yes | scoped |
| Tasks create/update/delete | yes | yes | yes | review-only | no | scoped |
| Comments/chat create | yes | yes | yes | yes | no | scoped |
| Work products create/update/export | yes | yes | yes | review/export only | no | scoped |
| Workflow definitions manage | yes | yes | no | no | no | no |
| Workflow runs execute/control | yes | yes | yes | approve only | no | scoped |
| Gate approvals/QA | yes | yes | yes | yes | no | scoped |
| Agent registry/routing manage | yes | yes | no | no | no | no |
| Git operations | yes | yes | yes | no | no | scoped |
| Policies/tool policies manage | yes | yes | no | no | no | no |
| Reports/metrics read | yes | yes | yes | yes | yes | scoped |
| Audit read/export | yes | yes | no | no | no | no |
| Backup/import/export | yes | yes | no | no | no | no |
| Maintenance cleanup | yes | yes | no | no | no | no |
| Ownership transfer/delete workspace | yes | no | no | no | no | no |
Route Permission Matrix
The current API has coarse global auth and authorizeWrite. v5 should move to
route metadata that declares the required permission, then a shared authorization
middleware should enforce it against the actor context.
| Route family | Read permission | Write/control permission | Notes |
|---|---|---|---|
/api/auth/status, /api/auth/setup, /api/auth/login, /api/auth/recover | public with rate limits | public with rate limits | Setup only when no owner exists. Recover requires recovery key. |
/api/auth/logout, /api/auth/change-password, /api/auth/sessions | session:read | session:manage | Users manage their own sessions; admins can revoke workspace sessions. |
/api/tasks, /api/backlog, /api/tasks/:id/* | task:read | task:create, task:update, task:delete | Entity checks apply for assigned agents and review-only mutation. |
/api/tasks/:id/comments, /api/chat, /api/squad | comment:read | comment:create, comment:delete | Agent tokens can post only as their agent identity. |
/api/tasks/:id/work-products, /api/work-products | work_product:read | work_product:create, work_product:update, work_product:delete, work_product:export | Export defaults must apply redaction by role. |
/api/workflows | workflow:read | workflow:create, workflow:update, workflow:delete | Definition writes are admin by default unless explicitly delegated. |
/api/workflows/runs, /api/automation, /api/deliverables | workflow_run:read | workflow_run:execute, workflow_run:update, workflow_run:delete | Agents require token scopes for specific workflow IDs or task IDs. |
/api/agents, /api/agents/register, /api/agents/permissions, /api/agents/routing | agent:read | agent:manage | Agent self-heartbeat is allowed only for matching agent token. |
/api/github, /api/diff, /api/preview | git:read | git:execute | Diff/preview reads are allowed to members; branch/PR mutation needs scoped write. |
/api/settings, /api/config, /api/integrations, /api/hooks | setting:read | setting:manage, integration:manage | Secrets remain redacted unless owner/admin requests a reveal workflow. |
/api/policies, /api/tool-policies, /api/delegation | policy:read | policy:manage | Policy writes are admin-only. |
/api/metrics, /api/analytics, /api/reports, /api/summary, /api/activity, /api/status-history | report:read | report:export or maintenance:manage | Deletes and retention controls are admin-only. |
/api/telemetry, /api/traces, /api/audit | telemetry:read or audit:read | telemetry:create, audit:export | Agent telemetry writes require token scope. Audit records are append-only. |
/api/sqlite/* | backup:read | backup:export, backup:import, admin:manage | Migration/import are owner/admin only and must emit audit entries. |
/api/search, /api/docs, /api/doc-freshness | board:read | setting:manage or maintenance:manage | Search results are filtered by entity permissions. |
/api/health, /api/health/live, /api/health/ready | public | none | Deep diagnostics require admin:manage. |
/api/webhooks/* | token-specific | token-specific | Webhooks use signed/shared secrets and map to service actors. |
Entity-Level Rules
Route authorization is necessary but not sufficient. Repository and service methods should enforce entity-level checks before returning rows or mutating state.
| Entity | Rule |
|---|---|
| Workspace | Actor must have an active membership or token for the workspace. |
| Task | Humans can access workspace tasks by role. Agent tokens can access assigned tasks, workflow task context, or tasks allowed by token scope. |
| Backlog/archive | Same as task rules. Archive restore/delete require task:update or task:delete. |
| Comment/chat message | Inherits task/session access. Delete requires author, admin, or owner. |
| Work product | Inherits source task/run access. Redaction level depends on role and export permission. |
| Workflow definition | Read requires workspace access. Mutation requires workflow:* admin permission unless owner/admin grants an override. |
| Workflow run | Inherits workflow plus source task access. Agents can control only runs they own or are assigned to. |
| Attachment/deliverable | Inherits task access. Delete requires uploader, task write permission, admin, or retention cleanup. |
| Settings/integration | Read is redacted by default. Secret reveal is owner-only and should be avoided where possible. |
| Audit/telemetry | Raw records are owner/admin only. Aggregates may be visible to read/report roles after redaction. |
Repository methods should accept AuthContext or a narrowed
AuthorizationSubject instead of relying on global local filters once #335 and
#336 begin implementation.
Localhost Bypass
Current behavior:
VERITAS_AUTH_LOCALHOST_BYPASS=truecan allow localhost requests outside production.VERITAS_AUTH_LOCALHOST_ROLEdefaults toread-only.- Production disables bypass in code.
v5 behavior:
| Mode | Localhost bypass |
|---|---|
| Local development | Allowed only when explicitly enabled. Default role remains read-only. |
| Desktop local app | Avoid bypass by default. The desktop shell should use a local session or keychain-backed token. |
| LAN/server | Disabled. Startup should warn if bypass env vars are present. |
| Mobile/remote pairing | Disabled. Pairing creates device sessions, not bypass grants. |
| Production | Disabled unconditionally. |
Remote/server mode should expose an auth diagnostic that reports bypass as
disabled_by_mode rather than merely echoing environment values.
Agent API Keys
Agent keys are not human sessions.
Rules:
- Stored only as hashes in
api_tokens.token_hash. - Bound to
actor_type = 'agent'and anagent_identities.id. - Include
workspace_id,scopes_json, optionalallowed_routes_json, expiry, and revocation timestamps. - Cannot call password, recovery, invitation, session, or membership endpoints.
- Cannot create new API tokens.
- Cannot change settings, integrations, policies, or role assignments.
- Can write telemetry, status, chat, task updates, work products, and workflow progress only when the token scope allows the target task/workflow.
- Must record
tokenIdandactorIdon audit, activity, status history, task history, workflow runs, work products, and telemetry.
Example scopes:
{
"tasks": ["read", "update", "comment"],
"workflowRuns": ["read", "execute", "update"],
"workProducts": ["create", "update", "export-redacted"],
"telemetry": ["create"],
"constraints": {
"taskIds": ["VK-123"],
"workflowIds": ["qa-review"],
"expiresAt": "2026-06-30T00:00:00.000Z"
}
}
Actor Attribution
Every write path must emit enough actor data to reconstruct who or what changed the system. Store normalized IDs for joins and denormalized display values for durable history.
Required fields for new audit/activity/task-history/workflow records:
| Field | Description |
|---|---|
workspace_id | Workspace where the action occurred. |
actor_type | user, agent, service, system, or localhost-bypass. |
actor_id | Stable user, agent, service token, or system id. |
actor_display_name | Display value at write time. |
actor_role | Effective role at write time. |
auth_method | session, api-token, recovery, localhost-bypass, or system. |
session_id | Device/session id for human sessions. |
token_id | API token id for agent/service tokens. |
request_id | Correlates API logs, audit entries, and telemetry. |
target_type | Entity type, such as task, workflow, policy, setting. |
target_id | Entity id. |
redaction_level | none, standard, sensitive, or secret-ref-only. |
For backward compatibility, old records without actor columns should render as:
actor_type: system
actor_id: legacy-migration
actor_display_name: Legacy Migration
Migration Plan
- Keep the current
localworkspace andlocal-userseed records. - Expand role constraints and migrate
viewermemberships toread-only. - Add invitation, device session, agent identity, scoped API token, and permission override tables.
- Convert
VERITAS_ADMIN_KEYinto an owner-capable service token at runtime. Do not persist plaintext env keys. If imported into SQLite, store only a token hash and marksource = env. - Convert
VERITAS_API_KEYS=name:key:roleentries into scopedapi_tokens. Role mapping:admin-> service token with admin permissionsagent-> agent identity plus scoped agent tokenread-only-> service token with read/report scopes
- Preserve password auth and recovery key behavior for the initial owner.
- Add
AuthContextto HTTP and WebSocket authentication. - Add route permission metadata and enforce role permissions.
- Add entity-level repository checks for workspace, task, workflow, and work product access.
- Emit actor attribution for all new writes. Keep legacy rows readable.
- Add admin diagnostics that flag unsafe remote/server auth configuration.
Rollback expectation:
- New tables can remain unused if remote/server mode is disabled.
- Existing local password and env-key auth must still work after rollback to single-user behavior.
- Token imports from env are repeatable because token hashes are unique and env source records can be upserted.
UX Flows
First-Run Local Mode
- User opens the desktop or local web app.
- App seeds
localworkspace andlocal-userowner if missing. - If no password exists, setup asks for an owner password and shows the recovery key once.
- The user lands on the board without choosing a workspace.
- Settings shows "Local workspace" and a path to enable server mode later.
Upgrade Local Board to Server Mode
- Owner opens Settings -> Access.
- App shows current auth risks: localhost bypass, weak admin key, missing HTTPS, unrotated env API keys.
- Owner chooses "Enable server mode".
- App disables bypass, requires an owner password, creates a server workspace mode flag, and prompts for HTTPS/reverse-proxy guidance.
- App offers to migrate env API keys into hashed scoped tokens.
- Audit records the mode change and token migration.
Invite a Human User
- Owner/admin enters email or local username and selects a role.
- App creates
workspace_invitationswith a hashed token and expiry. - Invitee accepts, sets password if needed, and joins as an active membership.
- Audit records invitation created, accepted, and role assigned.
Create an Agent Token
- Owner/admin creates or selects an agent identity.
- Owner/admin chooses task/workflow scopes and expiry.
- App shows the plaintext token once.
- Token hash, scopes, creator, and expiry are stored.
- Agent requests include the token and resolve to
actor_type = agent.
Mobile or Remote Device Pairing
- Owner/admin creates a short-lived pairing payload for a workspace.
- Device redeems the code/link with its client id, client mode, capabilities, nonce, signed timestamp, and signature.
- App creates a
device_sessionsrow with hashed secret, device metadata, scopes, capabilities, role, and expiry. - Session appears in account security UI with last seen, connection state, degraded reason, and revoke/test actions.
- Remote requests authenticate with
authMethod: device-sessionand are never treated as localhost bypass.
Lost Admin Recovery
- User chooses recovery.
- Recovery key verifies against the stored recovery hash.
- App rotates the password and invalidates device sessions.
- Owner membership is preserved; recovery cannot create a second owner unless no active owner exists.
- Audit records recovery without storing the recovery key.
Implementation Checklist
- Add schema migration for role expansion, invitations, device sessions, agent identities, API tokens, and permission overrides.
- Replace role-only auth with
AuthContextfor HTTP and WebSocket requests. - Add route permission metadata and a shared permission middleware.
- Add entity-level permission checks in storage/service layers.
- Add actor attribution to write paths and audit/activity records.
- Add user/workspace/member/token management APIs.
- Add UX for first-run owner setup, workspace access, invitations, sessions, and agent tokens.
- Add tests for local compatibility, remote bypass rejection, role matrix, agent token scope enforcement, and audit attribution.
Out of Scope
- External SSO provider implementation.
- Billing or organization hierarchy.
- Per-field permissions beyond redaction and secret reference rules.
- Public unauthenticated boards.