From ebea16344cf5ac78bab31617b6a982426c7ea846 Mon Sep 17 00:00:00 2001 From: Jeffrey Smith Date: Wed, 25 Mar 2026 20:22:02 -0400 Subject: [PATCH] step 4: fresh kernel-only migrations Deleted all 23+23 old chat-switchboard migration files. Wrote 9+9 clean kernel-only migrations in postgres/ and sqlite/ subdirs. 27 tables per dialect covering all 20 store interfaces: 001: users, refresh_tokens, policies, settings, presence, oidc 002: teams, team_members, groups, group_members 003: packages, package_user_settings, ext_permissions, ext_data_tables 004: ext_connections, ext_dependencies, resource_grants 005: notifications, notification_preferences 006: audit_log 007: workflows, workflow_stages, workflow_versions 008: tasks, task_runs 009: ws_tickets, rate_limit_counters Dropped: providers, personas, channels, messages, knowledge, notes, memory, workspaces, projects, folders, files, usage_log, tool_health, workflow_assignments, ext_view_channels. Schema changes vs old: - pgvector extension removed (no KB embeddings) - resource_grants: resource_type CHECK removed (open-ended) - workflow_stages: persona_id kept as nullable TEXT (no FK) - workflow_stages: stage_mode default=form_only, added custom - tasks: dropped output_channel_id, provider_config_id columns - tasks: task_type removed prompt, output_mode: notification|webhook|log - task_runs: dropped channel_id - platform_policies: kernel-only seeds - global_settings: kernel-only seeds, site name=Switchboard Core - Everyone group: kernel permissions (extension.use, workflow.submit) -2815/+487 lines. --- server/database/migrations/003_providers.sql | 183 ------------ server/database/migrations/004_personas.sql | 151 ---------- server/database/migrations/005_channels.sql | 262 ------------------ server/database/migrations/006_knowledge.sql | 114 -------- server/database/migrations/007_notes.sql | 75 ----- server/database/migrations/008_memory.sql | 89 ------ server/database/migrations/009_workspaces.sql | 126 --------- server/database/migrations/010_projects.sql | 152 ---------- .../migrations/012_package_user_settings.sql | 8 - server/database/migrations/013_files.sql | 33 --- .../database/migrations/014_audit_usage.sql | 58 ---- server/database/migrations/015_health.sql | 23 -- server/database/migrations/016_packages.sql | 129 --------- server/database/migrations/017_auth.sql | 18 -- server/database/migrations/018_workflows.sql | 149 ---------- server/database/migrations/020_ha.sql | 37 --- server/database/migrations/021_export.sql | 18 -- .../migrations/022_ext_connections.sql | 37 --- .../migrations/023_ext_dependencies.sql | 31 --- .../migrations/{ => postgres}/001_core.sql | 87 ++---- .../migrations/{ => postgres}/002_teams.sql | 40 +-- .../migrations/postgres/003_packages.sql | 78 ++++++ .../migrations/postgres/004_connections.sql | 66 +++++ .../005_notifications.sql} | 20 +- .../migrations/postgres/006_audit.sql | 20 ++ .../migrations/postgres/007_workflows.sql | 76 +++++ .../{019_tasks.sql => postgres/008_tasks.sql} | 57 ++-- .../database/migrations/postgres/009_ha.sql | 21 ++ .../database/migrations/sqlite/001_core.sql | 30 +- .../database/migrations/sqlite/002_teams.sql | 23 +- .../{016_packages.sql => 003_packages.sql} | 33 +-- .../migrations/sqlite/003_providers.sql | 110 -------- .../migrations/sqlite/004_connections.sql | 59 ++++ .../migrations/sqlite/004_personas.sql | 83 ------ .../migrations/sqlite/005_channels.sql | 160 ----------- ...otifications.sql => 005_notifications.sql} | 10 +- .../database/migrations/sqlite/006_audit.sql | 20 ++ .../migrations/sqlite/006_knowledge.sql | 73 ----- .../database/migrations/sqlite/007_notes.sql | 34 --- .../{018_workflows.sql => 007_workflows.sql} | 41 +-- .../database/migrations/sqlite/008_memory.sql | 45 --- .../sqlite/{019_tasks.sql => 008_tasks.sql} | 35 +-- server/database/migrations/sqlite/009_ha.sql | 21 ++ .../migrations/sqlite/009_workspaces.sql | 69 ----- .../migrations/sqlite/010_projects.sql | 62 ----- .../sqlite/012_package_user_settings.sql | 3 - .../database/migrations/sqlite/013_files.sql | 28 -- .../migrations/sqlite/014_audit_usage.sql | 42 --- .../database/migrations/sqlite/015_health.sql | 17 -- .../database/migrations/sqlite/017_auth.sql | 11 - server/database/migrations/sqlite/020_ha.sql | 34 --- .../database/migrations/sqlite/021_export.sql | 15 - .../migrations/sqlite/022_ext_connections.sql | 24 -- .../sqlite/023_ext_dependencies.sql | 62 ----- 54 files changed, 487 insertions(+), 2815 deletions(-) delete mode 100644 server/database/migrations/003_providers.sql delete mode 100644 server/database/migrations/004_personas.sql delete mode 100644 server/database/migrations/005_channels.sql delete mode 100644 server/database/migrations/006_knowledge.sql delete mode 100644 server/database/migrations/007_notes.sql delete mode 100644 server/database/migrations/008_memory.sql delete mode 100644 server/database/migrations/009_workspaces.sql delete mode 100644 server/database/migrations/010_projects.sql delete mode 100644 server/database/migrations/012_package_user_settings.sql delete mode 100644 server/database/migrations/013_files.sql delete mode 100644 server/database/migrations/014_audit_usage.sql delete mode 100644 server/database/migrations/015_health.sql delete mode 100644 server/database/migrations/016_packages.sql delete mode 100644 server/database/migrations/017_auth.sql delete mode 100644 server/database/migrations/018_workflows.sql delete mode 100644 server/database/migrations/020_ha.sql delete mode 100644 server/database/migrations/021_export.sql delete mode 100644 server/database/migrations/022_ext_connections.sql delete mode 100644 server/database/migrations/023_ext_dependencies.sql rename server/database/migrations/{ => postgres}/001_core.sql (55%) rename server/database/migrations/{ => postgres}/002_teams.sql (66%) create mode 100644 server/database/migrations/postgres/003_packages.sql create mode 100644 server/database/migrations/postgres/004_connections.sql rename server/database/migrations/{011_notifications.sql => postgres/005_notifications.sql} (61%) create mode 100644 server/database/migrations/postgres/006_audit.sql create mode 100644 server/database/migrations/postgres/007_workflows.sql rename server/database/migrations/{019_tasks.sql => postgres/008_tasks.sql} (55%) create mode 100644 server/database/migrations/postgres/009_ha.sql rename server/database/migrations/sqlite/{016_packages.sql => 003_packages.sql} (76%) delete mode 100644 server/database/migrations/sqlite/003_providers.sql create mode 100644 server/database/migrations/sqlite/004_connections.sql delete mode 100644 server/database/migrations/sqlite/004_personas.sql delete mode 100644 server/database/migrations/sqlite/005_channels.sql rename server/database/migrations/sqlite/{011_notifications.sql => 005_notifications.sql} (69%) create mode 100644 server/database/migrations/sqlite/006_audit.sql delete mode 100644 server/database/migrations/sqlite/006_knowledge.sql delete mode 100644 server/database/migrations/sqlite/007_notes.sql rename server/database/migrations/sqlite/{018_workflows.sql => 007_workflows.sql} (61%) delete mode 100644 server/database/migrations/sqlite/008_memory.sql rename server/database/migrations/sqlite/{019_tasks.sql => 008_tasks.sql} (61%) create mode 100644 server/database/migrations/sqlite/009_ha.sql delete mode 100644 server/database/migrations/sqlite/009_workspaces.sql delete mode 100644 server/database/migrations/sqlite/010_projects.sql delete mode 100644 server/database/migrations/sqlite/012_package_user_settings.sql delete mode 100644 server/database/migrations/sqlite/013_files.sql delete mode 100644 server/database/migrations/sqlite/014_audit_usage.sql delete mode 100644 server/database/migrations/sqlite/015_health.sql delete mode 100644 server/database/migrations/sqlite/017_auth.sql delete mode 100644 server/database/migrations/sqlite/020_ha.sql delete mode 100644 server/database/migrations/sqlite/021_export.sql delete mode 100644 server/database/migrations/sqlite/022_ext_connections.sql delete mode 100644 server/database/migrations/sqlite/023_ext_dependencies.sql diff --git a/server/database/migrations/003_providers.sql b/server/database/migrations/003_providers.sql deleted file mode 100644 index 52e0c64..0000000 --- a/server/database/migrations/003_providers.sql +++ /dev/null @@ -1,183 +0,0 @@ --- ========================================== --- Chat Switchboard — 003 Providers & Routing --- ========================================== --- Provider configs, model catalog, pricing, health, capability --- overrides, and routing policies. --- Consolidated v0.28.0: adds capability_match policy type. --- ========================================== - --- ========================================= --- PROVIDER CONFIGS --- ========================================= - -CREATE TABLE IF NOT EXISTS provider_configs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - scope VARCHAR(10) NOT NULL - CHECK (scope IN ('global', 'team', 'personal')), - owner_id UUID, - name VARCHAR(100) NOT NULL, - provider VARCHAR(50) NOT NULL, - endpoint TEXT NOT NULL, - - -- Encrypted API key (AES-256-GCM) - api_key_enc BYTEA, - key_nonce BYTEA, - key_scope TEXT NOT NULL DEFAULT 'global', - - model_default VARCHAR(100), - config JSONB DEFAULT '{}'::jsonb, - headers JSONB DEFAULT '{}'::jsonb, - settings JSONB DEFAULT '{}'::jsonb, - is_active BOOLEAN DEFAULT true, - is_private BOOLEAN DEFAULT false, - proxy_mode TEXT NOT NULL DEFAULT 'system' - CHECK (proxy_mode IN ('system', 'direct', 'custom')), - proxy_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - CONSTRAINT chk_proxy_url CHECK (proxy_mode != 'custom' OR proxy_url IS NOT NULL) -); - -CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope); -CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id) WHERE owner_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active) WHERE is_active = true; -DROP TRIGGER IF EXISTS provider_configs_updated_at ON provider_configs; -CREATE TRIGGER provider_configs_updated_at BEFORE UPDATE ON provider_configs - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON COLUMN provider_configs.scope IS 'global=admin-managed, team=team-scoped, personal=user BYOK'; -COMMENT ON COLUMN provider_configs.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal scope'; -COMMENT ON COLUMN provider_configs.api_key_enc IS 'AES-256-GCM encrypted API key (BYTEA)'; -COMMENT ON COLUMN provider_configs.key_nonce IS 'AES-GCM nonce for api_key_enc'; -COMMENT ON COLUMN provider_configs.key_scope IS 'Encryption tier: global/team use env key, personal uses UEK'; -COMMENT ON COLUMN provider_configs.headers IS 'Custom HTTP headers (e.g. OpenRouter HTTP-Referer)'; -COMMENT ON COLUMN provider_configs.settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)'; -COMMENT ON COLUMN provider_configs.is_private IS 'Data stays on-prem (local/self-hosted provider)'; -COMMENT ON COLUMN provider_configs.proxy_mode IS 'system=env proxy, direct=no proxy, custom=explicit URL'; -COMMENT ON COLUMN provider_configs.proxy_url IS 'Explicit proxy URL when proxy_mode=custom (http/https/socks5)'; - - --- ========================================= --- MODEL CATALOG --- ========================================= - -CREATE TABLE IF NOT EXISTS model_catalog ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - display_name TEXT, - model_type VARCHAR(20) DEFAULT 'chat', - capabilities JSONB NOT NULL DEFAULT '{}', - pricing JSONB, - visibility VARCHAR(10) DEFAULT 'disabled' - CHECK (visibility IN ('enabled', 'disabled', 'team')), - last_synced_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(provider_config_id, model_id) -); - -CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id); -CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility) WHERE visibility = 'enabled'; -CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type); -DROP TRIGGER IF EXISTS model_catalog_updated_at ON model_catalog; -CREATE TRIGGER model_catalog_updated_at BEFORE UPDATE ON model_catalog - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON COLUMN model_catalog.visibility IS 'disabled by default — admin must enable for global models'; -COMMENT ON COLUMN model_catalog.model_type IS 'chat, embedding, image — from provider API sync'; -COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}'; - - --- ========================================= --- MODEL PRICING --- ========================================= - -CREATE TABLE IF NOT EXISTS model_pricing ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - input_per_m NUMERIC(10,6), - output_per_m NUMERIC(10,6), - cache_create_per_m NUMERIC(10,6), - cache_read_per_m NUMERIC(10,6), - currency TEXT NOT NULL DEFAULT 'USD', - source TEXT NOT NULL DEFAULT 'manual', - updated_at TIMESTAMPTZ DEFAULT NOW(), - updated_by UUID REFERENCES users(id), - UNIQUE(provider_config_id, model_id) -); - - --- ========================================= --- PROVIDER HEALTH --- ========================================= - -CREATE TABLE IF NOT EXISTS provider_health ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - window_start TIMESTAMPTZ NOT NULL, - request_count INT NOT NULL DEFAULT 0, - error_count INT NOT NULL DEFAULT 0, - timeout_count INT NOT NULL DEFAULT 0, - total_latency_ms BIGINT NOT NULL DEFAULT 0, - max_latency_ms INT NOT NULL DEFAULT 0, - rate_limit_count INT NOT NULL DEFAULT 0, - last_error TEXT, - last_error_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - UNIQUE (provider_config_id, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_provider_health_window - ON provider_health (provider_config_id, window_start DESC); - - --- ========================================= --- CAPABILITY OVERRIDES --- ========================================= - -CREATE TABLE IF NOT EXISTS capability_overrides ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - field TEXT NOT NULL, - value TEXT NOT NULL, - set_by UUID REFERENCES users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - UNIQUE (provider_config_id, model_id, field) -); - -CREATE INDEX IF NOT EXISTS idx_capability_overrides_model - ON capability_overrides (model_id); - - --- ========================================= --- ROUTING POLICIES --- ========================================= - -CREATE TABLE IF NOT EXISTS routing_policies ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(200) NOT NULL, - scope VARCHAR(10) NOT NULL DEFAULT 'global' - CHECK (scope IN ('global', 'team')), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - priority INTEGER NOT NULL DEFAULT 100, - policy_type VARCHAR(30) NOT NULL - CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')), - config JSONB NOT NULL DEFAULT '{}'::jsonb, - is_active BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_routing_policies_active - ON routing_policies(is_active, priority) WHERE is_active = true; -CREATE INDEX IF NOT EXISTS idx_routing_policies_team - ON routing_policies(team_id) WHERE team_id IS NOT NULL; - -DROP TRIGGER IF EXISTS routing_policies_updated_at ON routing_policies; -CREATE TRIGGER routing_policies_updated_at BEFORE UPDATE ON routing_policies - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); diff --git a/server/database/migrations/004_personas.sql b/server/database/migrations/004_personas.sql deleted file mode 100644 index d662749..0000000 --- a/server/database/migrations/004_personas.sql +++ /dev/null @@ -1,151 +0,0 @@ --- ========================================== --- Chat Switchboard — 004 Personas & Grants --- ========================================== --- Persona definitions, persona capability grants, and resource access grants. --- ICD §4 (Personas) --- ========================================== - --- ========================================= --- PERSONAS --- ========================================= - -CREATE TABLE IF NOT EXISTS personas ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(100) NOT NULL, - handle VARCHAR(50), - description TEXT DEFAULT '', - icon VARCHAR(10) DEFAULT '', - avatar TEXT DEFAULT '', - - -- Base model binding - base_model_id TEXT NOT NULL, - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - - -- Behavioral configuration - system_prompt TEXT DEFAULT '', - temperature FLOAT, - max_tokens INT, - thinking_budget INT, - top_p FLOAT, - - -- Memory configuration (v0.18.0) - memory_enabled BOOLEAN NOT NULL DEFAULT true, - memory_extraction_prompt TEXT, - - -- Scope & ownership - scope VARCHAR(10) NOT NULL - CHECK (scope IN ('global', 'team', 'personal')), - owner_id UUID, - created_by UUID NOT NULL REFERENCES users(id), - - -- State - is_active BOOLEAN DEFAULT true, - is_shared BOOLEAN DEFAULT false, - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope); -CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id) WHERE owner_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active) WHERE is_active = true; -CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL; -DROP TRIGGER IF EXISTS personas_updated_at ON personas; -CREATE TRIGGER personas_updated_at BEFORE UPDATE ON personas - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON COLUMN personas.scope IS 'global=all users, team=team members, personal=creator only'; -COMMENT ON COLUMN personas.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal'; -COMMENT ON COLUMN personas.handle IS 'Unique @mention handle, e.g. veronica-sharpe. Auto-generated from name.'; -COMMENT ON COLUMN personas.is_shared IS 'Personal personas shared with others (read-only)'; -COMMENT ON COLUMN personas.memory_enabled IS 'Whether memory tools and extraction are active for this persona'; -COMMENT ON COLUMN personas.memory_extraction_prompt IS 'Custom extraction prompt; NULL = use system default'; - - --- ========================================= --- PERSONA GROUPS (v0.23.0) --- ========================================= --- Saved roster templates. A group is a named set of personas --- that can be stamped onto any new conversation. - -CREATE TABLE IF NOT EXISTS persona_groups ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(100) NOT NULL, - description TEXT DEFAULT '', - owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - scope VARCHAR(10) NOT NULL DEFAULT 'personal' - CHECK (scope IN ('global', 'team', 'personal')), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id); -DROP TRIGGER IF EXISTS persona_groups_updated_at ON persona_groups; -CREATE TRIGGER persona_groups_updated_at BEFORE UPDATE ON persona_groups - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TABLE IF NOT EXISTS persona_group_members ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - group_id UUID NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE, - persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - is_leader BOOLEAN NOT NULL DEFAULT false, - sort_order INT NOT NULL DEFAULT 0, - UNIQUE(group_id, persona_id) -); - -CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id); - - --- ========================================= --- PERSONA GRANTS (what a persona can do) --- ========================================= - -CREATE TABLE IF NOT EXISTS persona_grants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - grant_type VARCHAR(30) NOT NULL, - grant_ref TEXT NOT NULL, - config JSONB DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(persona_id, grant_type, grant_ref) -); - -CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id); -CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type); - -COMMENT ON TABLE persona_grants IS 'What a persona can do: tools it can call, KBs it can search, etc.'; -COMMENT ON COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base, api_endpoint'; -COMMENT ON COLUMN persona_grants.grant_ref IS 'tool: function name. knowledge_base: UUID. api_endpoint: identifier.'; - - --- ========================================= --- RESOURCE GRANTS (who can USE a resource) --- ========================================= - -CREATE TABLE IF NOT EXISTS resource_grants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - resource_type VARCHAR(30) NOT NULL - CHECK (resource_type IN ('persona', 'knowledge_base', 'project')), - resource_id UUID NOT NULL, - grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only' - CHECK (grant_scope IN ('team_only', 'global', 'groups')), - granted_groups UUID[] NOT NULL DEFAULT '{}', - created_by UUID NOT NULL REFERENCES users(id), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - - UNIQUE(resource_type, resource_id) -); - -CREATE INDEX IF NOT EXISTS idx_resource_grants_resource - ON resource_grants(resource_type, resource_id); -CREATE INDEX IF NOT EXISTS idx_resource_grants_groups - ON resource_grants USING gin(granted_groups); - -DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants; -CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE resource_grants IS 'Who can USE a resource (personas, KBs, projects). Cross-team access via groups.'; -COMMENT ON COLUMN resource_grants.grant_scope IS 'team_only: existing team behavior. global: all users. groups: specific group list.'; diff --git a/server/database/migrations/005_channels.sql b/server/database/migrations/005_channels.sql deleted file mode 100644 index 6948884..0000000 --- a/server/database/migrations/005_channels.sql +++ /dev/null @@ -1,262 +0,0 @@ --- ========================================== --- Chat Switchboard — 005 Channels & Conversations --- ========================================== --- Channels (all types, all columns), messages, participants, --- models, cursors, folders, user prefs, session participants. --- Consolidated v0.28.0: merges 005 + 016 (multiuser) + 017 (unread) --- + 021 (sessions) + 024 (workflow instance cols) + 026 (service type) --- + v0.28.0 (kb_auto_inject). --- --- Note: project_id/workspace_id FKs added by 010_projects.sql. --- Note: workflow_id FK added by 018_workflows.sql. --- ========================================== - --- ========================================= --- FOLDERS --- ========================================= - -CREATE TABLE IF NOT EXISTS folders ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name VARCHAR(200) NOT NULL, - parent_id UUID REFERENCES folders(id) ON DELETE CASCADE, - sort_order INT DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(user_id, name, parent_id) -); - -CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id); -CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id); -DROP TRIGGER IF EXISTS folders_updated_at ON folders; -CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - - --- ========================================= --- CHANNELS --- ========================================= - -CREATE TABLE IF NOT EXISTS channels ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(500) NOT NULL, - description TEXT, - type VARCHAR(20) DEFAULT 'direct' - CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service')), - model VARCHAR(100), - system_prompt TEXT, - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - is_archived BOOLEAN DEFAULT false, - is_pinned BOOLEAN DEFAULT false, - folder_id UUID REFERENCES folders(id) ON DELETE SET NULL, - folder TEXT, -- legacy text folder (compat) - team_id UUID REFERENCES teams(id) ON DELETE SET NULL, - settings JSONB DEFAULT '{}'::jsonb, - tags TEXT[], - - -- Multi-user (v0.23.1) - ai_mode VARCHAR(20) NOT NULL DEFAULT 'auto' - CHECK (ai_mode IN ('auto', 'mention_only', 'off')), - topic TEXT, - - -- Anonymous sessions (v0.24.3) - allow_anonymous BOOLEAN NOT NULL DEFAULT false, - - -- Workflow instance columns (v0.26.0) - -- workflow_id FK added by 018_workflows.sql (deferred — table doesn't exist yet) - workflow_id UUID, - workflow_version INTEGER, - current_stage INTEGER DEFAULT 0, - stage_data JSONB DEFAULT '{}'::jsonb, - workflow_status TEXT DEFAULT 'active' - CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')), - last_activity_at TIMESTAMPTZ DEFAULT NOW(), - stage_entered_at TIMESTAMPTZ, - - -- KB auto-injection (v0.28.0) - kb_auto_inject BOOLEAN NOT NULL DEFAULT false, - - -- Cross-domain FKs (added by 010_projects.sql) - project_id UUID, - workspace_id UUID, - - -- Retention (v0.37.14) - purge_after TIMESTAMPTZ, - - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id); -CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at DESC); -CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type); -CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_tags ON channels USING GIN(tags); -CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id) WHERE project_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id) WHERE workspace_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id) WHERE workflow_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_workflow_status ON channels(workflow_status) WHERE workflow_status = 'active'; -CREATE INDEX IF NOT EXISTS idx_channels_purge_after ON channels(purge_after) WHERE purge_after IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channels_workflow_active ON channels(workflow_id, workflow_status) WHERE workflow_id IS NOT NULL AND workflow_status = 'active'; - -DROP TRIGGER IF EXISTS channels_updated_at ON channels; -CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON COLUMN channels.type IS 'direct=single-user, dm=human-to-human, group=multi-participant, channel=named persistent, workflow=staged, service=autonomous task'; -COMMENT ON COLUMN channels.ai_mode IS 'auto=respond always, mention_only=@mention required, off=AI disabled'; -COMMENT ON COLUMN channels.topic IS 'Short channel description shown in header'; -COMMENT ON COLUMN channels.allow_anonymous IS 'When true, unauthenticated visitors can join via session token'; -COMMENT ON COLUMN channels.workflow_id IS 'FK to workflows — set when type=workflow (constraint added by 018)'; -COMMENT ON COLUMN channels.workflow_version IS 'Pinned version number from workflow_versions'; -COMMENT ON COLUMN channels.current_stage IS 'Ordinal of the active workflow stage'; -COMMENT ON COLUMN channels.stage_data IS 'Accumulated form data collected across stages'; -COMMENT ON COLUMN channels.workflow_status IS 'active/completed/stale/cancelled'; -COMMENT ON COLUMN channels.last_activity_at IS 'Updated on each message — used by staleness sweep'; -COMMENT ON COLUMN channels.stage_entered_at IS 'Timestamp when current workflow stage was entered — used for SLA computation'; -COMMENT ON COLUMN channels.kb_auto_inject IS 'When true, top-K KB chunks auto-prepend to context'; - - --- ========================================= --- MESSAGES --- ========================================= - -CREATE TABLE IF NOT EXISTS messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - role VARCHAR(20) NOT NULL - CHECK (role IN ('user', 'assistant', 'system', 'tool')), - content TEXT NOT NULL, - model VARCHAR(100), - tokens_used INTEGER, - tool_calls JSONB, - metadata JSONB DEFAULT '{}'::jsonb, - parent_id UUID REFERENCES messages(id) ON DELETE SET NULL, - sibling_index INTEGER DEFAULT 0, - participant_type VARCHAR(10) DEFAULT 'user', - participant_id VARCHAR(255), - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - deleted_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at); -CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id); -CREATE INDEX IF NOT EXISTS idx_messages_alive ON messages(channel_id, created_at) - WHERE deleted_at IS NULL; -CREATE INDEX IF NOT EXISTS idx_messages_parent_alive ON messages(parent_id, sibling_index) - WHERE deleted_at IS NULL; - -COMMENT ON COLUMN messages.parent_id IS 'Tree parent for conversation forking'; -COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)'; - - --- ========================================= --- CHANNEL PARTICIPANTS & MODELS --- ========================================= - -CREATE TABLE IF NOT EXISTS channel_participants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - participant_type VARCHAR(10) NOT NULL DEFAULT 'user' - CHECK (participant_type IN ('user', 'persona', 'session')), - participant_id UUID NOT NULL, - role VARCHAR(10) NOT NULL DEFAULT 'member' - CHECK (role IN ('owner', 'member', 'observer')), - display_name VARCHAR(200), - avatar_url TEXT, - joined_at TIMESTAMPTZ DEFAULT NOW(), - last_read_at TIMESTAMPTZ DEFAULT NOW(), - last_read_message_id UUID, -- v0.23.2: unread cursor - UNIQUE(channel_id, participant_type, participant_id) -); - -CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_id); - -CREATE TABLE IF NOT EXISTS channel_models ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - model_id VARCHAR(255) NOT NULL, - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, - display_name VARCHAR(100), - system_prompt TEXT, - settings JSONB DEFAULT '{}'::jsonb, - is_default BOOLEAN DEFAULT false, - added_at TIMESTAMPTZ DEFAULT NOW() -); - --- Raw model entries (no persona): unique per channel+model+provider -CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw - ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL; --- Persona entries: one per persona per channel (same model allowed for different personas) -CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona - ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL; - -CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id); - - --- ========================================= --- CHANNEL CURSORS (forking navigation) --- ========================================= - -CREATE TABLE IF NOT EXISTS channel_cursors ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - active_leaf_id UUID REFERENCES messages(id) ON DELETE SET NULL, - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(channel_id, user_id) -); - -CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id); - - --- ========================================= --- USER MODEL SETTINGS --- ========================================= - -CREATE TABLE IF NOT EXISTS user_model_settings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - hidden BOOLEAN DEFAULT false, - preferred_temperature FLOAT, - preferred_max_tokens INT, - sort_order INT DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(user_id, model_id, provider_config_id) -); - -CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id); -DROP TRIGGER IF EXISTS user_model_settings_updated_at ON user_model_settings; -CREATE TRIGGER user_model_settings_updated_at BEFORE UPDATE ON user_model_settings - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - - --- ========================================= --- SESSION PARTICIPANTS (v0.24.3) --- ========================================= - -CREATE TABLE IF NOT EXISTS session_participants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - session_token TEXT NOT NULL UNIQUE, - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - display_name TEXT NOT NULL DEFAULT 'Visitor', - fingerprint TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_session_participants_channel - ON session_participants(channel_id); - -CREATE INDEX IF NOT EXISTS idx_session_participants_token - ON session_participants(session_token); - -COMMENT ON TABLE session_participants IS 'Ephemeral identities for anonymous workflow channel visitors.'; diff --git a/server/database/migrations/006_knowledge.sql b/server/database/migrations/006_knowledge.sql deleted file mode 100644 index 20d5f98..0000000 --- a/server/database/migrations/006_knowledge.sql +++ /dev/null @@ -1,114 +0,0 @@ --- ========================================== --- Chat Switchboard — 006 Knowledge Bases --- ========================================== --- Knowledge bases, documents, chunks, and bindings to channels/personas. --- ICD §5 (Knowledge Bases) --- ========================================== - --- ========================================= --- KNOWLEDGE BASES --- ========================================= - -CREATE TABLE IF NOT EXISTS knowledge_bases ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - scope TEXT NOT NULL DEFAULT 'global', - owner_id UUID REFERENCES users(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - embedding_config JSONB NOT NULL DEFAULT '{}', - document_count INT NOT NULL DEFAULT 0, - chunk_count INT NOT NULL DEFAULT 0, - total_bytes BIGINT NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'active', - discoverable BOOLEAN NOT NULL DEFAULT true, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - CONSTRAINT kb_scope_check CHECK ( - (scope = 'global' AND owner_id IS NULL) OR - (scope = 'team' AND team_id IS NOT NULL) OR - (scope = 'personal' AND owner_id IS NOT NULL) - ) -); - -CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope); -CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id) WHERE owner_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id) WHERE team_id IS NOT NULL; - -COMMENT ON COLUMN knowledge_bases.discoverable IS 'false = hidden from user KB lists, only accessible through persona binding'; - - --- ========================================= --- KB DOCUMENTS --- ========================================= - -CREATE TABLE IF NOT EXISTS kb_documents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - filename TEXT NOT NULL, - content_type TEXT NOT NULL, - size_bytes BIGINT NOT NULL, - storage_key TEXT NOT NULL, - extracted_text TEXT, - chunk_count INT NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'pending', - error TEXT, - uploaded_by UUID NOT NULL REFERENCES users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id); - - --- ========================================= --- KB CHUNKS --- ========================================= - -CREATE TABLE IF NOT EXISTS kb_chunks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - document_id UUID NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE, - chunk_index INT NOT NULL, - content TEXT NOT NULL, - token_count INT NOT NULL DEFAULT 0, - embedding VECTOR(3072), - metadata JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id); -CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id); - - --- ========================================= --- CHANNEL ↔ KB BINDING --- ========================================= - -CREATE TABLE IF NOT EXISTS channel_knowledge_bases ( - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - enabled BOOLEAN NOT NULL DEFAULT true, - added_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (channel_id, kb_id) -); - - --- ========================================= --- PERSONA ↔ KB BINDING (v0.17.0) --- ========================================= - -CREATE TABLE IF NOT EXISTS persona_knowledge_bases ( - persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - auto_search BOOLEAN NOT NULL DEFAULT false, - added_at TIMESTAMPTZ NOT NULL DEFAULT now(), - PRIMARY KEY (persona_id, kb_id) -); - -CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id); -CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id); - -COMMENT ON TABLE persona_knowledge_bases IS 'Binds KBs to personas — the persona becomes a gateway to its KBs'; -COMMENT ON COLUMN persona_knowledge_bases.auto_search IS 'true = auto-prepend top-K results to context; false = kb_search tool only'; diff --git a/server/database/migrations/007_notes.sql b/server/database/migrations/007_notes.sql deleted file mode 100644 index 36c0e13..0000000 --- a/server/database/migrations/007_notes.sql +++ /dev/null @@ -1,75 +0,0 @@ --- ========================================== --- Chat Switchboard — 007 Notes & Links --- ========================================== --- Notes with full-text search, wikilink graph edges. --- ICD §6 (Notes) --- ========================================== - --- ========================================= --- NOTES --- ========================================= - -CREATE TABLE IF NOT EXISTS notes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(500) NOT NULL, - content TEXT DEFAULT '', - folder_path TEXT DEFAULT '/', - tags TEXT[] DEFAULT '{}', - metadata JSONB DEFAULT '{}', - source_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, - source_message_id UUID, - team_id UUID REFERENCES teams(id) ON DELETE SET NULL, - search_vector TSVECTOR, - embedding VECTOR(3072), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id); -CREATE INDEX IF NOT EXISTS idx_notes_search ON notes USING GIN(search_vector); -CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path); -CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes USING GIN(tags); -CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at DESC); -CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id) - WHERE source_message_id IS NOT NULL; - -CREATE OR REPLACE FUNCTION notes_search_update_fn() -RETURNS TRIGGER AS $$ -BEGIN - NEW.search_vector := - setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') || - setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B'); - NEW.updated_at := NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS notes_search_update ON notes; -CREATE TRIGGER notes_search_update - BEFORE INSERT OR UPDATE OF title, content ON notes - FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn(); - - --- ========================================= --- NOTE LINKS (v0.17.3 — wikilink graph) --- ========================================= - -CREATE TABLE IF NOT EXISTS note_links ( - source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE, - target_note_id UUID REFERENCES notes(id) ON DELETE CASCADE, - target_title TEXT NOT NULL, - display_text TEXT, - is_transclusion BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT now(), - PRIMARY KEY (source_note_id, target_title) -); - -CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id) - WHERE target_note_id IS NOT NULL; - -COMMENT ON TABLE note_links IS 'Directed links between notes, extracted from [[wikilink]] syntax on save'; -COMMENT ON COLUMN note_links.target_note_id IS 'NULL for unresolved links (target note does not exist yet)'; -COMMENT ON COLUMN note_links.target_title IS 'Raw [[title]] text — preserved for re-resolution after renames'; -COMMENT ON COLUMN note_links.is_transclusion IS 'true for ![[embed]] syntax, false for regular [[link]]'; diff --git a/server/database/migrations/008_memory.sql b/server/database/migrations/008_memory.sql deleted file mode 100644 index 34b8e4e..0000000 --- a/server/database/migrations/008_memory.sql +++ /dev/null @@ -1,89 +0,0 @@ --- ========================================== --- Chat Switchboard — 008 Memory --- ========================================== --- Long-term memory across conversations, extraction tracking. --- Consolidated v0.28.0: adds last_compacted_at + decay_rate. --- ========================================== - --- ========================================= --- MEMORIES --- ========================================= - -CREATE TABLE IF NOT EXISTS memories ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')), - owner_id UUID NOT NULL, - user_id UUID, - key TEXT NOT NULL, - value TEXT NOT NULL, - source_channel_id UUID, - confidence REAL NOT NULL DEFAULT 1.0, - status TEXT NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'pending_review', 'archived')), - embedding vector(3072), - - -- Compaction support (v0.28.0) - last_compacted_at TIMESTAMPTZ, - decay_rate REAL NOT NULL DEFAULT 0.0, - - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_memories_scope_owner - ON memories(scope, owner_id, status) - WHERE status = 'active'; - -CREATE INDEX IF NOT EXISTS idx_memories_persona_user - ON memories(owner_id, user_id) - WHERE scope = 'persona_user' AND status = 'active'; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key - ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key); - -CREATE INDEX IF NOT EXISTS idx_memories_fts - ON memories USING gin(to_tsvector('english', key || ' ' || value)); - -CREATE INDEX IF NOT EXISTS idx_memories_source_channel - ON memories(source_channel_id) - WHERE source_channel_id IS NOT NULL; - -COMMENT ON TABLE memories IS 'Long-term memory facts persisted across conversations'; -COMMENT ON COLUMN memories.scope IS 'user = personal facts; persona = shared; persona_user = per-user within a persona'; -COMMENT ON COLUMN memories.owner_id IS 'user_id for user scope, persona_id for persona/persona_user scopes'; -COMMENT ON COLUMN memories.confidence IS '0.0-1.0 confidence score — higher = more certain'; -COMMENT ON COLUMN memories.last_compacted_at IS 'Last time this memory was evaluated by the compaction process'; -COMMENT ON COLUMN memories.decay_rate IS 'Rate at which confidence decreases over time (0.0 = no decay)'; - -CREATE OR REPLACE FUNCTION update_memories_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = now(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS trg_memories_updated_at ON memories; -CREATE TRIGGER trg_memories_updated_at - BEFORE UPDATE ON memories - FOR EACH ROW EXECUTE FUNCTION update_memories_updated_at(); - - --- ========================================= --- MEMORY EXTRACTION LOG --- ========================================= - -CREATE TABLE IF NOT EXISTS memory_extraction_log ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL, - user_id UUID NOT NULL, - last_message_id UUID NOT NULL, - extracted_at TIMESTAMPTZ DEFAULT now(), - memory_count INT NOT NULL DEFAULT 0, - UNIQUE(channel_id, user_id) -); - -CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel - ON memory_extraction_log(channel_id); - -COMMENT ON TABLE memory_extraction_log IS 'Tracks extraction progress per channel+user to prevent re-processing'; diff --git a/server/database/migrations/009_workspaces.sql b/server/database/migrations/009_workspaces.sql deleted file mode 100644 index df003f8..0000000 --- a/server/database/migrations/009_workspaces.sql +++ /dev/null @@ -1,126 +0,0 @@ --- ========================================== --- Chat Switchboard — 009 Workspaces & Git --- ========================================== --- File workspaces, workspace file index, semantic chunks, git credentials. --- ICD §7 (Workspaces & Git) --- ========================================== - --- ========================================= --- WORKSPACES --- ========================================= - -CREATE TABLE IF NOT EXISTS workspaces ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - owner_type VARCHAR(20) NOT NULL - CHECK (owner_type IN ('user', 'project', 'channel', 'team')), - owner_id UUID NOT NULL, - name VARCHAR(200) NOT NULL, - root_path TEXT NOT NULL, - max_bytes BIGINT, - status VARCHAR(20) NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'archived', 'deleting')), - indexing_enabled BOOLEAN NOT NULL DEFAULT true, - -- Git integration (v0.21.4) - git_remote_url TEXT, - git_branch TEXT, - git_credential_id UUID, - git_last_sync TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id); -CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status) WHERE status != 'deleting'; -DROP TRIGGER IF EXISTS workspaces_updated_at ON workspaces; -CREATE TRIGGER workspaces_updated_at BEFORE UPDATE ON workspaces - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE workspaces IS 'File workspaces bound to users, projects, channels, or teams'; -COMMENT ON COLUMN workspaces.root_path IS 'PVC-relative path to workspace root directory'; - - --- ========================================= --- WORKSPACE FILES --- ========================================= - -CREATE TABLE IF NOT EXISTS workspace_files ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, - path TEXT NOT NULL, - is_directory BOOLEAN NOT NULL DEFAULT false, - content_type VARCHAR(100), - size_bytes BIGINT NOT NULL DEFAULT 0, - sha256 VARCHAR(64), - index_status VARCHAR(20) DEFAULT 'pending' - CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')), - chunk_count INT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path - ON workspace_files(workspace_id, path); -CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id); -CREATE INDEX IF NOT EXISTS idx_workspace_files_type - ON workspace_files(workspace_id, content_type) WHERE content_type IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status - ON workspace_files(workspace_id, index_status) - WHERE index_status IN ('pending', 'indexing'); - -DROP TRIGGER IF EXISTS workspace_files_updated_at ON workspace_files; -CREATE TRIGGER workspace_files_updated_at BEFORE UPDATE ON workspace_files - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE workspace_files IS 'Metadata index for workspace files — filesystem is source of truth'; - - --- ========================================= --- WORKSPACE CHUNKS (v0.21.2) --- ========================================= - -CREATE TABLE IF NOT EXISTS workspace_chunks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, - file_id UUID NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE, - chunk_index INT NOT NULL, - content TEXT NOT NULL, - token_count INT NOT NULL DEFAULT 0, - embedding vector(3072), - metadata JSONB DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id); -CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id); - -COMMENT ON TABLE workspace_chunks IS 'Text chunks with vector embeddings for workspace file semantic search'; - - --- ========================================= --- GIT CREDENTIALS (v0.21.4) --- ========================================= - -CREATE TABLE IF NOT EXISTS git_credentials ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name TEXT NOT NULL DEFAULT '', - auth_type TEXT NOT NULL DEFAULT 'https_pat', - encrypted_data BYTEA NOT NULL DEFAULT '', - nonce BYTEA NOT NULL DEFAULT '', - public_key TEXT NOT NULL DEFAULT '', - fingerprint TEXT NOT NULL DEFAULT '', - persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id); - --- Deferred FK: workspaces.git_credential_id → git_credentials.id -DO $$ BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_workspaces_git_credential' - ) THEN - ALTER TABLE workspaces ADD CONSTRAINT fk_workspaces_git_credential - FOREIGN KEY (git_credential_id) REFERENCES git_credentials(id) ON DELETE SET NULL; - END IF; -END $$; diff --git a/server/database/migrations/010_projects.sql b/server/database/migrations/010_projects.sql deleted file mode 100644 index 55430e8..0000000 --- a/server/database/migrations/010_projects.sql +++ /dev/null @@ -1,152 +0,0 @@ --- ========================================== --- Chat Switchboard — 010 Projects --- ========================================== --- Projects, junction tables, and cross-domain FK columns on --- channels/projects for workspace/project bindings. --- ICD §8 (Projects) --- ========================================== - --- ========================================= --- PROJECTS --- ========================================= - -CREATE TABLE IF NOT EXISTS projects ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(200) NOT NULL, - description TEXT, - color VARCHAR(7), - icon VARCHAR(50), - scope VARCHAR(20) NOT NULL DEFAULT 'personal' - CHECK (scope IN ('personal', 'team', 'global')), - owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE SET NULL, - workspace_id UUID REFERENCES workspaces(id) ON DELETE SET NULL, - is_archived BOOLEAN NOT NULL DEFAULT false, - settings JSONB DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id); -CREATE INDEX IF NOT EXISTS idx_projects_team ON projects(team_id) WHERE team_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope); -CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id) WHERE workspace_id IS NOT NULL; - -DROP TRIGGER IF EXISTS projects_updated_at ON projects; -CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE projects IS 'Organizes channels, KBs, and notes into named workspaces'; -COMMENT ON COLUMN projects.scope IS 'personal=owner only, team=team members, global=all users'; - - --- ========================================= --- PROJECT ↔ CHANNEL JUNCTION --- ========================================= - -CREATE TABLE IF NOT EXISTS project_channels ( - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - folder TEXT, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (project_id, channel_id), - UNIQUE (channel_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_channels_project ON project_channels(project_id); -CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id); - -COMMENT ON TABLE project_channels IS 'Links channels to projects. UNIQUE(channel_id) enforces one-project-per-channel.'; - - --- ========================================= --- PROJECT ↔ KB JUNCTION --- ========================================= - -CREATE TABLE IF NOT EXISTS project_knowledge_bases ( - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - auto_search BOOLEAN NOT NULL DEFAULT false, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (project_id, kb_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_kb_project ON project_knowledge_bases(project_id); -CREATE INDEX IF NOT EXISTS idx_project_kb_kb ON project_knowledge_bases(kb_id); - - --- ========================================= --- PROJECT ↔ NOTE JUNCTION --- ========================================= - -CREATE TABLE IF NOT EXISTS project_notes ( - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (project_id, note_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id); -CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id); - - --- ========================================= --- CROSS-DOMAIN FK COLUMNS --- ========================================= --- Deferred FKs: channels.project_id and workspace_id declared in 005 --- without FK constraints (referenced tables didn't exist yet). - -DO $$ BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_project' - ) THEN - ALTER TABLE channels ADD CONSTRAINT fk_channels_project - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL; - END IF; -END $$; - -DO $$ BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workspace' - ) THEN - ALTER TABLE channels ADD CONSTRAINT fk_channels_workspace - FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL; - END IF; -END $$; - - --- ========================================= --- FOLDER → PROJECT MIGRATION (idempotent) --- ========================================= --- Best-effort migration of legacy folder values into projects. --- Only runs if there are channels with a folder value and no project_id. - -DO $$ -DECLARE - _folder TEXT; - _user_id UUID; - _proj_id UUID; - _ch_id UUID; -BEGIN - FOR _user_id, _folder IN - SELECT DISTINCT user_id, folder FROM channels - WHERE folder IS NOT NULL AND folder != '' AND project_id IS NULL - LOOP - INSERT INTO projects (name, scope, owner_id) - VALUES (_folder, 'personal', _user_id) - RETURNING id INTO _proj_id; - - FOR _ch_id IN - SELECT id FROM channels - WHERE user_id = _user_id AND folder = _folder AND project_id IS NULL - LOOP - INSERT INTO project_channels (project_id, channel_id, position) - VALUES (_proj_id, _ch_id, 0) - ON CONFLICT DO NOTHING; - - UPDATE channels SET project_id = _proj_id WHERE id = _ch_id; - END LOOP; - END LOOP; -END -$$; diff --git a/server/database/migrations/012_package_user_settings.sql b/server/database/migrations/012_package_user_settings.sql deleted file mode 100644 index 2f687b6..0000000 --- a/server/database/migrations/012_package_user_settings.sql +++ /dev/null @@ -1,8 +0,0 @@ --- ========================================== --- Chat Switchboard — 012 (placeholder) --- ========================================== --- Previously: extensions + extension_user_settings tables. --- v0.28.7: Both tables removed. Unified package registry in 016. --- package_user_settings also in 016 (FK dependency on packages). --- This file is intentionally empty to preserve migration numbering. --- ========================================== diff --git a/server/database/migrations/013_files.sql b/server/database/migrations/013_files.sql deleted file mode 100644 index b8d6b9f..0000000 --- a/server/database/migrations/013_files.sql +++ /dev/null @@ -1,33 +0,0 @@ --- ========================================== --- Chat Switchboard — 013 Files --- ========================================== --- Unified files table. Fresh install — no legacy attachments migration. --- Consolidated v0.28.0: removes attachments→files migration dance. --- ========================================== - -CREATE TABLE IF NOT EXISTS files ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, - message_id UUID REFERENCES messages(id) ON DELETE SET NULL, - user_id UUID REFERENCES users(id) ON DELETE SET NULL, - project_id UUID REFERENCES projects(id) ON DELETE SET NULL, - origin VARCHAR(20) NOT NULL DEFAULT 'user_upload' - CHECK (origin IN ('user_upload', 'tool_output', 'system')), - filename VARCHAR(255) NOT NULL, - content_type VARCHAR(127) NOT NULL DEFAULT 'application/octet-stream', - size_bytes BIGINT NOT NULL DEFAULT 0, - storage_key TEXT NOT NULL, - display_hint VARCHAR(20) NOT NULL DEFAULT 'download' - CHECK (display_hint IN ('inline', 'download', 'thumbnail')), - extracted_text TEXT, - metadata JSONB DEFAULT '{}'::jsonb, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id); -CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id); -CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id); -CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id) WHERE project_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin); -CREATE INDEX IF NOT EXISTS idx_files_orphan ON files(created_at) WHERE message_id IS NULL AND origin = 'user_upload'; diff --git a/server/database/migrations/014_audit_usage.sql b/server/database/migrations/014_audit_usage.sql deleted file mode 100644 index cd0dc03..0000000 --- a/server/database/migrations/014_audit_usage.sql +++ /dev/null @@ -1,58 +0,0 @@ --- ========================================== --- Chat Switchboard — 014 Audit & Usage --- ========================================== --- Audit trail and usage/cost tracking. --- ICD §16 (Platform Administration), §17 (Export & Utility) --- ========================================== - --- ========================================= --- AUDIT LOG --- ========================================= - -CREATE TABLE IF NOT EXISTS audit_log ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - actor_id UUID REFERENCES users(id) ON DELETE SET NULL, - action VARCHAR(100) NOT NULL, - resource_type VARCHAR(50) NOT NULL, - resource_id VARCHAR(255), - metadata JSONB DEFAULT '{}'::jsonb, - ip_address VARCHAR(45), - user_agent TEXT DEFAULT '', - created_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC); -CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id); -CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action); - -COMMENT ON TABLE audit_log IS 'Immutable audit trail of all mutating operations'; - - --- ========================================= --- USAGE LOG --- ========================================= - -CREATE TABLE IF NOT EXISTS usage_log ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - provider_scope TEXT NOT NULL DEFAULT 'global', - model_id TEXT NOT NULL, - role TEXT, - prompt_tokens INT NOT NULL DEFAULT 0, - completion_tokens INT NOT NULL DEFAULT 0, - cache_creation_tokens INT NOT NULL DEFAULT 0, - cache_read_tokens INT NOT NULL DEFAULT 0, - cost_input NUMERIC(12,6), - cost_output NUMERIC(12,6), - routing_decision JSONB, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at); -CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope); diff --git a/server/database/migrations/015_health.sql b/server/database/migrations/015_health.sql deleted file mode 100644 index e4cd646..0000000 --- a/server/database/migrations/015_health.sql +++ /dev/null @@ -1,23 +0,0 @@ --- ========================================== --- Chat Switchboard — 015 Tool Health --- ========================================== --- Health tracking for built-in tools (web_search, url_fetch, etc.). --- ========================================== - -CREATE TABLE IF NOT EXISTS tool_health ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tool_name TEXT NOT NULL, - window_start TIMESTAMPTZ NOT NULL, - request_count INT NOT NULL DEFAULT 0, - error_count INT NOT NULL DEFAULT 0, - total_latency_ms BIGINT NOT NULL DEFAULT 0, - max_latency_ms INT NOT NULL DEFAULT 0, - last_error TEXT, - last_error_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), - - UNIQUE (tool_name, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_tool_health_window - ON tool_health (tool_name, window_start DESC); diff --git a/server/database/migrations/016_packages.sql b/server/database/migrations/016_packages.sql deleted file mode 100644 index 81fbf66..0000000 --- a/server/database/migrations/016_packages.sql +++ /dev/null @@ -1,129 +0,0 @@ --- ========================================== --- Chat Switchboard — 016 Packages --- ========================================== --- Unified package registry. Replaces surface_registry (v0.25.0) --- and extensions (v0.11.0) tables. A package is a surface, an --- extension, or both. --- --- v0.28.7: packages table + package_user_settings in 012. --- v0.29.0: status column + extension_permissions table. --- v0.29.2: ext_data_tables catalog + platform read views. --- ========================================== - -CREATE TABLE IF NOT EXISTS packages ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - type TEXT NOT NULL DEFAULT 'surface' - CHECK (type IN ('surface', 'extension', 'full', 'workflow')), - version TEXT NOT NULL DEFAULT '0.0.0', - description TEXT NOT NULL DEFAULT '', - author TEXT NOT NULL DEFAULT '', - tier TEXT NOT NULL DEFAULT 'browser' - CHECK (tier IN ('browser', 'starlark', 'sidecar')), - is_system BOOLEAN NOT NULL DEFAULT false, - scope TEXT NOT NULL DEFAULT 'global' - CHECK (scope IN ('global', 'team', 'personal')), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - installed_by UUID REFERENCES users(id) ON DELETE SET NULL, - manifest JSONB NOT NULL DEFAULT '{}', - enabled BOOLEAN NOT NULL DEFAULT true, - status TEXT NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'pending_review', 'suspended')), - schema_version INTEGER NOT NULL DEFAULT 0, - package_settings JSONB NOT NULL DEFAULT '{}', - source TEXT NOT NULL DEFAULT 'core' - CHECK (source IN ('core', 'builtin', 'extension', 'registry')), - installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type); -CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true; -CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id); -CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source); -CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status); - -COMMENT ON TABLE packages IS 'Unified package registry. Surfaces, extensions, and full packages. Replaces surface_registry + extensions tables.'; -COMMENT ON COLUMN packages.id IS 'Slug identifier from manifest "id" field. Used in URLs: /s/:id'; -COMMENT ON COLUMN packages.type IS 'surface = routable page, extension = hooks/tools/pipes, full = both, workflow = bundled workflow definition'; -COMMENT ON COLUMN packages.source IS 'core = page-engine seeded, builtin = extensions/builtin/ seeded, extension = admin-uploaded .pkg'; -COMMENT ON COLUMN packages.enabled IS 'Admin toggle — disabled surfaces redirect to / and hide from nav'; -COMMENT ON COLUMN packages.status IS 'Lifecycle: active (running), pending_review (needs admin permission grant), suspended (permission revoked)'; - - --- ========================================= --- PACKAGE USER SETTINGS --- ========================================= --- Per-user overrides for packages (enable/disable, custom config). --- Replaces extension_user_settings from the former extensions schema. --- Lives in 016 (not 012) because of FK dependency on packages table. - -CREATE TABLE IF NOT EXISTS package_user_settings ( - package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - settings JSONB NOT NULL DEFAULT '{}', - is_enabled BOOLEAN NOT NULL DEFAULT true, - PRIMARY KEY (package_id, user_id) -); - - --- ========================================= --- EXTENSION PERMISSIONS --- ========================================= --- Declared capabilities from package manifests. Admin grants control --- which modules the Starlark sandbox injects at runtime. --- v0.29.0: Permission model for extension sandboxing. - -CREATE TABLE IF NOT EXISTS extension_permissions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, - permission TEXT NOT NULL, - granted BOOLEAN NOT NULL DEFAULT false, - granted_by UUID REFERENCES users(id) ON DELETE SET NULL, - granted_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(package_id, permission) -); - -CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id); -CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true; - -COMMENT ON TABLE extension_permissions IS 'Declared permissions from package manifests. Admin grants control runtime module injection.'; -COMMENT ON COLUMN extension_permissions.permission IS 'Capability key: secrets.read, notifications.send, filters.pre_completion, db.read, db.write, api.http'; -COMMENT ON COLUMN extension_permissions.granted IS 'Admin has reviewed and approved this capability for the package'; - - --- ========================================= --- EXTENSION DATA TABLES CATALOG --- ========================================= --- Tracks namespaced tables created for each extension on install. --- Used by uninstall hooks (DROP TABLE) and db.list_tables(). --- Table names stored without the ext_{id}_ prefix. --- v0.29.2: Extension data storage. - -CREATE TABLE IF NOT EXISTS ext_data_tables ( - package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, - table_name TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (package_id, table_name) -); - -CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id); - -COMMENT ON TABLE ext_data_tables IS 'Catalog of namespaced tables created by extension install hooks. Drives uninstall cleanup and db.list_tables().'; -COMMENT ON COLUMN ext_data_tables.table_name IS 'Logical name (without ext_{id}_ prefix). Physical table is ext_{package_id}_{table_name}.'; - - --- ========================================= --- PLATFORM VIEWS (extension read access) --- ========================================= --- Column-allowlisted views over core platform tables. --- Extensions query these via db.view("users"), db.view("channels"). --- Only safe, non-sensitive columns are exposed. --- v0.29.2: Read contract for cross-platform data access. - -CREATE OR REPLACE VIEW ext_view_users AS - SELECT id, display_name, email FROM users; - -CREATE OR REPLACE VIEW ext_view_channels AS - SELECT id, title, type, team_id FROM channels; diff --git a/server/database/migrations/017_auth.sql b/server/database/migrations/017_auth.sql deleted file mode 100644 index db858e0..0000000 --- a/server/database/migrations/017_auth.sql +++ /dev/null @@ -1,18 +0,0 @@ --- ========================================== --- Chat Switchboard — 017 Auth Providers --- ========================================== --- OIDC authorization state tracking (ephemeral). --- Consolidated v0.28.0: renumbered from 019. groups.source moved to 002. --- ========================================== - -CREATE TABLE IF NOT EXISTS oidc_auth_state ( - state TEXT PRIMARY KEY, - nonce TEXT NOT NULL, - redirect_to TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_oidc_state_created - ON oidc_auth_state(created_at); - -COMMENT ON TABLE oidc_auth_state IS 'Ephemeral OIDC authorization code flow state. Rows deleted after callback.'; diff --git a/server/database/migrations/018_workflows.sql b/server/database/migrations/018_workflows.sql deleted file mode 100644 index 7b29a5b..0000000 --- a/server/database/migrations/018_workflows.sql +++ /dev/null @@ -1,149 +0,0 @@ --- ========================================== --- Chat Switchboard — 018 Workflows --- ========================================== --- Workflow definitions, stages, version snapshots, assignments. --- Consolidated v0.28.0: merges 023 + 025 (assignments) + 027 (webhooks). --- Also adds deferred FK: channels.workflow_id → workflows.id. --- ========================================== - --- ========================================= --- WORKFLOW DEFINITIONS --- ========================================= - -CREATE TABLE IF NOT EXISTS workflows ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - name TEXT NOT NULL, - slug TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - branding JSONB NOT NULL DEFAULT '{}', - entry_mode TEXT NOT NULL DEFAULT 'public_link' - CHECK (entry_mode IN ('public_link', 'team_only')), - is_active BOOLEAN NOT NULL DEFAULT false, - version INTEGER NOT NULL DEFAULT 1, - on_complete JSONB, - retention JSONB NOT NULL DEFAULT '{"mode": "archive"}', - - -- Webhooks (v0.27.3) - webhook_url TEXT, - webhook_secret TEXT, - - created_by UUID NOT NULL REFERENCES users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug - ON workflows(team_id, slug) WHERE team_id IS NOT NULL; -CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug - ON workflows(slug) WHERE team_id IS NULL; -CREATE INDEX IF NOT EXISTS idx_workflows_active - ON workflows(is_active) WHERE is_active = true; - -DROP TRIGGER IF EXISTS workflows_updated_at ON workflows; -CREATE TRIGGER workflows_updated_at - BEFORE UPDATE ON workflows - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE workflows IS 'Team-owned or global workflow definitions with staged processes.'; -COMMENT ON COLUMN workflows.slug IS 'URL-safe identifier, unique within scope (team or global).'; -COMMENT ON COLUMN workflows.branding IS '{"accent_color": "#hex", "logo_url": "...", "tagline": "..."}'; -COMMENT ON COLUMN workflows.on_complete IS 'Chaining hook — triggers another workflow on completion.'; -COMMENT ON COLUMN workflows.retention IS '{"mode": "archive"|"delete", "delete_after_days": N}'; - - --- ========================================= --- WORKFLOW STAGES --- ========================================= - -CREATE TABLE IF NOT EXISTS workflow_stages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - ordinal INTEGER NOT NULL, - name TEXT NOT NULL, - persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, - assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL, - form_template JSONB NOT NULL DEFAULT '{}', - stage_mode TEXT NOT NULL DEFAULT 'chat_only' - CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')), - history_mode TEXT NOT NULL DEFAULT 'full' - CHECK (history_mode IN ('full', 'summary', 'fresh')), - auto_transition BOOLEAN NOT NULL DEFAULT false, - transition_rules JSONB NOT NULL DEFAULT '{}', - surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL, - sla_seconds INTEGER, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow - ON workflow_stages(workflow_id, ordinal); - -COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stage has a driving persona and optional human assignment.'; -COMMENT ON COLUMN workflow_stages.history_mode IS 'full=complete history, summary=utility-role summary, fresh=clean slate'; -COMMENT ON COLUMN workflow_stages.form_template IS 'JSON schema of fields the persona should collect from the visitor.'; -COMMENT ON COLUMN workflow_stages.surface_pkg_id IS 'Optional package that provides a custom stage surface. NULL = built-in surface based on stage_mode.'; -COMMENT ON COLUMN workflow_stages.sla_seconds IS 'SLA budget in seconds for this stage. NULL = no SLA.'; - - --- ========================================= --- WORKFLOW VERSIONS (immutable snapshots) --- ========================================= - -CREATE TABLE IF NOT EXISTS workflow_versions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, - version_number INTEGER NOT NULL, - snapshot JSONB NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - UNIQUE (workflow_id, version_number) -); - -CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow - ON workflow_versions(workflow_id, version_number DESC); - -COMMENT ON TABLE workflow_versions IS 'Immutable snapshots of workflow definitions. Active instances run against a pinned version.'; -COMMENT ON COLUMN workflow_versions.snapshot IS 'Full JSON: definition + stages + persona tool grants at publish time.'; - - --- ========================================= --- WORKFLOW ASSIGNMENTS --- ========================================= - -CREATE TABLE IF NOT EXISTS workflow_assignments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - stage INTEGER NOT NULL, - team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, - assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, - status TEXT NOT NULL DEFAULT 'unassigned' - CHECK (status IN ('unassigned', 'claimed', 'completed')), - review_comments JSONB NOT NULL DEFAULT '[]', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - claimed_at TIMESTAMPTZ, - completed_at TIMESTAMPTZ -); - -CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel - ON workflow_assignments(channel_id, stage); -CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status - ON workflow_assignments(team_id, status) WHERE status = 'unassigned'; -CREATE INDEX IF NOT EXISTS idx_workflow_assignments_user - ON workflow_assignments(assigned_to) WHERE assigned_to IS NOT NULL; - -COMMENT ON TABLE workflow_assignments IS 'Queue entries for human review stages in workflows.'; -COMMENT ON COLUMN workflow_assignments.status IS 'unassigned=waiting, claimed=being worked, completed=done'; - - --- ========================================= --- DEFERRED FK: channels.workflow_id → workflows --- ========================================= - -DO $$ BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workflow' - ) THEN - ALTER TABLE channels ADD CONSTRAINT fk_channels_workflow - FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL; - END IF; -END $$; diff --git a/server/database/migrations/020_ha.sql b/server/database/migrations/020_ha.sql deleted file mode 100644 index 54fc90d..0000000 --- a/server/database/migrations/020_ha.sql +++ /dev/null @@ -1,37 +0,0 @@ --- ========================================== --- Chat Switchboard — 020 Multi-Replica HA --- ========================================== --- Shared state tables for multi-replica operation. --- v0.32.0: ws_tickets, rate_limit_counters. --- ========================================== - --- ========================================= --- WEBSOCKET TICKETS (replaces in-memory sync.Map) --- ========================================= --- Short-lived, single-use tokens for WebSocket authentication. --- Ticket issued on pod-1 must validate on pod-2. - -CREATE TABLE IF NOT EXISTS ws_tickets ( - id TEXT PRIMARY KEY, - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - expires_at TIMESTAMPTZ NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires - ON ws_tickets (expires_at); - --- ========================================= --- RATE LIMIT COUNTERS (replaces in-memory token bucket) --- ========================================= --- Fixed-window counters keyed by scope:identifier and time bucket. --- Key format: "{scope}:{identifier}" e.g. "auth:192.168.1.1" - -CREATE TABLE IF NOT EXISTS rate_limit_counters ( - key TEXT NOT NULL, - window_start TIMESTAMPTZ NOT NULL, - tokens REAL NOT NULL DEFAULT 0, - PRIMARY KEY (key, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window - ON rate_limit_counters (window_start); diff --git a/server/database/migrations/021_export.sql b/server/database/migrations/021_export.sql deleted file mode 100644 index b0d83ed..0000000 --- a/server/database/migrations/021_export.sql +++ /dev/null @@ -1,18 +0,0 @@ --- ========================================== --- Chat Switchboard — 021 Data Portability --- ========================================== --- v0.34.0: Indexes for data export and GDPR operations. --- ========================================== - --- Partial index for batch message export (only non-deleted) -CREATE INDEX IF NOT EXISTS idx_messages_channel_active - ON messages (channel_id, created_at) - WHERE deleted_at IS NULL; - --- Index for user-scoped channel listing (export) -CREATE INDEX IF NOT EXISTS idx_channels_user_id - ON channels (user_id); - --- Index for file lookup by user (export) -CREATE INDEX IF NOT EXISTS idx_files_user_id - ON files (user_id); diff --git a/server/database/migrations/022_ext_connections.sql b/server/database/migrations/022_ext_connections.sql deleted file mode 100644 index 82dce43..0000000 --- a/server/database/migrations/022_ext_connections.sql +++ /dev/null @@ -1,37 +0,0 @@ --- ========================================== --- Chat Switchboard — 022 Extension Connections --- ========================================== --- v0.38.1: Scoped credential management for extensions. --- Same scope/resolution pattern as provider_configs. --- ========================================== - -CREATE TABLE IF NOT EXISTS ext_connections ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - type TEXT NOT NULL, - package_id TEXT NOT NULL, - scope VARCHAR(10) NOT NULL - CHECK (scope IN ('global', 'team', 'personal')), - owner_id TEXT NOT NULL DEFAULT '', - name TEXT NOT NULL, - config JSONB NOT NULL DEFAULT '{}'::jsonb, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(type, scope, owner_id, name) -); - -CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type); -CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope); -CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != ''; -CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true; - -DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections; -CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -COMMENT ON TABLE ext_connections IS 'v0.38.1: Extension connection credentials (scoped, encrypted secrets in config JSON)'; -COMMENT ON COLUMN ext_connections.type IS 'Connection type identifier, shared across packages (e.g. "gitea", "github")'; -COMMENT ON COLUMN ext_connections.package_id IS 'Declaring package ID (UI attribution only, not access control)'; -COMMENT ON COLUMN ext_connections.scope IS 'global=admin-managed, team=team-scoped, personal=user-owned'; -COMMENT ON COLUMN ext_connections.owner_id IS 'Empty for global; team_id for team scope; user_id for personal scope'; -COMMENT ON COLUMN ext_connections.config IS 'JSON config blob; secret-type fields encrypted at rest by handler layer'; diff --git a/server/database/migrations/023_ext_dependencies.sql b/server/database/migrations/023_ext_dependencies.sql deleted file mode 100644 index 0d8c2f0..0000000 --- a/server/database/migrations/023_ext_dependencies.sql +++ /dev/null @@ -1,31 +0,0 @@ --- ========================================== --- Chat Switchboard — 023 Extension Dependencies --- ========================================== --- v0.38.2: Library package dependency tracking. --- Consumers declare which libraries they depend on. --- Libraries with active consumers cannot be uninstalled. --- ========================================== - --- Add 'library' to the packages type constraint. -ALTER TABLE packages DROP CONSTRAINT IF EXISTS packages_type_check; -ALTER TABLE packages ADD CONSTRAINT packages_type_check - CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')); - -CREATE TABLE IF NOT EXISTS ext_dependencies ( - consumer_id TEXT NOT NULL, - library_id TEXT NOT NULL, - version_spec TEXT NOT NULL DEFAULT '>=0.0.0', - resolved_ver TEXT NOT NULL DEFAULT '0.0.0', - PRIMARY KEY (consumer_id, library_id), - FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE, - FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT -); - -CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id); -CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id); - -COMMENT ON TABLE ext_dependencies IS 'v0.38.2: Package dependency graph — consumers → libraries'; -COMMENT ON COLUMN ext_dependencies.consumer_id IS 'Package that depends on the library'; -COMMENT ON COLUMN ext_dependencies.library_id IS 'Library package being depended upon'; -COMMENT ON COLUMN ext_dependencies.version_spec IS 'Semver version spec declared by consumer (e.g. ">=1.0.0")'; -COMMENT ON COLUMN ext_dependencies.resolved_ver IS 'Actual library version at dependency creation time'; diff --git a/server/database/migrations/001_core.sql b/server/database/migrations/postgres/001_core.sql similarity index 55% rename from server/database/migrations/001_core.sql rename to server/database/migrations/postgres/001_core.sql index a346f42..e96f418 100644 --- a/server/database/migrations/001_core.sql +++ b/server/database/migrations/postgres/001_core.sql @@ -1,17 +1,10 @@ -- ========================================== --- Chat Switchboard — 001 Core +-- Switchboard Core — 001 Core -- ========================================== --- Users (with auth fields), refresh tokens, platform policies, --- global settings, user presence. --- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence). +-- Users, auth, platform policies, global settings, presence, OIDC. -- ========================================== --- ── Extensions ────────────────────────────── - -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid() -CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector (KB embeddings) - --- ── Utility: auto-update updated_at ───────── +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ @@ -21,47 +14,37 @@ BEGIN END; $$ LANGUAGE plpgsql; - --- ========================================= --- USERS --- ========================================= +-- ── Users ─────────────────────────────────── CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(50) NOT NULL, email VARCHAR(255) NOT NULL, - password_hash TEXT, -- nullable for mTLS/OIDC + password_hash TEXT, display_name VARCHAR(100), avatar_url TEXT, role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin')), is_active BOOLEAN DEFAULT true, settings JSONB DEFAULT '{}'::jsonb, - - -- Auth abstraction (v0.24.0) auth_source VARCHAR(20) NOT NULL DEFAULT 'builtin' CHECK (auth_source IN ('builtin', 'mtls', 'oidc')), external_id TEXT, handle VARCHAR(100) NOT NULL, - - -- Vault: per-user encryption key (UEK) for BYOK API keys encrypted_uek BYTEA, uek_salt BYTEA, uek_nonce BYTEA, vault_set BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), last_login_at TIMESTAMPTZ ); --- Case-insensitive unique indexes CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (LOWER(username)); CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (LOWER(email)); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(LOWER(handle)); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id ON users(auth_source, external_id) WHERE external_id IS NOT NULL; - CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); @@ -69,18 +52,7 @@ DROP TRIGGER IF EXISTS users_updated_at ON users; CREATE TRIGGER users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -COMMENT ON COLUMN users.auth_source IS 'builtin=password, mtls=cert, oidc=SSO'; -COMMENT ON COLUMN users.external_id IS 'IdP subject claim (OIDC) or cert fingerprint (mTLS)'; -COMMENT ON COLUMN users.handle IS 'Unique @mention handle, auto-generated from username'; -COMMENT ON COLUMN users.encrypted_uek IS 'UEK encrypted with Argon2id(password)-derived key'; -COMMENT ON COLUMN users.uek_salt IS 'Argon2id salt for UEK derivation'; -COMMENT ON COLUMN users.uek_nonce IS 'AES-GCM nonce for UEK wrapping'; -COMMENT ON COLUMN users.vault_set IS 'true once UEK has been generated and wrapped'; - - --- ========================================= --- AUTH — REFRESH TOKENS --- ========================================= +-- ── Refresh Tokens ────────────────────────── CREATE TABLE IF NOT EXISTS refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -94,10 +66,7 @@ CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL; - --- ========================================= --- PLATFORM POLICIES --- ========================================= +-- ── Platform Policies ─────────────────────── CREATE TABLE IF NOT EXISTS platform_policies ( key VARCHAR(50) PRIMARY KEY, @@ -107,21 +76,11 @@ CREATE TABLE IF NOT EXISTS platform_policies ( ); INSERT INTO platform_policies (key, value) VALUES - ('allow_user_byok', 'false'), - ('allow_user_personas', 'false'), - ('allow_raw_model_access', 'true'), - ('allow_registration', 'true'), - ('default_user_active', 'false'), - ('allow_team_providers', 'true'), - ('kb_direct_access', 'true') + ('allow_registration', 'true'), + ('default_user_active', 'false') ON CONFLICT (key) DO NOTHING; -COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior'; - - --- ========================================= --- GLOBAL SETTINGS --- ========================================= +-- ── Global Settings ───────────────────────── CREATE TABLE IF NOT EXISTS global_settings ( key VARCHAR(100) PRIMARY KEY, @@ -132,20 +91,11 @@ CREATE TABLE IF NOT EXISTS global_settings ( INSERT INTO global_settings (key, value) VALUES ('registration', '{"enabled": true}'::jsonb), - ('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb), - ('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb), - ('model_roles', '{ - "utility": { "primary": null, "fallback": null }, - "embedding": { "primary": null, "fallback": null }, - "generation": { "primary": null, "fallback": null } - }'::jsonb), - ('retention_ttl_days', '{"value": 0}'::jsonb) + ('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'::jsonb), + ('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb) ON CONFLICT (key) DO NOTHING; - --- ========================================= --- USER PRESENCE (v0.23.1) --- ========================================= +-- ── User Presence ─────────────────────────── CREATE TABLE IF NOT EXISTS user_presence ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, @@ -156,4 +106,13 @@ CREATE TABLE IF NOT EXISTS user_presence ( CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC); -COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.'; +-- ── OIDC Auth State ───────────────────────── + +CREATE TABLE IF NOT EXISTS oidc_auth_state ( + state TEXT PRIMARY KEY, + nonce TEXT NOT NULL, + redirect_to TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at); diff --git a/server/database/migrations/002_teams.sql b/server/database/migrations/postgres/002_teams.sql similarity index 66% rename from server/database/migrations/002_teams.sql rename to server/database/migrations/postgres/002_teams.sql index 759cd26..8dc6e9c 100644 --- a/server/database/migrations/002_teams.sql +++ b/server/database/migrations/postgres/002_teams.sql @@ -1,14 +1,6 @@ -- ========================================== --- Chat Switchboard — 002 Teams & Access Control +-- Switchboard Core — 002 Teams & Access Control -- ========================================== --- Teams, team membership, groups (with permissions, budgets, source), --- group membership, Everyone group seed. --- Consolidated v0.28.0: merges 002 + 019 (source) + 020 (permissions). --- ========================================== - --- ========================================= --- TEAMS --- ========================================= CREATE TABLE IF NOT EXISTS teams ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -26,8 +18,6 @@ DROP TRIGGER IF EXISTS teams_updated_at ON teams; CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}'; - CREATE TABLE IF NOT EXISTS team_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, @@ -41,10 +31,7 @@ CREATE TABLE IF NOT EXISTS team_members ( CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id); CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id); - --- ========================================= --- GROUPS (ACLs + Permissions) --- ========================================= +-- ── Groups ────────────────────────────────── CREATE TABLE IF NOT EXISTS groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -53,19 +40,15 @@ CREATE TABLE IF NOT EXISTS groups ( scope VARCHAR(20) NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')), team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - created_by UUID REFERENCES users(id), -- nullable for system-seeded + created_by UUID REFERENCES users(id), source VARCHAR(20) NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'oidc', 'system')), - - -- Fine-grained permissions (v0.24.2) permissions JSONB NOT NULL DEFAULT '[]'::jsonb, token_budget_daily BIGINT, token_budget_monthly BIGINT, allowed_models JSONB, - created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), - CONSTRAINT groups_scope_team CHECK ( (scope = 'global' AND team_id IS NULL) OR (scope = 'team' AND team_id IS NOT NULL) @@ -74,19 +57,10 @@ CREATE TABLE IF NOT EXISTS groups ( CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000')); - DROP TRIGGER IF EXISTS groups_updated_at ON groups; CREATE TRIGGER groups_updated_at BEFORE UPDATE ON groups FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -COMMENT ON TABLE groups IS 'Access-control groups with permission grants. Decouple resource visibility from team membership.'; -COMMENT ON COLUMN groups.scope IS 'global: admin-managed. team: team-admin-managed.'; -COMMENT ON COLUMN groups.source IS 'manual=admin-created, oidc=synced from IdP, system=platform-seeded'; -COMMENT ON COLUMN groups.permissions IS 'Array of permission strings granted to group members'; -COMMENT ON COLUMN groups.token_budget_daily IS 'Daily token ceiling for members (NULL = unlimited)'; -COMMENT ON COLUMN groups.token_budget_monthly IS 'Monthly token ceiling for members (NULL = unlimited)'; -COMMENT ON COLUMN groups.allowed_models IS 'Array of model_id strings this group may use. NULL = unrestricted.'; - CREATE TABLE IF NOT EXISTS group_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, @@ -99,15 +73,13 @@ CREATE TABLE IF NOT EXISTS group_members ( CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id); CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id); --- Seed the Everyone group with a stable well-known ID. +-- Seed Everyone group INSERT INTO groups (id, name, description, scope, created_by, source, permissions) VALUES ( '00000000-0000-0000-0000-000000000001', 'Everyone', 'Implicit group — all authenticated users receive these permissions.', - 'global', - NULL, - 'system', - '["model.use","kb.read","channel.create"]'::jsonb + 'global', NULL, 'system', + '["extension.use","workflow.submit"]'::jsonb ) ON CONFLICT (id) DO NOTHING; diff --git a/server/database/migrations/postgres/003_packages.sql b/server/database/migrations/postgres/003_packages.sql new file mode 100644 index 0000000..ee0930f --- /dev/null +++ b/server/database/migrations/postgres/003_packages.sql @@ -0,0 +1,78 @@ +-- ========================================== +-- Switchboard Core — 003 Packages & Extensions +-- ========================================== + +CREATE TABLE IF NOT EXISTS packages ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'surface' + CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')), + version TEXT NOT NULL DEFAULT '0.0.0', + description TEXT NOT NULL DEFAULT '', + author TEXT NOT NULL DEFAULT '', + tier TEXT NOT NULL DEFAULT 'browser' + CHECK (tier IN ('browser', 'starlark', 'sidecar')), + is_system BOOLEAN NOT NULL DEFAULT false, + scope TEXT NOT NULL DEFAULT 'global' + CHECK (scope IN ('global', 'team', 'personal')), + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + installed_by UUID REFERENCES users(id) ON DELETE SET NULL, + manifest JSONB NOT NULL DEFAULT '{}', + enabled BOOLEAN NOT NULL DEFAULT true, + status TEXT NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'pending_review', 'suspended')), + schema_version INTEGER NOT NULL DEFAULT 0, + package_settings JSONB NOT NULL DEFAULT '{}', + source TEXT NOT NULL DEFAULT 'core' + CHECK (source IN ('core', 'builtin', 'extension', 'registry')), + installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type); +CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true; +CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id); +CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source); +CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status); + +-- ── Package User Settings ─────────────────── + +CREATE TABLE IF NOT EXISTS package_user_settings ( + package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + settings JSONB NOT NULL DEFAULT '{}', + is_enabled BOOLEAN NOT NULL DEFAULT true, + PRIMARY KEY (package_id, user_id) +); + +-- ── Extension Permissions ─────────────────── + +CREATE TABLE IF NOT EXISTS extension_permissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, + permission TEXT NOT NULL, + granted BOOLEAN NOT NULL DEFAULT false, + granted_by UUID REFERENCES users(id) ON DELETE SET NULL, + granted_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(package_id, permission) +); + +CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id); +CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true; + +-- ── Extension Data Tables Catalog ─────────── + +CREATE TABLE IF NOT EXISTS ext_data_tables ( + package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, + table_name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (package_id, table_name) +); + +CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id); + +-- ── Platform Read View ────────────────────── + +CREATE OR REPLACE VIEW ext_view_users AS + SELECT id, display_name, email FROM users; diff --git a/server/database/migrations/postgres/004_connections.sql b/server/database/migrations/postgres/004_connections.sql new file mode 100644 index 0000000..1697937 --- /dev/null +++ b/server/database/migrations/postgres/004_connections.sql @@ -0,0 +1,66 @@ +-- ========================================== +-- Switchboard Core — 004 Connections & Dependencies +-- ========================================== + +CREATE TABLE IF NOT EXISTS ext_connections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type TEXT NOT NULL, + package_id TEXT NOT NULL, + scope VARCHAR(10) NOT NULL + CHECK (scope IN ('global', 'team', 'personal')), + owner_id TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(type, scope, owner_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type); +CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope); +CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != ''; +CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true; + +DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections; +CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- ── Extension Dependencies ────────────────── + +CREATE TABLE IF NOT EXISTS ext_dependencies ( + consumer_id TEXT NOT NULL, + library_id TEXT NOT NULL, + version_spec TEXT NOT NULL DEFAULT '>=0.0.0', + resolved_ver TEXT NOT NULL DEFAULT '0.0.0', + PRIMARY KEY (consumer_id, library_id), + FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE, + FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT +); + +CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id); +CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id); + +-- ── Resource Grants ───────────────────────── + +CREATE TABLE IF NOT EXISTS resource_grants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + resource_type VARCHAR(30) NOT NULL, + resource_id UUID NOT NULL, + grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only' + CHECK (grant_scope IN ('team_only', 'global', 'groups')), + granted_groups UUID[] NOT NULL DEFAULT '{}', + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(resource_type, resource_id) +); + +CREATE INDEX IF NOT EXISTS idx_resource_grants_resource + ON resource_grants(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_resource_grants_groups + ON resource_grants USING gin(granted_groups); + +DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants; +CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); diff --git a/server/database/migrations/011_notifications.sql b/server/database/migrations/postgres/005_notifications.sql similarity index 61% rename from server/database/migrations/011_notifications.sql rename to server/database/migrations/postgres/005_notifications.sql index a8faa81..490066f 100644 --- a/server/database/migrations/011_notifications.sql +++ b/server/database/migrations/postgres/005_notifications.sql @@ -1,13 +1,6 @@ -- ========================================== --- Chat Switchboard — 011 Notifications +-- Switchboard Core — 005 Notifications -- ========================================== --- Notification delivery and per-user preferences. --- ICD §12 (Notifications) --- ========================================== - --- ========================================= --- NOTIFICATIONS --- ========================================= CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -21,13 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications ( created_at TIMESTAMPTZ DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at DESC); -CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at DESC); - - --- ========================================= --- NOTIFICATION PREFERENCES --- ========================================= +CREATE INDEX IF NOT EXISTS idx_notifications_user_unread + ON notifications(user_id, is_read, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_notifications_user_created + ON notifications(user_id, created_at DESC); CREATE TABLE IF NOT EXISTS notification_preferences ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/server/database/migrations/postgres/006_audit.sql b/server/database/migrations/postgres/006_audit.sql new file mode 100644 index 0000000..ec979f6 --- /dev/null +++ b/server/database/migrations/postgres/006_audit.sql @@ -0,0 +1,20 @@ +-- ========================================== +-- Switchboard Core — 006 Audit +-- ========================================== + +CREATE TABLE IF NOT EXISTS audit_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + actor_id UUID REFERENCES users(id) ON DELETE SET NULL, + action VARCHAR(100) NOT NULL, + resource_type VARCHAR(50) NOT NULL, + resource_id VARCHAR(255), + metadata JSONB DEFAULT '{}'::jsonb, + ip_address VARCHAR(45), + user_agent TEXT DEFAULT '', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action); diff --git a/server/database/migrations/postgres/007_workflows.sql b/server/database/migrations/postgres/007_workflows.sql new file mode 100644 index 0000000..cec47a5 --- /dev/null +++ b/server/database/migrations/postgres/007_workflows.sql @@ -0,0 +1,76 @@ +-- ========================================== +-- Switchboard Core — 007 Workflows +-- ========================================== +-- Workflow definitions, stages, version snapshots. +-- persona_id kept as nullable TEXT (no FK — personas are extensions now). +-- workflow_assignments dropped (channel-dependent, rebuild as needed). +-- ========================================== + +CREATE TABLE IF NOT EXISTS workflows ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + team_id UUID REFERENCES teams(id) ON DELETE CASCADE, + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + branding JSONB NOT NULL DEFAULT '{}', + entry_mode TEXT NOT NULL DEFAULT 'public_link' + CHECK (entry_mode IN ('public_link', 'team_only')), + is_active BOOLEAN NOT NULL DEFAULT false, + version INTEGER NOT NULL DEFAULT 1, + on_complete JSONB, + retention JSONB NOT NULL DEFAULT '{"mode": "archive"}', + webhook_url TEXT, + webhook_secret TEXT, + created_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug + ON workflows(team_id, slug) WHERE team_id IS NOT NULL; +CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug + ON workflows(slug) WHERE team_id IS NULL; +CREATE INDEX IF NOT EXISTS idx_workflows_active + ON workflows(is_active) WHERE is_active = true; + +DROP TRIGGER IF EXISTS workflows_updated_at ON workflows; +CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- ── Workflow Stages ───────────────────────── + +CREATE TABLE IF NOT EXISTS workflow_stages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, + ordinal INTEGER NOT NULL, + name TEXT NOT NULL, + persona_id TEXT, + assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL, + form_template JSONB NOT NULL DEFAULT '{}', + stage_mode TEXT NOT NULL DEFAULT 'form_only' + CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')), + history_mode TEXT NOT NULL DEFAULT 'full' + CHECK (history_mode IN ('full', 'summary', 'fresh')), + auto_transition BOOLEAN NOT NULL DEFAULT false, + transition_rules JSONB NOT NULL DEFAULT '{}', + surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL, + sla_seconds INTEGER, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow + ON workflow_stages(workflow_id, ordinal); + +-- ── Workflow Versions ─────────────────────── + +CREATE TABLE IF NOT EXISTS workflow_versions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, + version_number INTEGER NOT NULL, + snapshot JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (workflow_id, version_number) +); + +CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow + ON workflow_versions(workflow_id, version_number DESC); diff --git a/server/database/migrations/019_tasks.sql b/server/database/migrations/postgres/008_tasks.sql similarity index 55% rename from server/database/migrations/019_tasks.sql rename to server/database/migrations/postgres/008_tasks.sql index 409e852..e29073f 100644 --- a/server/database/migrations/019_tasks.sql +++ b/server/database/migrations/postgres/008_tasks.sql @@ -1,15 +1,11 @@ -- ========================================== --- Chat Switchboard — 019 Tasks +-- Switchboard Core — 008 Tasks -- ========================================== --- Task definitions and run history for autonomous agents. --- Consolidated v0.28.0: merges 026 + 027 (webhook_secret inline). --- v0.28.0-cs3: trigger_token, action task_type, queued run status. +-- Task scheduling and run history. +-- Stripped: persona_id (kept nullable, no FK), output_channel_id, +-- provider_config_id. Output modes: notification | webhook | log. -- ========================================== --- ========================================= --- TASK DEFINITIONS --- ========================================= - CREATE TABLE IF NOT EXISTS tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -18,44 +14,28 @@ CREATE TABLE IF NOT EXISTS tasks ( description TEXT DEFAULT '', scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'team', 'global')), - - -- What to run - task_type TEXT NOT NULL DEFAULT 'prompt' - CHECK (task_type IN ('prompt', 'workflow', 'action', 'system')), + task_type TEXT NOT NULL DEFAULT 'action' + CHECK (task_type IN ('action', 'workflow', 'system')), system_function TEXT DEFAULT '', - persona_id UUID REFERENCES personas(id) ON DELETE SET NULL, + persona_id TEXT, model_id TEXT, system_prompt TEXT DEFAULT '', user_prompt TEXT DEFAULT '', workflow_id UUID REFERENCES workflows(id) ON DELETE SET NULL, tool_grants JSONB, - - -- Schedule: cron expression, "once", or "webhook" schedule TEXT NOT NULL, timezone TEXT NOT NULL DEFAULT 'UTC', is_active BOOLEAN NOT NULL DEFAULT true, - - -- Webhook trigger (inbound) — token-based auth for /hooks/t/:token trigger_token TEXT UNIQUE, - - -- Execution policy max_tokens INTEGER NOT NULL DEFAULT 4096, max_tool_calls INTEGER NOT NULL DEFAULT 10, max_wall_clock INTEGER NOT NULL DEFAULT 300, - output_mode TEXT NOT NULL DEFAULT 'channel' - CHECK (output_mode IN ('channel', 'note', 'webhook')), - output_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, + output_mode TEXT NOT NULL DEFAULT 'log' + CHECK (output_mode IN ('notification', 'webhook', 'log')), webhook_url TEXT, webhook_secret TEXT, - - -- Provider routing - provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL, - - -- Notifications notify_on_complete BOOLEAN NOT NULL DEFAULT false, notify_on_failure BOOLEAN NOT NULL DEFAULT true, - - -- Bookkeeping last_run_at TIMESTAMPTZ, next_run_at TIMESTAMPTZ, run_count INTEGER NOT NULL DEFAULT 0, @@ -63,20 +43,17 @@ CREATE TABLE IF NOT EXISTS tasks ( updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = true; -CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id); -CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at) WHERE is_active = true; +CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id); +CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token + ON tasks(trigger_token) WHERE trigger_token IS NOT NULL; - --- ========================================= --- TASK RUN HISTORY --- ========================================= +-- ── Task Runs ─────────────────────────────── CREATE TABLE IF NOT EXISTS task_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')), trigger_payload TEXT, @@ -88,5 +65,5 @@ CREATE TABLE IF NOT EXISTS task_runs ( error TEXT ); -CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id); -CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status); +CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id); +CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status); diff --git a/server/database/migrations/postgres/009_ha.sql b/server/database/migrations/postgres/009_ha.sql new file mode 100644 index 0000000..7288df4 --- /dev/null +++ b/server/database/migrations/postgres/009_ha.sql @@ -0,0 +1,21 @@ +-- ========================================== +-- Switchboard Core — 009 Multi-Replica HA +-- ========================================== + +CREATE TABLE IF NOT EXISTS ws_tickets ( + id TEXT PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expires_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at); + +CREATE TABLE IF NOT EXISTS rate_limit_counters ( + key TEXT NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + tokens REAL NOT NULL DEFAULT 0, + PRIMARY KEY (key, window_start) +); + +CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window + ON rate_limit_counters(window_start); diff --git a/server/database/migrations/sqlite/001_core.sql b/server/database/migrations/sqlite/001_core.sql index c20c9a2..01055bf 100644 --- a/server/database/migrations/sqlite/001_core.sql +++ b/server/database/migrations/sqlite/001_core.sql @@ -1,7 +1,6 @@ -- ========================================== --- Chat Switchboard — 001 Core (SQLite) +-- Switchboard Core — 001 Core (SQLite) -- ========================================== --- Consolidated v0.28.0 PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; @@ -59,13 +58,8 @@ CREATE TABLE IF NOT EXISTS platform_policies ( ); INSERT OR IGNORE INTO platform_policies (key, value) VALUES - ('allow_user_byok', 'false'), - ('allow_user_personas', 'false'), - ('allow_raw_model_access', 'true'), - ('allow_registration', 'true'), - ('default_user_active', 'false'), - ('allow_team_providers', 'true'), - ('kb_direct_access', 'true'); + ('allow_registration', 'true'), + ('default_user_active', 'false'); CREATE TABLE IF NOT EXISTS global_settings ( key TEXT PRIMARY KEY, @@ -76,15 +70,23 @@ CREATE TABLE IF NOT EXISTS global_settings ( INSERT OR IGNORE INTO global_settings (key, value) VALUES ('registration', '{"enabled": true}'), - ('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'), - ('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'), - ('model_roles', '{"utility":{"primary":null,"fallback":null},"embedding":{"primary":null,"fallback":null},"generation":{"primary":null,"fallback":null}}'), - ('retention_ttl_days', '{"value": 0}'); + ('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'), + ('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'); CREATE TABLE IF NOT EXISTS user_presence ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_seen TEXT NOT NULL DEFAULT (datetime('now')), status TEXT NOT NULL DEFAULT 'online' + CHECK (status IN ('online', 'away', 'offline')) ); -CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC); +CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen); + +CREATE TABLE IF NOT EXISTS oidc_auth_state ( + state TEXT PRIMARY KEY, + nonce TEXT NOT NULL, + redirect_to TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at); diff --git a/server/database/migrations/sqlite/002_teams.sql b/server/database/migrations/sqlite/002_teams.sql index 1edd9e3..e10f3a6 100644 --- a/server/database/migrations/sqlite/002_teams.sql +++ b/server/database/migrations/sqlite/002_teams.sql @@ -1,11 +1,12 @@ --- Chat Switchboard — 002 Teams & Access Control (SQLite) --- Consolidated v0.28.0 +-- ========================================== +-- Switchboard Core — 002 Teams & Access Control (SQLite) +-- ========================================== CREATE TABLE IF NOT EXISTS teams ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT DEFAULT '', - created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + created_by TEXT NOT NULL REFERENCES users(id), is_active INTEGER DEFAULT 1, settings TEXT DEFAULT '{}', created_at TEXT DEFAULT (datetime('now')), @@ -14,6 +15,10 @@ CREATE TABLE IF NOT EXISTS teams ( CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active); +CREATE TRIGGER IF NOT EXISTS teams_updated_at AFTER UPDATE ON teams + FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at + BEGIN UPDATE teams SET updated_at = datetime('now') WHERE id = NEW.id; END; + CREATE TABLE IF NOT EXISTS team_members ( id TEXT PRIMARY KEY, team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE, @@ -43,7 +48,11 @@ CREATE TABLE IF NOT EXISTS groups ( ); CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope - ON groups(name, COALESCE(team_id, '')); + ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000')); + +CREATE TRIGGER IF NOT EXISTS groups_updated_at AFTER UPDATE ON groups + FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at + BEGIN UPDATE groups SET updated_at = datetime('now') WHERE id = NEW.id; END; CREATE TABLE IF NOT EXISTS group_members ( id TEXT PRIMARY KEY, @@ -57,13 +66,11 @@ CREATE TABLE IF NOT EXISTS group_members ( CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id); CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id); -INSERT OR IGNORE INTO groups - (id, name, description, scope, created_by, source, permissions, created_at, updated_at) +INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions) VALUES ( '00000000-0000-0000-0000-000000000001', 'Everyone', 'Implicit group — all authenticated users receive these permissions.', 'global', NULL, 'system', - '["model.use","kb.read","channel.create"]', - datetime('now'), datetime('now') + '["extension.use","workflow.submit"]' ); diff --git a/server/database/migrations/sqlite/016_packages.sql b/server/database/migrations/sqlite/003_packages.sql similarity index 76% rename from server/database/migrations/sqlite/016_packages.sql rename to server/database/migrations/sqlite/003_packages.sql index f1111fa..979e73a 100644 --- a/server/database/migrations/sqlite/016_packages.sql +++ b/server/database/migrations/sqlite/003_packages.sql @@ -1,14 +1,12 @@ --- Chat Switchboard — 016 Packages (SQLite) --- Unified package registry. Replaces surface_registry + extensions. --- v0.28.7: packages + package_user_settings --- v0.29.0: status column + extension_permissions table --- v0.29.2: ext_data_tables catalog + platform read views +-- ========================================== +-- Switchboard Core — 003 Packages & Extensions (SQLite) +-- ========================================== CREATE TABLE IF NOT EXISTS packages ( id TEXT PRIMARY KEY, title TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'surface' - CHECK (type IN ('surface', 'extension', 'full', 'workflow')), + CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')), version TEXT NOT NULL DEFAULT '0.0.0', description TEXT NOT NULL DEFAULT '', author TEXT NOT NULL DEFAULT '', @@ -37,20 +35,14 @@ CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id); CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source); CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status); - --- Per-user overrides for packages (enable/disable, custom config). - CREATE TABLE IF NOT EXISTS package_user_settings ( package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - settings TEXT NOT NULL DEFAULT '{}', + settings TEXT NOT NULL DEFAULT '{}', is_enabled INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (package_id, user_id) ); - --- Extension permissions (v0.29.0) - CREATE TABLE IF NOT EXISTS extension_permissions ( id TEXT PRIMARY KEY, package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, @@ -65,10 +57,6 @@ CREATE TABLE IF NOT EXISTS extension_permissions ( CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id); CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted); - --- Extension data tables catalog (v0.29.2) --- Tracks namespaced tables created by extension install hooks. - CREATE TABLE IF NOT EXISTS ext_data_tables ( package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, table_name TEXT NOT NULL, @@ -78,14 +66,5 @@ CREATE TABLE IF NOT EXISTS ext_data_tables ( CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id); - --- Platform views — column-allowlisted read access for extensions (v0.29.2) --- Extensions query these via db.view("users"), db.view("channels"). - -DROP VIEW IF EXISTS ext_view_users; -CREATE VIEW ext_view_users AS +CREATE VIEW IF NOT EXISTS ext_view_users AS SELECT id, display_name, email FROM users; - -DROP VIEW IF EXISTS ext_view_channels; -CREATE VIEW ext_view_channels AS - SELECT id, title, type, team_id FROM channels; diff --git a/server/database/migrations/sqlite/003_providers.sql b/server/database/migrations/sqlite/003_providers.sql deleted file mode 100644 index 4328830..0000000 --- a/server/database/migrations/sqlite/003_providers.sql +++ /dev/null @@ -1,110 +0,0 @@ --- Chat Switchboard — 003 Providers & Routing (SQLite) --- Consolidated v0.28.0: adds capability_match policy type - -CREATE TABLE IF NOT EXISTS provider_configs ( - id TEXT PRIMARY KEY, - scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')), - owner_id TEXT, - name TEXT NOT NULL, - provider TEXT NOT NULL, - endpoint TEXT NOT NULL, - api_key_enc BLOB, - key_nonce BLOB, - key_scope TEXT NOT NULL DEFAULT 'global', - model_default TEXT, - config TEXT DEFAULT '{}', - headers TEXT DEFAULT '{}', - settings TEXT DEFAULT '{}', - is_active INTEGER DEFAULT 1, - is_private INTEGER DEFAULT 0, - proxy_mode TEXT NOT NULL DEFAULT 'system' CHECK (proxy_mode IN ('system', 'direct', 'custom')), - proxy_url TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope); -CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id); -CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active); - -CREATE TABLE IF NOT EXISTS model_catalog ( - id TEXT PRIMARY KEY, - provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - display_name TEXT, - model_type TEXT DEFAULT 'chat', - capabilities TEXT NOT NULL DEFAULT '{}', - pricing TEXT, - visibility TEXT DEFAULT 'disabled' CHECK (visibility IN ('enabled', 'disabled', 'team')), - last_synced_at TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - UNIQUE(provider_config_id, model_id) -); - -CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id); -CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility); -CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type); - -CREATE TABLE IF NOT EXISTS model_pricing ( - id TEXT PRIMARY KEY, - provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - input_per_m REAL, - output_per_m REAL, - cache_create_per_m REAL, - cache_read_per_m REAL, - currency TEXT NOT NULL DEFAULT 'USD', - source TEXT NOT NULL DEFAULT 'manual', - updated_at TEXT DEFAULT (datetime('now')), - updated_by TEXT REFERENCES users(id), - UNIQUE(provider_config_id, model_id) -); - -CREATE TABLE IF NOT EXISTS provider_health ( - id TEXT PRIMARY KEY, - provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - window_start TEXT NOT NULL, - request_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - timeout_count INTEGER NOT NULL DEFAULT 0, - total_latency_ms INTEGER NOT NULL DEFAULT 0, - max_latency_ms INTEGER NOT NULL DEFAULT 0, - rate_limit_count INTEGER NOT NULL DEFAULT 0, - last_error TEXT, - last_error_at TEXT, - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (provider_config_id, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_provider_health_window - ON provider_health (provider_config_id, window_start DESC); - -CREATE TABLE IF NOT EXISTS capability_overrides ( - id TEXT PRIMARY KEY, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - field TEXT NOT NULL, - value TEXT NOT NULL, - set_by TEXT REFERENCES users(id) ON DELETE SET NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (provider_config_id, model_id, field) -); - -CREATE INDEX IF NOT EXISTS idx_capability_overrides_model ON capability_overrides(model_id); - -CREATE TABLE IF NOT EXISTS routing_policies ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - scope TEXT NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')), - team_id TEXT REFERENCES teams(id) ON DELETE CASCADE, - priority INTEGER NOT NULL DEFAULT 100, - policy_type TEXT NOT NULL - CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')), - config TEXT NOT NULL DEFAULT '{}', - is_active INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_routing_policies_active ON routing_policies(is_active, priority); diff --git a/server/database/migrations/sqlite/004_connections.sql b/server/database/migrations/sqlite/004_connections.sql new file mode 100644 index 0000000..802e0ce --- /dev/null +++ b/server/database/migrations/sqlite/004_connections.sql @@ -0,0 +1,59 @@ +-- ========================================== +-- Switchboard Core — 004 Connections & Dependencies (SQLite) +-- ========================================== + +CREATE TABLE IF NOT EXISTS ext_connections ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + package_id TEXT NOT NULL, + scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')), + owner_id TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL, + config TEXT NOT NULL DEFAULT '{}', + is_active INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + UNIQUE(type, scope, owner_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type); +CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope); +CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id); +CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active); + +CREATE TRIGGER IF NOT EXISTS ext_connections_updated_at AFTER UPDATE ON ext_connections + FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at + BEGIN UPDATE ext_connections SET updated_at = datetime('now') WHERE id = NEW.id; END; + +CREATE TABLE IF NOT EXISTS ext_dependencies ( + consumer_id TEXT NOT NULL, + library_id TEXT NOT NULL, + version_spec TEXT NOT NULL DEFAULT '>=0.0.0', + resolved_ver TEXT NOT NULL DEFAULT '0.0.0', + PRIMARY KEY (consumer_id, library_id), + FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE, + FOREIGN KEY (library_id) REFERENCES packages(id) +); + +CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id); +CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id); + +CREATE TABLE IF NOT EXISTS resource_grants ( + id TEXT PRIMARY KEY, + resource_type TEXT NOT NULL, + resource_id TEXT NOT NULL, + grant_scope TEXT NOT NULL DEFAULT 'team_only' + CHECK (grant_scope IN ('team_only', 'global', 'groups')), + granted_groups TEXT NOT NULL DEFAULT '[]', + created_by TEXT NOT NULL REFERENCES users(id), + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + UNIQUE(resource_type, resource_id) +); + +CREATE INDEX IF NOT EXISTS idx_resource_grants_resource + ON resource_grants(resource_type, resource_id); + +CREATE TRIGGER IF NOT EXISTS resource_grants_updated_at AFTER UPDATE ON resource_grants + FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at + BEGIN UPDATE resource_grants SET updated_at = datetime('now') WHERE id = NEW.id; END; diff --git a/server/database/migrations/sqlite/004_personas.sql b/server/database/migrations/sqlite/004_personas.sql deleted file mode 100644 index 5c947cc..0000000 --- a/server/database/migrations/sqlite/004_personas.sql +++ /dev/null @@ -1,83 +0,0 @@ --- Chat Switchboard — 004 Personas & Grants (SQLite) - -CREATE TABLE IF NOT EXISTS personas ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - handle TEXT, - description TEXT DEFAULT '', - icon TEXT DEFAULT '', - avatar TEXT DEFAULT '', - base_model_id TEXT NOT NULL, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, - system_prompt TEXT DEFAULT '', - temperature REAL, - max_tokens INTEGER, - thinking_budget INTEGER, - top_p REAL, - memory_enabled INTEGER NOT NULL DEFAULT 1, - memory_extraction_prompt TEXT, - scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')), - owner_id TEXT, - created_by TEXT NOT NULL REFERENCES users(id), - is_active INTEGER DEFAULT 1, - is_shared INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope); -CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id); -CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active); -CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL; - -CREATE TABLE IF NOT EXISTS persona_groups ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT DEFAULT '', - owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('global', 'team', 'personal')), - team_id TEXT REFERENCES teams(id) ON DELETE CASCADE, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id); - -CREATE TABLE IF NOT EXISTS persona_group_members ( - id TEXT PRIMARY KEY, - group_id TEXT NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE, - persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - is_leader INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - UNIQUE(group_id, persona_id) -); - -CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id); - -CREATE TABLE IF NOT EXISTS persona_grants ( - id TEXT PRIMARY KEY, - persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - grant_type TEXT NOT NULL, - grant_ref TEXT NOT NULL, - config TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')), - UNIQUE(persona_id, grant_type, grant_ref) -); - -CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id); -CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type); - -CREATE TABLE IF NOT EXISTS resource_grants ( - id TEXT PRIMARY KEY, - resource_type TEXT NOT NULL CHECK (resource_type IN ('persona', 'knowledge_base', 'project')), - resource_id TEXT NOT NULL, - grant_scope TEXT NOT NULL DEFAULT 'team_only' - CHECK (grant_scope IN ('team_only', 'global', 'groups')), - granted_groups TEXT NOT NULL DEFAULT '[]', - created_by TEXT NOT NULL REFERENCES users(id), - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - UNIQUE(resource_type, resource_id) -); - -CREATE INDEX IF NOT EXISTS idx_resource_grants_resource ON resource_grants(resource_type, resource_id); diff --git a/server/database/migrations/sqlite/005_channels.sql b/server/database/migrations/sqlite/005_channels.sql deleted file mode 100644 index 6456ef4..0000000 --- a/server/database/migrations/sqlite/005_channels.sql +++ /dev/null @@ -1,160 +0,0 @@ --- Chat Switchboard — 005 Channels & Conversations (SQLite) --- Consolidated v0.28.0 - -CREATE TABLE IF NOT EXISTS folders ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - parent_id TEXT REFERENCES folders(id) ON DELETE CASCADE, - sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - UNIQUE(user_id, name, parent_id) -); - -CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id); -CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id); - -CREATE TABLE IF NOT EXISTS channels ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users(id) ON DELETE CASCADE, - title TEXT NOT NULL, - description TEXT, - type TEXT DEFAULT 'direct' - CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service')), - model TEXT, - system_prompt TEXT, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, - is_archived INTEGER DEFAULT 0, - is_pinned INTEGER DEFAULT 0, - folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL, - folder TEXT, - team_id TEXT REFERENCES teams(id) ON DELETE SET NULL, - settings TEXT DEFAULT '{}', - tags TEXT DEFAULT '[]', - ai_mode TEXT NOT NULL DEFAULT 'auto', - topic TEXT, - allow_anonymous INTEGER NOT NULL DEFAULT 0, - workflow_id TEXT, - workflow_version INTEGER, - current_stage INTEGER DEFAULT 0, - stage_data TEXT DEFAULT '{}', - workflow_status TEXT DEFAULT 'active' - CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')), - last_activity_at TEXT DEFAULT (datetime('now')), - stage_entered_at TEXT, - kb_auto_inject INTEGER NOT NULL DEFAULT 0, - project_id TEXT, - workspace_id TEXT, - purge_after TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id); -CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at); -CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type); -CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id); -CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id); -CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id); -CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id); -CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id); - -CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')), - content TEXT NOT NULL, - model TEXT, - tokens_used INTEGER, - tool_calls TEXT, - metadata TEXT DEFAULT '{}', - parent_id TEXT REFERENCES messages(id) ON DELETE SET NULL, - sibling_index INTEGER DEFAULT 0, - participant_type TEXT DEFAULT 'user', - participant_id TEXT, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, - deleted_at TEXT, - created_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at); -CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id); - -CREATE TABLE IF NOT EXISTS channel_participants ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - participant_type TEXT NOT NULL DEFAULT 'user' - CHECK (participant_type IN ('user', 'persona', 'session')), - participant_id TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'member' - CHECK (role IN ('owner', 'member', 'observer')), - display_name TEXT, - avatar_url TEXT, - joined_at TEXT DEFAULT (datetime('now')), - last_read_at TEXT DEFAULT (datetime('now')), - last_read_message_id TEXT, - UNIQUE(channel_id, participant_type, participant_id) -); - -CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_id); - -CREATE TABLE IF NOT EXISTS channel_models ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, - persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL, - display_name TEXT, - system_prompt TEXT, - settings TEXT DEFAULT '{}', - is_default INTEGER DEFAULT 0, - added_at TEXT DEFAULT (datetime('now')) -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw - ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL; -CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona - ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id); - -CREATE TABLE IF NOT EXISTS channel_cursors ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - active_leaf_id TEXT REFERENCES messages(id) ON DELETE SET NULL, - updated_at TEXT DEFAULT (datetime('now')), - UNIQUE(channel_id, user_id) -); - -CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id); - -CREATE TABLE IF NOT EXISTS user_model_settings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - model_id TEXT NOT NULL, - provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE, - hidden INTEGER DEFAULT 0, - preferred_temperature REAL, - preferred_max_tokens INTEGER, - sort_order INTEGER DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - UNIQUE(user_id, model_id, provider_config_id) -); - -CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id); - -CREATE TABLE IF NOT EXISTS session_participants ( - id TEXT PRIMARY KEY, - session_token TEXT NOT NULL UNIQUE, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - display_name TEXT NOT NULL DEFAULT 'Visitor', - fingerprint TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_session_participants_channel ON session_participants(channel_id); -CREATE INDEX IF NOT EXISTS idx_session_participants_token ON session_participants(session_token); diff --git a/server/database/migrations/sqlite/011_notifications.sql b/server/database/migrations/sqlite/005_notifications.sql similarity index 69% rename from server/database/migrations/sqlite/011_notifications.sql rename to server/database/migrations/sqlite/005_notifications.sql index ca83549..8f23407 100644 --- a/server/database/migrations/sqlite/011_notifications.sql +++ b/server/database/migrations/sqlite/005_notifications.sql @@ -1,4 +1,6 @@ --- Chat Switchboard — 011 Notifications (SQLite) +-- ========================================== +-- Switchboard Core — 005 Notifications (SQLite) +-- ========================================== CREATE TABLE IF NOT EXISTS notifications ( id TEXT PRIMARY KEY, @@ -12,8 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications ( created_at TEXT DEFAULT (datetime('now')) ); -CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at); -CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at); +CREATE INDEX IF NOT EXISTS idx_notifications_user_unread + ON notifications(user_id, is_read, created_at); +CREATE INDEX IF NOT EXISTS idx_notifications_user_created + ON notifications(user_id, created_at); CREATE TABLE IF NOT EXISTS notification_preferences ( id TEXT PRIMARY KEY, diff --git a/server/database/migrations/sqlite/006_audit.sql b/server/database/migrations/sqlite/006_audit.sql new file mode 100644 index 0000000..c738a3c --- /dev/null +++ b/server/database/migrations/sqlite/006_audit.sql @@ -0,0 +1,20 @@ +-- ========================================== +-- Switchboard Core — 006 Audit (SQLite) +-- ========================================== + +CREATE TABLE IF NOT EXISTS audit_log ( + id TEXT PRIMARY KEY, + actor_id TEXT REFERENCES users(id) ON DELETE SET NULL, + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + metadata TEXT DEFAULT '{}', + ip_address TEXT, + user_agent TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at); +CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action); diff --git a/server/database/migrations/sqlite/006_knowledge.sql b/server/database/migrations/sqlite/006_knowledge.sql deleted file mode 100644 index d825a3b..0000000 --- a/server/database/migrations/sqlite/006_knowledge.sql +++ /dev/null @@ -1,73 +0,0 @@ --- Chat Switchboard — 006 Knowledge Bases (SQLite) - -CREATE TABLE IF NOT EXISTS knowledge_bases ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - scope TEXT NOT NULL DEFAULT 'global', - owner_id TEXT REFERENCES users(id) ON DELETE CASCADE, - team_id TEXT REFERENCES teams(id) ON DELETE CASCADE, - embedding_config TEXT NOT NULL DEFAULT '{}', - document_count INTEGER NOT NULL DEFAULT 0, - chunk_count INTEGER NOT NULL DEFAULT 0, - total_bytes INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'active', - discoverable INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope); -CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id); -CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id); - -CREATE TABLE IF NOT EXISTS kb_documents ( - id TEXT PRIMARY KEY, - kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - filename TEXT NOT NULL, - content_type TEXT NOT NULL, - size_bytes INTEGER NOT NULL, - storage_key TEXT NOT NULL, - extracted_text TEXT, - chunk_count INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'pending', - error TEXT, - uploaded_by TEXT NOT NULL REFERENCES users(id), - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id); - -CREATE TABLE IF NOT EXISTS kb_chunks ( - id TEXT PRIMARY KEY, - kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - document_id TEXT NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE, - chunk_index INTEGER NOT NULL, - content TEXT NOT NULL, - token_count INTEGER NOT NULL DEFAULT 0, - metadata TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id); -CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id); - -CREATE TABLE IF NOT EXISTS channel_knowledge_bases ( - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - enabled INTEGER NOT NULL DEFAULT 1, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (channel_id, kb_id) -); - -CREATE TABLE IF NOT EXISTS persona_knowledge_bases ( - persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE, - kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - auto_search INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (persona_id, kb_id) -); - -CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id); -CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id); diff --git a/server/database/migrations/sqlite/007_notes.sql b/server/database/migrations/sqlite/007_notes.sql deleted file mode 100644 index 7e33531..0000000 --- a/server/database/migrations/sqlite/007_notes.sql +++ /dev/null @@ -1,34 +0,0 @@ --- Chat Switchboard — 007 Notes & Links (SQLite) - -CREATE TABLE IF NOT EXISTS notes ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title TEXT NOT NULL, - content TEXT DEFAULT '', - folder_path TEXT DEFAULT '/', - tags TEXT DEFAULT '[]', - metadata TEXT DEFAULT '{}', - source_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL, - source_message_id TEXT, - team_id TEXT REFERENCES teams(id) ON DELETE SET NULL, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id); -CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path); -CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at); -CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id); -CREATE INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id); - -CREATE TABLE IF NOT EXISTS note_links ( - source_note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, - target_note_id TEXT REFERENCES notes(id) ON DELETE CASCADE, - target_title TEXT NOT NULL, - display_text TEXT, - is_transclusion INTEGER NOT NULL DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - PRIMARY KEY (source_note_id, target_title) -); - -CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id); diff --git a/server/database/migrations/sqlite/018_workflows.sql b/server/database/migrations/sqlite/007_workflows.sql similarity index 61% rename from server/database/migrations/sqlite/018_workflows.sql rename to server/database/migrations/sqlite/007_workflows.sql index dbd257a..dbf54d3 100644 --- a/server/database/migrations/sqlite/018_workflows.sql +++ b/server/database/migrations/sqlite/007_workflows.sql @@ -1,5 +1,6 @@ --- Chat Switchboard — 018 Workflows (SQLite) --- Consolidated v0.28.0: merges 023 + 025 + 027 +-- ========================================== +-- Switchboard Core — 007 Workflows (SQLite) +-- ========================================== CREATE TABLE IF NOT EXISTS workflows ( id TEXT PRIMARY KEY, @@ -22,25 +23,28 @@ CREATE TABLE IF NOT EXISTS workflows ( ); CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug - ON workflows(team_id, slug) WHERE team_id IS NOT NULL; -CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug - ON workflows(slug) WHERE team_id IS NULL; + ON workflows(team_id, slug); +CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active); + +CREATE TRIGGER IF NOT EXISTS workflows_updated_at AFTER UPDATE ON workflows + FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at + BEGIN UPDATE workflows SET updated_at = datetime('now') WHERE id = NEW.id; END; CREATE TABLE IF NOT EXISTS workflow_stages ( id TEXT PRIMARY KEY, workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE, ordinal INTEGER NOT NULL, name TEXT NOT NULL, - persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL, + persona_id TEXT, assignment_team_id TEXT REFERENCES teams(id) ON DELETE SET NULL, form_template TEXT NOT NULL DEFAULT '{}', - stage_mode TEXT NOT NULL DEFAULT 'chat_only' - CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')), + stage_mode TEXT NOT NULL DEFAULT 'form_only' + CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')), history_mode TEXT NOT NULL DEFAULT 'full' CHECK (history_mode IN ('full', 'summary', 'fresh')), auto_transition INTEGER NOT NULL DEFAULT 0, transition_rules TEXT NOT NULL DEFAULT '{}', - surface_pkg_id TEXT, + surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL, sla_seconds INTEGER, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); @@ -59,22 +63,3 @@ CREATE TABLE IF NOT EXISTS workflow_versions ( CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow ON workflow_versions(workflow_id, version_number); - -CREATE TABLE IF NOT EXISTS workflow_assignments ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - stage INTEGER NOT NULL, - team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE, - assigned_to TEXT REFERENCES users(id) ON DELETE SET NULL, - status TEXT NOT NULL DEFAULT 'unassigned' - CHECK (status IN ('unassigned', 'claimed', 'completed')), - review_comments TEXT NOT NULL DEFAULT '[]', - created_at TEXT NOT NULL DEFAULT (datetime('now')), - claimed_at TEXT, - completed_at TEXT -); - -CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel - ON workflow_assignments(channel_id, stage); -CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status - ON workflow_assignments(team_id, status); diff --git a/server/database/migrations/sqlite/008_memory.sql b/server/database/migrations/sqlite/008_memory.sql deleted file mode 100644 index 9fd36c6..0000000 --- a/server/database/migrations/sqlite/008_memory.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Chat Switchboard — 008 Memory (SQLite) --- Consolidated v0.28.0: adds last_compacted_at + decay_rate - -CREATE TABLE IF NOT EXISTS memories ( - id TEXT PRIMARY KEY, - scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')), - owner_id TEXT NOT NULL, - user_id TEXT, - key TEXT NOT NULL, - value TEXT NOT NULL, - source_channel_id TEXT, - confidence REAL NOT NULL DEFAULT 1.0, - status TEXT NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'pending_review', 'archived')), - embedding TEXT, - last_compacted_at TEXT, - decay_rate REAL NOT NULL DEFAULT 0.0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_memories_scope_owner - ON memories(scope, owner_id, status); - -CREATE INDEX IF NOT EXISTS idx_memories_persona_user - ON memories(owner_id, user_id); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key - ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key); - -CREATE INDEX IF NOT EXISTS idx_memories_source_channel - ON memories(source_channel_id); - -CREATE TABLE IF NOT EXISTS memory_extraction_log ( - id TEXT PRIMARY KEY, - channel_id TEXT NOT NULL, - user_id TEXT NOT NULL, - last_message_id TEXT NOT NULL, - extracted_at TEXT DEFAULT (datetime('now')), - memory_count INTEGER NOT NULL DEFAULT 0, - UNIQUE(channel_id, user_id) -); - -CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel - ON memory_extraction_log(channel_id); diff --git a/server/database/migrations/sqlite/019_tasks.sql b/server/database/migrations/sqlite/008_tasks.sql similarity index 61% rename from server/database/migrations/sqlite/019_tasks.sql rename to server/database/migrations/sqlite/008_tasks.sql index eda98e7..0cf186b 100644 --- a/server/database/migrations/sqlite/019_tasks.sql +++ b/server/database/migrations/sqlite/008_tasks.sql @@ -1,6 +1,6 @@ --- Chat Switchboard — 019 Tasks (SQLite) --- Consolidated v0.28.0: merges 026 + 027 --- v0.28.0-cs3: trigger_token, action task_type, queued run status. +-- ========================================== +-- Switchboard Core — 008 Tasks (SQLite) +-- ========================================== CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, @@ -8,10 +8,12 @@ CREATE TABLE IF NOT EXISTS tasks ( team_id TEXT REFERENCES teams(id) ON DELETE SET NULL, name TEXT NOT NULL, description TEXT DEFAULT '', - scope TEXT NOT NULL DEFAULT 'personal', - task_type TEXT NOT NULL DEFAULT 'prompt', + scope TEXT NOT NULL DEFAULT 'personal' + CHECK (scope IN ('personal', 'team', 'global')), + task_type TEXT NOT NULL DEFAULT 'action' + CHECK (task_type IN ('action', 'workflow', 'system')), system_function TEXT DEFAULT '', - persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL, + persona_id TEXT, model_id TEXT, system_prompt TEXT DEFAULT '', user_prompt TEXT DEFAULT '', @@ -24,11 +26,10 @@ CREATE TABLE IF NOT EXISTS tasks ( max_tokens INTEGER NOT NULL DEFAULT 4096, max_tool_calls INTEGER NOT NULL DEFAULT 10, max_wall_clock INTEGER NOT NULL DEFAULT 300, - output_mode TEXT NOT NULL DEFAULT 'channel', - output_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL, + output_mode TEXT NOT NULL DEFAULT 'log' + CHECK (output_mode IN ('notification', 'webhook', 'log')), webhook_url TEXT, webhook_secret TEXT, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, notify_on_complete INTEGER NOT NULL DEFAULT 0, notify_on_failure INTEGER NOT NULL DEFAULT 1, last_run_at TEXT, @@ -38,16 +39,16 @@ CREATE TABLE IF NOT EXISTS tasks ( updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = 1; -CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id); -CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at); +CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id); +CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks(trigger_token); CREATE TABLE IF NOT EXISTS task_runs ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL, - status TEXT NOT NULL DEFAULT 'running', + status TEXT NOT NULL DEFAULT 'running' + CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')), trigger_payload TEXT, started_at TEXT NOT NULL DEFAULT (datetime('now')), completed_at TEXT, @@ -57,5 +58,5 @@ CREATE TABLE IF NOT EXISTS task_runs ( error TEXT ); -CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id); -CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status); +CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id); +CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status); diff --git a/server/database/migrations/sqlite/009_ha.sql b/server/database/migrations/sqlite/009_ha.sql new file mode 100644 index 0000000..25cd56f --- /dev/null +++ b/server/database/migrations/sqlite/009_ha.sql @@ -0,0 +1,21 @@ +-- ========================================== +-- Switchboard Core — 009 Multi-Replica HA (SQLite) +-- ========================================== + +CREATE TABLE IF NOT EXISTS ws_tickets ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + expires_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at); + +CREATE TABLE IF NOT EXISTS rate_limit_counters ( + key TEXT NOT NULL, + window_start TEXT NOT NULL, + tokens REAL NOT NULL DEFAULT 0, + PRIMARY KEY (key, window_start) +); + +CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window + ON rate_limit_counters(window_start); diff --git a/server/database/migrations/sqlite/009_workspaces.sql b/server/database/migrations/sqlite/009_workspaces.sql deleted file mode 100644 index 2c1c8d1..0000000 --- a/server/database/migrations/sqlite/009_workspaces.sql +++ /dev/null @@ -1,69 +0,0 @@ --- Chat Switchboard — 009 Workspaces & Git (SQLite) - -CREATE TABLE IF NOT EXISTS workspaces ( - id TEXT PRIMARY KEY, - owner_type TEXT NOT NULL CHECK (owner_type IN ('user', 'project', 'channel', 'team')), - owner_id TEXT NOT NULL, - name TEXT NOT NULL, - root_path TEXT NOT NULL, - max_bytes INTEGER, - status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleting')), - indexing_enabled INTEGER NOT NULL DEFAULT 1, - git_remote_url TEXT, - git_branch TEXT, - git_credential_id TEXT, - git_last_sync TEXT, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id); -CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status); - -CREATE TABLE IF NOT EXISTS workspace_files ( - id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, - path TEXT NOT NULL, - is_directory INTEGER NOT NULL DEFAULT 0, - content_type TEXT, - size_bytes INTEGER NOT NULL DEFAULT 0, - sha256 TEXT, - index_status TEXT DEFAULT 'pending' CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')), - chunk_count INTEGER NOT NULL DEFAULT 0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path ON workspace_files(workspace_id, path); -CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id); -CREATE INDEX IF NOT EXISTS idx_workspace_files_type ON workspace_files(workspace_id, content_type); -CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status ON workspace_files(workspace_id, index_status); - -CREATE TABLE IF NOT EXISTS workspace_chunks ( - id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, - file_id TEXT NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE, - chunk_index INTEGER NOT NULL, - content TEXT NOT NULL, - token_count INTEGER NOT NULL DEFAULT 0, - metadata TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id); -CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id); - -CREATE TABLE IF NOT EXISTS git_credentials ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name TEXT NOT NULL DEFAULT '', - auth_type TEXT NOT NULL DEFAULT 'https_pat', - encrypted_data BLOB NOT NULL DEFAULT '', - nonce BLOB NOT NULL DEFAULT '', - public_key TEXT NOT NULL DEFAULT '', - fingerprint TEXT NOT NULL DEFAULT '', - persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id); diff --git a/server/database/migrations/sqlite/010_projects.sql b/server/database/migrations/sqlite/010_projects.sql deleted file mode 100644 index 3b87212..0000000 --- a/server/database/migrations/sqlite/010_projects.sql +++ /dev/null @@ -1,62 +0,0 @@ --- Chat Switchboard — 010 Projects (SQLite) - -CREATE TABLE IF NOT EXISTS projects ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - color TEXT, - icon TEXT, - scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'team', 'global')), - owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - team_id TEXT REFERENCES teams(id) ON DELETE SET NULL, - workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL, - is_archived INTEGER NOT NULL DEFAULT 0, - settings TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id); -CREATE INDEX IF NOT EXISTS idx_projects_team ON projects(team_id); -CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope); -CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id); - -CREATE TABLE IF NOT EXISTS project_channels ( - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - folder TEXT, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (project_id, channel_id), - UNIQUE (channel_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_channels_project ON project_channels(project_id); -CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id); - -CREATE TABLE IF NOT EXISTS project_knowledge_bases ( - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, - auto_search INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (project_id, kb_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_kb_project ON project_knowledge_bases(project_id); -CREATE INDEX IF NOT EXISTS idx_project_kb_kb ON project_knowledge_bases(kb_id); - -CREATE TABLE IF NOT EXISTS project_notes ( - project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (project_id, note_id) -); - -CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id); -CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id); - --- Cross-domain FK columns on channels (SQLite cannot ADD COLUMN with FK in all cases, --- so these may already exist from initial channel creation — safe to skip on error) --- ALTER TABLE channels ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL; --- ALTER TABLE channels ADD COLUMN workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL; --- Note: SQLite ADD COLUMN IF NOT EXISTS not supported; channels table created with these columns. diff --git a/server/database/migrations/sqlite/012_package_user_settings.sql b/server/database/migrations/sqlite/012_package_user_settings.sql deleted file mode 100644 index 3921b0f..0000000 --- a/server/database/migrations/sqlite/012_package_user_settings.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Chat Switchboard — 012 (placeholder, SQLite) --- Previously: extensions + extension_user_settings tables. --- v0.28.7: Both tables removed. Unified package registry in 016. diff --git a/server/database/migrations/sqlite/013_files.sql b/server/database/migrations/sqlite/013_files.sql deleted file mode 100644 index e35b548..0000000 --- a/server/database/migrations/sqlite/013_files.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Chat Switchboard — 013 Files (SQLite) --- Consolidated v0.28.0: clean install, no legacy attachments migration - -CREATE TABLE IF NOT EXISTS files ( - id TEXT PRIMARY KEY, - channel_id TEXT REFERENCES channels(id) ON DELETE CASCADE, - message_id TEXT REFERENCES messages(id) ON DELETE SET NULL, - user_id TEXT REFERENCES users(id) ON DELETE SET NULL, - project_id TEXT REFERENCES projects(id) ON DELETE SET NULL, - origin TEXT NOT NULL DEFAULT 'user_upload' - CHECK (origin IN ('user_upload', 'tool_output', 'system')), - filename TEXT NOT NULL, - content_type TEXT NOT NULL DEFAULT 'application/octet-stream', - size_bytes INTEGER NOT NULL DEFAULT 0, - storage_key TEXT NOT NULL, - display_hint TEXT NOT NULL DEFAULT 'download' - CHECK (display_hint IN ('inline', 'download', 'thumbnail')), - extracted_text TEXT, - metadata TEXT DEFAULT '{}', - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id); -CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id); -CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id); -CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id); -CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin); diff --git a/server/database/migrations/sqlite/014_audit_usage.sql b/server/database/migrations/sqlite/014_audit_usage.sql deleted file mode 100644 index ceff9d3..0000000 --- a/server/database/migrations/sqlite/014_audit_usage.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Chat Switchboard — 014 Audit & Usage (SQLite) - -CREATE TABLE IF NOT EXISTS audit_log ( - id TEXT PRIMARY KEY, - actor_id TEXT REFERENCES users(id) ON DELETE SET NULL, - action TEXT NOT NULL, - resource_type TEXT NOT NULL, - resource_id TEXT, - metadata TEXT DEFAULT '{}', - ip_address TEXT, - user_agent TEXT DEFAULT '', - created_at TEXT DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at); -CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id); -CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id); -CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action); - -CREATE TABLE IF NOT EXISTS usage_log ( - id TEXT PRIMARY KEY, - channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL, - provider_scope TEXT NOT NULL DEFAULT 'global', - model_id TEXT NOT NULL, - role TEXT, - prompt_tokens INTEGER NOT NULL DEFAULT 0, - completion_tokens INTEGER NOT NULL DEFAULT 0, - cache_creation_tokens INTEGER NOT NULL DEFAULT 0, - cache_read_tokens INTEGER NOT NULL DEFAULT 0, - cost_input REAL, - cost_output REAL, - routing_decision TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at); -CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id); -CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope); diff --git a/server/database/migrations/sqlite/015_health.sql b/server/database/migrations/sqlite/015_health.sql deleted file mode 100644 index fd09a57..0000000 --- a/server/database/migrations/sqlite/015_health.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Chat Switchboard — 015 Tool Health (SQLite) - -CREATE TABLE IF NOT EXISTS tool_health ( - id TEXT PRIMARY KEY, - tool_name TEXT NOT NULL, - window_start TEXT NOT NULL, - request_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0, - total_latency_ms INTEGER NOT NULL DEFAULT 0, - max_latency_ms INTEGER NOT NULL DEFAULT 0, - last_error TEXT, - last_error_at TEXT, - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE (tool_name, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_tool_health_window ON tool_health(tool_name, window_start); diff --git a/server/database/migrations/sqlite/017_auth.sql b/server/database/migrations/sqlite/017_auth.sql deleted file mode 100644 index cc851b1..0000000 --- a/server/database/migrations/sqlite/017_auth.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Chat Switchboard — 017 Auth Providers (SQLite) --- Consolidated v0.28.0 - -CREATE TABLE IF NOT EXISTS oidc_auth_state ( - state TEXT PRIMARY KEY, - nonce TEXT NOT NULL, - redirect_to TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at); diff --git a/server/database/migrations/sqlite/020_ha.sql b/server/database/migrations/sqlite/020_ha.sql deleted file mode 100644 index 37f2d26..0000000 --- a/server/database/migrations/sqlite/020_ha.sql +++ /dev/null @@ -1,34 +0,0 @@ --- ========================================== --- Chat Switchboard — 020 Multi-Replica HA --- ========================================== --- Shared state tables for multi-replica operation. --- v0.32.0: ws_tickets, rate_limit_counters. --- SQLite parity — functional for single-process test coverage. --- ========================================== - --- ========================================= --- WEBSOCKET TICKETS --- ========================================= - -CREATE TABLE IF NOT EXISTS ws_tickets ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - expires_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires - ON ws_tickets (expires_at); - --- ========================================= --- RATE LIMIT COUNTERS --- ========================================= - -CREATE TABLE IF NOT EXISTS rate_limit_counters ( - key TEXT NOT NULL, - window_start TEXT NOT NULL, - tokens REAL NOT NULL DEFAULT 0, - PRIMARY KEY (key, window_start) -); - -CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window - ON rate_limit_counters (window_start); diff --git a/server/database/migrations/sqlite/021_export.sql b/server/database/migrations/sqlite/021_export.sql deleted file mode 100644 index 180c284..0000000 --- a/server/database/migrations/sqlite/021_export.sql +++ /dev/null @@ -1,15 +0,0 @@ --- ========================================== --- Chat Switchboard — 021 Data Portability --- ========================================== --- v0.34.0: Indexes for data export and GDPR operations. --- SQLite version (no partial indexes). --- ========================================== - -CREATE INDEX IF NOT EXISTS idx_messages_channel_active - ON messages (channel_id, created_at); - -CREATE INDEX IF NOT EXISTS idx_channels_user_id - ON channels (user_id); - -CREATE INDEX IF NOT EXISTS idx_files_user_id - ON files (user_id); diff --git a/server/database/migrations/sqlite/022_ext_connections.sql b/server/database/migrations/sqlite/022_ext_connections.sql deleted file mode 100644 index ed9f2f6..0000000 --- a/server/database/migrations/sqlite/022_ext_connections.sql +++ /dev/null @@ -1,24 +0,0 @@ --- ========================================== --- Chat Switchboard — 022 Extension Connections --- ========================================== --- v0.38.1: Scoped credential management for extensions. --- SQLite version (no partial indexes, TEXT types). --- ========================================== - -CREATE TABLE IF NOT EXISTS ext_connections ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - package_id TEXT NOT NULL, - scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')), - owner_id TEXT NOT NULL DEFAULT '', - name TEXT NOT NULL, - config TEXT NOT NULL DEFAULT '{}', - is_active INTEGER DEFAULT 1, - created_at TEXT, - updated_at TEXT, - UNIQUE(type, scope, owner_id, name) -); - -CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type); -CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope); -CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id); diff --git a/server/database/migrations/sqlite/023_ext_dependencies.sql b/server/database/migrations/sqlite/023_ext_dependencies.sql deleted file mode 100644 index 7621c4f..0000000 --- a/server/database/migrations/sqlite/023_ext_dependencies.sql +++ /dev/null @@ -1,62 +0,0 @@ --- ========================================== --- Chat Switchboard — 023 Extension Dependencies --- ========================================== --- v0.38.2: Library package dependency tracking. --- SQLite version. --- --- Also adds 'library' to the packages type CHECK constraint. --- SQLite doesn't support ALTER CONSTRAINT, so we rebuild the table. --- ========================================== - --- Step 1: Rebuild packages table with updated CHECK constraint. -CREATE TABLE IF NOT EXISTS packages_new ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - type TEXT NOT NULL DEFAULT 'surface' - CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')), - version TEXT NOT NULL DEFAULT '0.0.0', - description TEXT NOT NULL DEFAULT '', - author TEXT NOT NULL DEFAULT '', - tier TEXT NOT NULL DEFAULT 'browser' - CHECK (tier IN ('browser', 'starlark', 'sidecar')), - is_system INTEGER NOT NULL DEFAULT 0, - scope TEXT NOT NULL DEFAULT 'global' - CHECK (scope IN ('global', 'team', 'personal')), - team_id TEXT REFERENCES teams(id) ON DELETE CASCADE, - installed_by TEXT REFERENCES users(id) ON DELETE SET NULL, - manifest TEXT NOT NULL DEFAULT '{}', - enabled INTEGER NOT NULL DEFAULT 1, - status TEXT NOT NULL DEFAULT 'active' - CHECK (status IN ('active', 'pending_review', 'suspended')), - schema_version INTEGER NOT NULL DEFAULT 0, - package_settings TEXT NOT NULL DEFAULT '{}', - source TEXT NOT NULL DEFAULT 'core' - CHECK (source IN ('core', 'builtin', 'extension', 'registry')), - installed_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -INSERT INTO packages_new SELECT * FROM packages; -DROP TABLE packages; -ALTER TABLE packages_new RENAME TO packages; - --- Recreate indexes (dropped with old table) -CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type); -CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled); -CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id); -CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source); -CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status); - --- Step 2: Dependencies table. -CREATE TABLE IF NOT EXISTS ext_dependencies ( - consumer_id TEXT NOT NULL, - library_id TEXT NOT NULL, - version_spec TEXT NOT NULL DEFAULT '>=0.0.0', - resolved_ver TEXT NOT NULL DEFAULT '0.0.0', - PRIMARY KEY (consumer_id, library_id), - FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE, - FOREIGN KEY (library_id) REFERENCES packages(id) -); - -CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id); -CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);