Status: planned for v5.0-alpha.1 Issue: #334
Veritas Kanban v5 needs a multi-user identity and authorization model that works in three operating modes:
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.
local and
local-user, and the owner can continue using password auth or an admin key.| 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;
}
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);
| 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.
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
| 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 |
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. |
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.
Current behavior:
VERITAS_AUTH_LOCALHOST_BYPASS=true can allow localhost requests outside
production.VERITAS_AUTH_LOCALHOST_ROLE defaults to read-only.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 keys are not human sessions.
Rules:
api_tokens.token_hash.actor_type = 'agent' and an agent_identities.id.workspace_id, scopes_json, optional allowed_routes_json, expiry,
and revocation timestamps.tokenId and actorId on 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"
}
}
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
local workspace and local-user seed records.viewer memberships to read-only.VERITAS_ADMIN_KEY into an owner-capable service token at runtime.
Do not persist plaintext env keys. If imported into SQLite, store only a
token hash and mark source = env.VERITAS_API_KEYS=name:key:role entries into scoped api_tokens.
Role mapping:
admin -> service token with admin permissionsagent -> agent identity plus scoped agent tokenread-only -> service token with read/report scopesAuthContext to HTTP and WebSocket authentication.Rollback expectation:
local workspace and local-user owner if missing.workspace_invitations with a hashed token and expiry.actor_type = agent.device_sessions row with hashed secret, device metadata,
scopes, capabilities, role, and expiry.authMethod: device-session and are never
treated as localhost bypass.AuthContext for HTTP and WebSocket requests.