From 77c2767b507d693e33eb7124c2a0460ff7daccc3 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 06:39:04 -0400 Subject: [PATCH 01/24] fix: sync PostgresSchema.sql with V043 (drop username column) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove username TEXT NOT NULL and users_username_unique constraint from the users table definition (matches V043 SQLite table recreation) - Update handle_auth_user_created trigger to not INSERT username - Add idempotent ALTER TABLE users DROP COLUMN IF EXISTS username block for existing Postgres/Supabase deployments upgrading from pre-V043 - Bump mirror version comment V001–V042 → V001–V043 Note: the user_id PK rewrite (usr_{hex} → email) from V043 does not apply to Postgres/Supabase — user_id there has always been the GoTrue UUID. Only the username column drop is reflected here. Co-Authored-By: Claude Sonnet 4.6 --- .../Storage/PostgresSchema.sql | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Sovrant.Runtime/Storage/PostgresSchema.sql b/src/Sovrant.Runtime/Storage/PostgresSchema.sql index d1c0b0a0..8546d815 100644 --- a/src/Sovrant.Runtime/Storage/PostgresSchema.sql +++ b/src/Sovrant.Runtime/Storage/PostgresSchema.sql @@ -1,5 +1,5 @@ -- Sovrant PostgreSQL schema (Supabase-compatible). --- Mirrors V001–V042 SQLite migrations. Safe to run multiple times (idempotent). +-- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). -- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. -- BYTEA used for encrypted blobs (credentials table). -- V035/V037 (built-in knowledge seed data) are handled by the app at startup, not here. @@ -11,7 +11,6 @@ CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, - username TEXT NOT NULL, email TEXT, role TEXT NOT NULL DEFAULT 'user', team TEXT, @@ -19,8 +18,7 @@ CREATE TABLE IF NOT EXISTS users ( password_hash TEXT, created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT users_username_unique UNIQUE (username), - CONSTRAINT users_email_unique UNIQUE (email) + CONSTRAINT users_email_unique UNIQUE (email) ); CREATE TABLE IF NOT EXISTS workspaces ( @@ -803,10 +801,9 @@ BEGIN ELSE 'user' END; - INSERT INTO public.users (user_id, username, email, role, status, created_at, updated_at) + INSERT INTO public.users (user_id, email, role, status, created_at, updated_at) VALUES ( NEW.id::TEXT, - COALESCE(NEW.raw_user_meta_data->>'username', split_part(NEW.email, '@', 1)), NEW.email, _role, 'active', @@ -887,6 +884,17 @@ CREATE OR REPLACE TRIGGER on_auth_user_updated -- CREATE POLICY workspace_memory_owner ON workspace_memory -- USING (is_private = 0 OR owner_user_id = auth.uid()::text); +-- ── V043 upgrade: drop username column (existing deployments) ───────────────── +-- Run this block once on any Postgres/Supabase instance that was created from a +-- pre-V043 version of this schema. New installations created from this file will +-- never have the column, so the IF EXISTS guard makes this safe to run on both. +-- +-- Note: for Supabase deployments the user_id remains the GoTrue UUID (not the +-- email). V043's PK rewrite (usr_{hex} → email) only affects SQLite standalone +-- deployments. The only Postgres-visible change from V043 is the username drop. + +ALTER TABLE public.users DROP COLUMN IF EXISTS username; + -- CREATE POLICY session_summaries_owner ON session_summaries -- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); From 9904de3a15640256b9f67e0e526bed3c4589c79c Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 06:58:53 -0400 Subject: [PATCH 02/24] Split PostgresSchema.sql into standalone Postgres and Supabase paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgresSchema.sql is now standalone Postgres only — Supabase-specific trigger functions, RLS stubs, and GoTrue notes removed. db/supabase/migrations/20260625000000_initial_schema.sql is the new Supabase path: full schema + GoTrue mirror triggers + commented RLS policies. Run migrations from db/supabase/ with the Supabase CLI. Both files updated to schema version 43 (V043 username drop reflected). Co-Authored-By: Claude Sonnet 4.6 --- db/supabase/config.toml | 40 + .../20260625000000_initial_schema.sql | 876 ++++++++++++++++++ .../Storage/PostgresSchema.sql | 145 +-- 3 files changed, 925 insertions(+), 136 deletions(-) create mode 100644 db/supabase/config.toml create mode 100644 db/supabase/migrations/20260625000000_initial_schema.sql diff --git a/db/supabase/config.toml b/db/supabase/config.toml new file mode 100644 index 00000000..98c5592c --- /dev/null +++ b/db/supabase/config.toml @@ -0,0 +1,40 @@ +# Supabase CLI project configuration. +# Run migrations from this directory: cd db/supabase && supabase db push +# Or link to a remote project: supabase link --project-ref + +[api] +enabled = true +port = 54321 +schemas = ["public", "storage", "graphql_public"] +extra_search_path = ["public", "extensions"] +max_rows = 1000 + +[db] +port = 54322 +shadow_port = 54320 +major_version = 15 + +[studio] +enabled = true +port = 54323 + +[inbucket] +enabled = true +port = 54324 + +[storage] +enabled = true + +[auth] +enabled = true +# Sovrant uses public.users as the domain anchor. +# The GoTrue auth.users mirror triggers are in the initial migration. +site_url = "http://localhost:5100" +additional_redirect_urls = ["http://localhost:5100"] +jwt_expiry = 3600 +enable_signup = true + +[auth.email] +enable_signup = true +double_confirm_changes = true +enable_confirmations = false diff --git a/db/supabase/migrations/20260625000000_initial_schema.sql b/db/supabase/migrations/20260625000000_initial_schema.sql new file mode 100644 index 00000000..b649068e --- /dev/null +++ b/db/supabase/migrations/20260625000000_initial_schema.sql @@ -0,0 +1,876 @@ +-- Sovrant initial schema for Supabase deployments. +-- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). +-- For standalone PostgreSQL use src/Sovrant.Runtime/Storage/PostgresSchema.sql instead. +-- +-- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. +-- BYTEA used for encrypted blobs (credentials table). +-- V035/V037 (built-in knowledge seed data) are handled by the app at startup, not here. +-- +-- NOTE: user_id remains the GoTrue UUID (auth.users.id::TEXT) in Supabase mode. +-- V043's PK rewrite (usr_{hex} → email) applies to SQLite standalone deployments only. + +-- ── V001 Foundation ─────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + email TEXT, + role TEXT NOT NULL DEFAULT 'user', + team TEXT, + status TEXT NOT NULL DEFAULT 'active', + password_hash TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT users_email_unique UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS workspaces ( + workspace_id TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT 'personal', + name TEXT NOT NULL, + slug TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users(user_id), + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT workspaces_slug_unique UNIQUE (slug) +); + +CREATE TABLE IF NOT EXISTS workspace_members ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + user_id TEXT NOT NULL REFERENCES users(user_id), + role TEXT NOT NULL DEFAULT 'member', + joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (workspace_id, user_id) +); + +CREATE TABLE IF NOT EXISTS workspace_config ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (workspace_id, key) +); + +CREATE TABLE IF NOT EXISTS workspace_invites ( + invite_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + email TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'member', + token TEXT NOT NULL, + expires_at TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT workspace_invites_token_unique UNIQUE (token) +); + +CREATE TABLE IF NOT EXISTS projects ( + project_id TEXT PRIMARY KEY, + workspace_id TEXT REFERENCES workspaces(workspace_id), + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + archived_at TEXT, + CONSTRAINT projects_ws_slug_unique UNIQUE (workspace_id, slug) +); + +CREATE TABLE IF NOT EXISTS project_members ( + project_id TEXT NOT NULL REFERENCES projects(project_id), + user_id TEXT NOT NULL REFERENCES users(user_id), + role TEXT NOT NULL DEFAULT 'contributor', + joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (project_id, user_id) +); + +CREATE TABLE IF NOT EXISTS project_config ( + project_id TEXT NOT NULL REFERENCES projects(project_id), + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (project_id, key) +); + +CREATE TABLE IF NOT EXISTS config ( + scope TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (scope, key) +); + +CREATE TABLE IF NOT EXISTS api_tokens ( + token_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(user_id), + token_hash TEXT NOT NULL, + token_prefix TEXT NOT NULL, + name TEXT, + scopes TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + expires_at TEXT, + revoked_at TEXT, + last_used_at TEXT, + CONSTRAINT api_tokens_hash_unique UNIQUE (token_hash) +); + +CREATE TABLE IF NOT EXISTS roles ( + role_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + is_system INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT roles_name_unique UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS permissions ( + permission_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + CONSTRAINT permissions_name_unique UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id TEXT NOT NULL REFERENCES roles(role_id), + permission_id TEXT NOT NULL REFERENCES permissions(permission_id), + PRIMARY KEY (role_id, permission_id) +); + +CREATE TABLE IF NOT EXISTS user_roles ( + user_id TEXT NOT NULL REFERENCES users(user_id), + role_id TEXT NOT NULL REFERENCES roles(role_id), + workspace_id TEXT NOT NULL DEFAULT '', + granted_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (user_id, role_id, workspace_id) +); + +CREATE TABLE IF NOT EXISTS audit_governance ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp TEXT NOT NULL, + phase TEXT NOT NULL, + tool TEXT NOT NULL, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + action TEXT NOT NULL, + rule TEXT NOT NULL, + reason TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS audit_bash ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp TEXT NOT NULL, + command TEXT NOT NULL, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + exit_code INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS ix_api_tokens_user ON api_tokens(user_id); +CREATE INDEX IF NOT EXISTS ix_api_tokens_hash ON api_tokens(token_hash); +CREATE INDEX IF NOT EXISTS ix_audit_governance_session ON audit_governance(session_id); +CREATE INDEX IF NOT EXISTS ix_audit_bash_session ON audit_bash(session_id); + +-- ── V002 Sessions ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + project_id TEXT, + model TEXT, + status TEXT NOT NULL DEFAULT 'active', + title TEXT, + mcp_servers TEXT, + started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + ended_at TEXT, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS session_entries ( + entry_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE, + entry_uid TEXT NOT NULL, + timestamp TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + model TEXT, + provider TEXT, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + tool_name TEXT, + tool_use_id TEXT, + is_error INTEGER NOT NULL DEFAULT 0, + -- Full-text search via PostgreSQL tsvector (replaces SQLite FTS5) + search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED, + CONSTRAINT uq_session_entries_uid UNIQUE (session_id, entry_uid) +); + +CREATE TABLE IF NOT EXISTS token_usage ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + project_id TEXT, + model TEXT NOT NULL, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cost_usd DOUBLE PRECISION, + recorded_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_sessions_user ON sessions(user_id); +CREATE INDEX IF NOT EXISTS ix_sessions_status ON sessions(status); +CREATE INDEX IF NOT EXISTS ix_sessions_workspace ON sessions(workspace_id); +CREATE INDEX IF NOT EXISTS ix_sessions_project ON sessions(project_id); +CREATE INDEX IF NOT EXISTS ix_session_entries_session ON session_entries(session_id); +CREATE INDEX IF NOT EXISTS ix_session_entries_fts ON session_entries USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS ix_token_usage_session ON token_usage(session_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_user ON token_usage(user_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_workspace ON token_usage(workspace_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_project ON token_usage(project_id); + +-- ── V003 Memory ─────────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS session_summaries ( + session_id TEXT PRIMARY KEY, + project TEXT NOT NULL, + workspace_id TEXT, + started_at TEXT NOT NULL, + ended_at TEXT NOT NULL, + tasks TEXT NOT NULL DEFAULT '[]', + tools_used TEXT NOT NULL DEFAULT '[]', + files_modified TEXT NOT NULL DEFAULT '[]', + outcome TEXT NOT NULL DEFAULT 'Unknown', + total_input_tokens INTEGER NOT NULL DEFAULT 0, + total_output_tokens INTEGER NOT NULL DEFAULT 0, + turn_count INTEGER NOT NULL DEFAULT 0, + error_count INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS learned_patterns ( + id TEXT PRIMARY KEY, + pattern TEXT NOT NULL, + project TEXT NOT NULL, + source_session TEXT, + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + last_used TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS instincts ( + id TEXT PRIMARY KEY, + trigger TEXT NOT NULL, + action TEXT NOT NULL, + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, + evidence TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_session_summaries_project ON session_summaries(project); +CREATE INDEX IF NOT EXISTS ix_session_summaries_workspace ON session_summaries(workspace_id); +CREATE INDEX IF NOT EXISTS ix_learned_patterns_project ON learned_patterns(project); + +-- ── V004 Credentials ────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS credentials ( + key_hash TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + nonce BYTEA NOT NULL, + tag BYTEA NOT NULL, + ciphertext BYTEA NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V005 Swarm + Evals ──────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS swarm_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + swarm_id TEXT NOT NULL, + event_type TEXT NOT NULL, + agent_id TEXT, + workspace_id TEXT, + project_id TEXT, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + kind TEXT NOT NULL DEFAULT 'swarm', + run_id TEXT, + user_id TEXT, + parent_swarm_id TEXT +); + +CREATE TABLE IF NOT EXISTS eval_runs ( + run_id TEXT PRIMARY KEY, + suite_name TEXT NOT NULL, + workspace_id TEXT, + started_at TEXT NOT NULL, + duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, + pass_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + pass_at_1_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + total_passed INTEGER NOT NULL DEFAULT 0, + total_failed INTEGER NOT NULL DEFAULT 0, + total_skipped INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS eval_results ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES eval_runs(run_id) ON DELETE CASCADE, + eval_name TEXT NOT NULL, + category TEXT NOT NULL, + grader_type TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, + pass_at_1 INTEGER NOT NULL DEFAULT 0, + pass_count INTEGER NOT NULL DEFAULT 0, + attempt_count INTEGER NOT NULL DEFAULT 0, + average_score DOUBLE PRECISION, + duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, + skipped INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS ix_swarm_events_swarm ON swarm_events(swarm_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_workspace ON swarm_events(workspace_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_project ON swarm_events(project_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_run_id ON swarm_events(run_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_kind ON swarm_events(kind); +CREATE INDEX IF NOT EXISTS ix_swarm_events_user ON swarm_events(user_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_parent ON swarm_events(parent_swarm_id); +CREATE INDEX IF NOT EXISTS ix_eval_runs_suite ON eval_runs(suite_name); +CREATE INDEX IF NOT EXISTS ix_eval_runs_workspace ON eval_runs(workspace_id); +CREATE INDEX IF NOT EXISTS ix_eval_results_run ON eval_results(run_id); + +-- ── V006 Workspace Memory ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS workspace_memory ( + memory_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + layer TEXT NOT NULL, + content TEXT NOT NULL, + confidence DOUBLE PRECISION, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_workspace_memory_workspace ON workspace_memory(workspace_id); +CREATE INDEX IF NOT EXISTS ix_workspace_memory_layer ON workspace_memory(workspace_id, layer); +CREATE INDEX IF NOT EXISTS ix_workspace_memory_project ON workspace_memory(workspace_id, project_id); + +-- ── V010 Runtime Traces ─────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS runtime_traces ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + runtime_run_id TEXT NOT NULL, + plan_id TEXT NOT NULL, + plan_version INTEGER NOT NULL, + step_index INTEGER, + entry_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + session_id TEXT, + workspace_id TEXT, + project_id TEXT +); + +CREATE TABLE IF NOT EXISTS mission_scratchpad ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + mission_id TEXT NOT NULL, + step_index INTEGER NOT NULL, + agent_id TEXT, + namespace TEXT NOT NULL DEFAULT 'default', + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + workspace_id TEXT, + project_id TEXT +); + +CREATE INDEX IF NOT EXISTS ix_runtime_traces_run ON runtime_traces(runtime_run_id, id); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_entry_type ON runtime_traces(entry_type); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_workspace ON runtime_traces(workspace_id); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_project ON runtime_traces(project_id); +CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_mission ON mission_scratchpad(mission_id, step_index); +CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_workspace ON mission_scratchpad(workspace_id); + +-- ── V011 Missions ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS missions ( + id TEXT PRIMARY KEY, + goal TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'planning', + plan_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + completed_at TEXT, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + owner_user_id TEXT +); + +CREATE TABLE IF NOT EXISTS mission_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + mission_id TEXT NOT NULL, + event_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + workspace_id TEXT, + project_id TEXT +); + +CREATE INDEX IF NOT EXISTS ix_missions_status ON missions(status); +CREATE INDEX IF NOT EXISTS ix_missions_workspace ON missions(workspace_id); +CREATE INDEX IF NOT EXISTS ix_missions_project ON missions(project_id); +CREATE INDEX IF NOT EXISTS ix_missions_owner ON missions(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_mission_events_mission ON mission_events(mission_id, id); +CREATE INDEX IF NOT EXISTS ix_mission_events_workspace ON mission_events(workspace_id); + +-- ── V012 Unified Orchestration ──────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS teams ( + team_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL, + project_id TEXT, + name TEXT NOT NULL, + description TEXT, + origin TEXT NOT NULL DEFAULT 'user', + created_by TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + run_mode TEXT NOT NULL DEFAULT 'sequential', + max_concurrent INTEGER NOT NULL DEFAULT 1, + file_locks_enabled INTEGER NOT NULL DEFAULT 0, + quality_gate_enabled INTEGER NOT NULL DEFAULT 0, + quality_gate_threshold INTEGER NOT NULL DEFAULT 7, + decomposition_mode TEXT NOT NULL DEFAULT 'off' +); + +CREATE TABLE IF NOT EXISTS team_members ( + member_id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(team_id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL, + project_id TEXT, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'general', + template TEXT, + system_prompt TEXT, + allowed_tools TEXT, + model_level TEXT, + created_by TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + last_used_at TEXT, + status TEXT NOT NULL DEFAULT 'active' +); + +CREATE TABLE IF NOT EXISTS agent_runs ( + run_id TEXT PRIMARY KEY, + parent_run_id TEXT, + team_id TEXT, + member_id TEXT, + workspace_id TEXT NOT NULL, + project_id TEXT, + user_id TEXT NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'queued', + started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + ended_at TEXT, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cost_usd DOUBLE PRECISION, + prompt TEXT +); + +CREATE INDEX IF NOT EXISTS ix_teams_workspace ON teams(workspace_id); +CREATE INDEX IF NOT EXISTS ix_teams_project ON teams(project_id); +CREATE INDEX IF NOT EXISTS ix_teams_name ON teams(workspace_id, name); +CREATE INDEX IF NOT EXISTS ix_team_members_team ON team_members(team_id); +CREATE INDEX IF NOT EXISTS ix_team_members_workspace ON team_members(workspace_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_parent ON agent_runs(parent_run_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_team ON agent_runs(team_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_workspace ON agent_runs(workspace_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_user ON agent_runs(user_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_kind ON agent_runs(kind); +CREATE INDEX IF NOT EXISTS ix_agent_runs_status ON agent_runs(status); + +-- ── V013 Coordination Mailbox ───────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS coordination_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + source_group_id TEXT NOT NULL, + target_group_id TEXT NOT NULL, + event_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending', + workspace_id TEXT NOT NULL, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + delivered_at TEXT, + acknowledged_at TEXT +); + +CREATE TABLE IF NOT EXISTS group_pm_assignments ( + group_id TEXT PRIMARY KEY, + group_type TEXT NOT NULL, + pm_template TEXT NOT NULL DEFAULT 'pm-coordinator', + workspace_id TEXT NOT NULL, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_coordination_events_target ON coordination_events(target_group_id, status); +CREATE INDEX IF NOT EXISTS ix_coordination_events_source ON coordination_events(source_group_id); +CREATE INDEX IF NOT EXISTS ix_coordination_events_ws ON coordination_events(workspace_id); +CREATE INDEX IF NOT EXISTS ix_group_pm_assignments_ws ON group_pm_assignments(workspace_id); + +-- ── V017 Hooks ──────────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS hooks ( + hook_id TEXT PRIMARY KEY, + event TEXT NOT NULL, + command TEXT NOT NULL, + matcher_tool TEXT, + matcher_glob TEXT, + timeout_ms INTEGER NOT NULL DEFAULT 30000, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS hooks_event_enabled_idx ON hooks(event) WHERE enabled = 1; + +-- ── V018 Workspace Settings ─────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS workspace_settings ( + workspace_id TEXT NOT NULL DEFAULT '', + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (workspace_id, key) +); + +CREATE INDEX IF NOT EXISTS workspace_settings_key_idx ON workspace_settings(key); + +-- ── V019 MCP/LSP Servers ────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS mcp_servers ( + id TEXT NOT NULL DEFAULT '', + name TEXT PRIMARY KEY, + command TEXT NOT NULL DEFAULT '', + args_json TEXT NOT NULL DEFAULT '[]', + env_json TEXT NOT NULL DEFAULT '{}', + oauth_config_json TEXT, + url TEXT, + headers_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; + +CREATE TABLE IF NOT EXISTS lsp_servers ( + language TEXT PRIMARY KEY, + command TEXT NOT NULL DEFAULT '', + args_json TEXT NOT NULL DEFAULT '[]', + env_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V020 User Preferences ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (user_id, key) +); + +CREATE INDEX IF NOT EXISTS user_preferences_key_idx ON user_preferences(key); + +-- ── V021 Provider Profiles ──────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS provider_profiles ( + profile_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + provider_kind TEXT NOT NULL, + base_url TEXT NOT NULL, + default_model TEXT, + max_tokens INTEGER, + credential_id TEXT NOT NULL, + workspace_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS provider_profiles_user_idx ON provider_profiles(user_id); +CREATE INDEX IF NOT EXISTS provider_profiles_workspace_idx ON provider_profiles(workspace_id) + WHERE workspace_id IS NOT NULL; + +-- ── V026 Auth Credentials ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS server_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS password_reset_tokens ( + token_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + token_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + expires_at TEXT NOT NULL, + used_at TEXT, + CONSTRAINT prt_hash_unique UNIQUE (token_hash) +); + +CREATE INDEX IF NOT EXISTS ix_prt_user ON password_reset_tokens(user_id); +CREATE INDEX IF NOT EXISTS ix_prt_hash ON password_reset_tokens(token_hash); + +-- ── V030 Activity is Private ────────────────────────────────────────────────── + +ALTER TABLE missions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; +ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; +ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS ix_missions_is_private ON missions(is_private); +CREATE INDEX IF NOT EXISTS ix_agent_runs_is_private ON agent_runs(is_private); +CREATE INDEX IF NOT EXISTS ix_sessions_is_private ON sessions(is_private); + +-- ── V031 Session Agent Name ─────────────────────────────────────────────────── + +ALTER TABLE sessions ADD COLUMN IF NOT EXISTS agent_name TEXT; + +CREATE INDEX IF NOT EXISTS idx_sessions_agent_name ON sessions(agent_name) WHERE agent_name IS NOT NULL; + +-- ── V032 MCP Trust Rules ────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS mcp_trust_rules ( + rule_id TEXT NOT NULL PRIMARY KEY, + workspace_id TEXT NOT NULL, + server_name TEXT NOT NULL, + tool_pattern TEXT NOT NULL, + action TEXT NOT NULL, + reason TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_workspace ON mcp_trust_rules(workspace_id); +CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_server ON mcp_trust_rules(server_name); + +-- ── V033/V034/V036 Knowledge Pages ─────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS knowledge_pages ( + knowledge_id TEXT NOT NULL PRIMARY KEY, + kind TEXT NOT NULL, + slug TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + tier TEXT NOT NULL DEFAULT 'User', + trigger TEXT, + agents TEXT, + tools TEXT, + industry TEXT, + default_format TEXT, + category TEXT, + role TEXT, + recommended_level TEXT, + fields_json TEXT, + filename_template TEXT, + body TEXT NOT NULL DEFAULT '', + workspace_id TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + UNIQUE (kind, slug, workspace_id) +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_pages_kind ON knowledge_pages(kind); +CREATE INDEX IF NOT EXISTS idx_knowledge_pages_workspace ON knowledge_pages(workspace_id); + +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS role TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS recommended_level TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS fields_json TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS filename_template TEXT; + +-- ── V038 Knowledge Attributions ─────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS knowledge_attributions ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL, + turn_index INTEGER NOT NULL, + kind TEXT NOT NULL, + slug TEXT NOT NULL, + used_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_attributions_session ON knowledge_attributions(session_id); + +-- ── V039 Keystore ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS keystore ( + scope TEXT NOT NULL PRIMARY KEY, + key_hex TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V040 MCP Server stable ID ──────────────────────────────────────────────── + +ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS id TEXT NOT NULL DEFAULT ''; +CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; +UPDATE mcp_servers SET id = gen_random_uuid()::text WHERE id = ''; + +-- ── V041 Workspace Memory Privacy ──────────────────────────────────────────── + +ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS ix_workspace_memory_owner ON workspace_memory(owner_user_id); + +-- ── V042 Memory Owner User ID ───────────────────────────────────────────────── + +ALTER TABLE session_summaries ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE learned_patterns ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE instincts ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; + +CREATE INDEX IF NOT EXISTS ix_session_summaries_owner ON session_summaries(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_learned_patterns_owner ON learned_patterns(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_instincts_owner ON instincts(owner_user_id); + +-- ── V043 Drop username column ───────────────────────────────────────────────── +-- Fresh installs never have this column. IF EXISTS makes this safe to run on both +-- new and upgraded instances. user_id remains the GoTrue UUID in Supabase mode. + +ALTER TABLE public.users DROP COLUMN IF EXISTS username; + +-- ── Schema version tracking ─────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS sovrant_schema_version ( + id INTEGER PRIMARY KEY DEFAULT 1, + version INTEGER NOT NULL, + applied_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT single_row CHECK (id = 1) +); + +INSERT INTO sovrant_schema_version (id, version) +VALUES (1, 43) +ON CONFLICT (id) DO UPDATE SET version = EXCLUDED.version, applied_at = EXCLUDED.applied_at; + +-- ════════════════════════════════════════════════════════════════════════════════ +-- SUPABASE AUTH EXTENSION +-- Keeps public.users in sync with auth.users (GoTrue) automatically. +-- Every time a user signs up or updates their email/role in Supabase Auth, +-- these triggers mirror the change so all FK constraints remain intact. +-- +-- user_id = auth.users.id::TEXT (UUID string, NOT the email address). +-- The email-as-PK rewrite in V043 applies to SQLite standalone only. +-- ════════════════════════════════════════════════════════════════════════════════ + +-- ── Mirror trigger: auth.users INSERT → public.users INSERT ────────────────── + +CREATE OR REPLACE FUNCTION public.handle_auth_user_created() +RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ +DECLARE + _role TEXT; +BEGIN + -- Read sovrant_role from app_metadata (service-role only, user cannot self-set). + -- Whitelist: only 'admin' is elevated; anything else (missing, null, unknown) → 'user'. + _role := CASE + WHEN NEW.raw_app_meta_data->>'sovrant_role' = 'admin' THEN 'admin' + ELSE 'user' + END; + + INSERT INTO public.users (user_id, email, role, status, created_at, updated_at) + VALUES ( + NEW.id::TEXT, + NEW.email, + _role, + 'active', + to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), + to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') + ) + ON CONFLICT (user_id) DO NOTHING; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_auth_user_created(); + +-- ── Mirror trigger: auth.users email/role UPDATE → public.users UPDATE ─────── +-- Fires on email changes AND raw_app_meta_data changes so that setting +-- sovrant_role in app_metadata immediately syncs to public.users.role. + +CREATE OR REPLACE FUNCTION public.handle_auth_user_updated() +RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ +DECLARE + _role TEXT; +BEGIN + IF NEW.raw_app_meta_data IS DISTINCT FROM OLD.raw_app_meta_data THEN + _role := CASE + WHEN NEW.raw_app_meta_data->>'sovrant_role' = 'admin' THEN 'admin' + ELSE 'user' + END; + UPDATE public.users + SET email = NEW.email, + role = _role, + updated_at = to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') + WHERE user_id = NEW.id::TEXT; + ELSE + UPDATE public.users + SET email = NEW.email, + updated_at = to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') + WHERE user_id = NEW.id::TEXT; + END IF; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE TRIGGER on_auth_user_updated + AFTER UPDATE OF email, raw_app_meta_data ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_auth_user_updated(); + +-- ── Notes for Supabase deployments ─────────────────────────────────────────── +-- • public.users.password_hash is always NULL for Supabase-authenticated users. +-- GoTrue owns password management; the column is inert but kept for schema +-- parity with SQLite / standalone Postgres deployments. +-- • password_reset_tokens is also inert — Supabase handles password resets. +-- The table is retained so the schema remains identical across all modes. +-- • svt_* api_tokens still work for CLI / headless API access in Supabase mode. +-- Only interactive UI sessions use Supabase JWTs; machine tokens use the table. +-- • First admin: create the user in the Supabase dashboard (Auth → Users). +-- The mirror trigger fires automatically (role defaults to 'user'). Elevate to +-- admin by setting sovrant_role in app_metadata (service-role only): +-- UPDATE auth.users +-- SET raw_app_meta_data = jsonb_set( +-- COALESCE(raw_app_meta_data, '{}'), '{sovrant_role}', '"admin"') +-- WHERE email = 'you@example.com'; +-- The on_auth_user_updated trigger fires and syncs role = 'admin' to +-- public.users automatically. Never UPDATE public.users.role directly. + +-- ── Row-Level Security (recommended for Supabase deployments) ──────────────── +-- Uncomment to enforce the owner_user_id privacy model at the database layer. +-- Required if any Supabase Edge Functions or dashboard queries need to respect +-- per-user memory privacy without going through the Sovrant API. +-- +-- NOTE: auth.uid() only works for Supabase JWT holders. svt_* machine tokens +-- use the service role and bypass RLS entirely by design. Sovrant's own API +-- enforces privacy at the application layer for machine-token callers. + +-- ALTER TABLE workspace_memory ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE session_summaries ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE learned_patterns ENABLE ROW LEVEL SECURITY; +-- ALTER TABLE instincts ENABLE ROW LEVEL SECURITY; + +-- CREATE POLICY workspace_memory_owner ON workspace_memory +-- USING (is_private = 0 OR owner_user_id = auth.uid()::text); + +-- CREATE POLICY session_summaries_owner ON session_summaries +-- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); + +-- CREATE POLICY learned_patterns_owner ON learned_patterns +-- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); + +-- CREATE POLICY instincts_owner ON instincts +-- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); diff --git a/src/Sovrant.Runtime/Storage/PostgresSchema.sql b/src/Sovrant.Runtime/Storage/PostgresSchema.sql index 8546d815..9bd4157c 100644 --- a/src/Sovrant.Runtime/Storage/PostgresSchema.sql +++ b/src/Sovrant.Runtime/Storage/PostgresSchema.sql @@ -1,5 +1,6 @@ --- Sovrant PostgreSQL schema (Supabase-compatible). +-- Sovrant standalone PostgreSQL schema. -- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). +-- For Supabase deployments use supabase/migrations/ instead (includes GoTrue auth triggers). -- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. -- BYTEA used for encrypted blobs (credentials table). -- V035/V037 (built-in knowledge seed data) are handled by the app at startup, not here. @@ -757,9 +758,13 @@ CREATE INDEX IF NOT EXISTS ix_session_summaries_owner ON session_summaries(owner CREATE INDEX IF NOT EXISTS ix_learned_patterns_owner ON learned_patterns(owner_user_id); CREATE INDEX IF NOT EXISTS ix_instincts_owner ON instincts(owner_user_id); +-- ── V043 Drop username column ───────────────────────────────────────────────── +-- For instances initialized before V043. Fresh installs never had this column. +-- V043's PK rewrite (usr_{hex} → email) applies to SQLite standalone only. + +ALTER TABLE public.users DROP COLUMN IF EXISTS username; + -- ── Schema version tracking ─────────────────────────────────────────────────── --- Records the last successfully applied schema version so the admin UI --- can show "Up to date" vs "Needs initialization". CREATE TABLE IF NOT EXISTS sovrant_schema_version ( id INTEGER PRIMARY KEY DEFAULT 1, @@ -769,137 +774,5 @@ CREATE TABLE IF NOT EXISTS sovrant_schema_version ( ); INSERT INTO sovrant_schema_version (id, version) -VALUES (1, 42) +VALUES (1, 43) ON CONFLICT (id) DO UPDATE SET version = EXCLUDED.version, applied_at = EXCLUDED.applied_at; - --- ════════════════════════════════════════════════════════════════════════════════ --- SUPABASE AUTH EXTENSION --- Run this section ONLY on Supabase-hosted deployments, AFTER the schema above. --- Standalone PostgreSQL deployments skip this entire section. --- --- Why: Supabase Auth stores identities in auth.users (UUID PK, managed by GoTrue). --- The app uses public.users (TEXT PK) as the FK anchor for all domain tables. --- These triggers keep public.users in sync automatically so every FK constraint --- continues to work without any application code changes. --- --- user_id = auth.users.id::TEXT — UUID string, compatible with TEXT PK on all --- existing FK columns (workspaces, workspace_members, api_tokens, user_roles, --- project_members, password_reset_tokens). --- ════════════════════════════════════════════════════════════════════════════════ - --- ── Mirror trigger: auth.users INSERT → public.users INSERT ────────────────── - -CREATE OR REPLACE FUNCTION public.handle_auth_user_created() -RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ -DECLARE - _role TEXT; -BEGIN - -- Read sovrant_role from app_metadata (service-role only, user cannot self-set). - -- Whitelist: only 'admin' is elevated; anything else (missing, null, unknown) → 'user'. - _role := CASE - WHEN NEW.raw_app_meta_data->>'sovrant_role' = 'admin' THEN 'admin' - ELSE 'user' - END; - - INSERT INTO public.users (user_id, email, role, status, created_at, updated_at) - VALUES ( - NEW.id::TEXT, - NEW.email, - _role, - 'active', - to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), - to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') - ) - ON CONFLICT (user_id) DO NOTHING; - RETURN NEW; -END; -$$; - -CREATE OR REPLACE TRIGGER on_auth_user_created - AFTER INSERT ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.handle_auth_user_created(); - --- ── Mirror trigger: auth.users email/role UPDATE → public.users UPDATE ─────── --- Fires on email changes AND raw_app_meta_data changes so that setting --- sovrant_role in app_metadata immediately syncs to public.users.role. - -CREATE OR REPLACE FUNCTION public.handle_auth_user_updated() -RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ -DECLARE - _role TEXT; -BEGIN - IF NEW.raw_app_meta_data IS DISTINCT FROM OLD.raw_app_meta_data THEN - _role := CASE - WHEN NEW.raw_app_meta_data->>'sovrant_role' = 'admin' THEN 'admin' - ELSE 'user' - END; - UPDATE public.users - SET email = NEW.email, - role = _role, - updated_at = to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') - WHERE user_id = NEW.id::TEXT; - ELSE - UPDATE public.users - SET email = NEW.email, - updated_at = to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') - WHERE user_id = NEW.id::TEXT; - END IF; - RETURN NEW; -END; -$$; - -CREATE OR REPLACE TRIGGER on_auth_user_updated - AFTER UPDATE OF email, raw_app_meta_data ON auth.users - FOR EACH ROW EXECUTE FUNCTION public.handle_auth_user_updated(); - --- ── Notes for Supabase deployments ─────────────────────────────────────────── --- • public.users.password_hash is always NULL for Supabase-authenticated users. --- GoTrue owns password management; the column is inert and kept for schema --- parity with SQLite / standalone Postgres deployments. --- • password_reset_tokens is also inert — Supabase handles password resets. --- The table is retained so the schema remains identical across all modes. --- • svt_* api_tokens still work for CLI / headless API access in Supabase mode. --- Only interactive UI sessions use JWTs; machine tokens use the token table. --- • First admin: create the user in the Supabase dashboard (Auth → Users). --- The mirror trigger fires automatically (role defaults to 'user'). Elevate to --- admin by setting sovrant_role in app_metadata (service-role only): --- UPDATE auth.users --- SET raw_app_meta_data = jsonb_set( --- COALESCE(raw_app_meta_data, '{}'), '{sovrant_role}', '"admin"') --- WHERE email = 'you@example.com'; --- The on_auth_user_updated trigger fires and syncs role = 'admin' to --- public.users automatically. Never UPDATE public.users.role directly. - --- ── Row-Level Security (recommended for Supabase deployments) ──────────────── --- Uncomment to enforce the owner_user_id privacy model at the database layer --- rather than relying solely on application-layer query filters. Required if --- any Supabase Edge Functions, dashboard queries, or service-role clients need --- to respect per-user memory privacy without going through the Sovrant API. - --- ALTER TABLE workspace_memory ENABLE ROW LEVEL SECURITY; --- ALTER TABLE session_summaries ENABLE ROW LEVEL SECURITY; --- ALTER TABLE learned_patterns ENABLE ROW LEVEL SECURITY; --- ALTER TABLE instincts ENABLE ROW LEVEL SECURITY; - --- CREATE POLICY workspace_memory_owner ON workspace_memory --- USING (is_private = 0 OR owner_user_id = auth.uid()::text); - --- ── V043 upgrade: drop username column (existing deployments) ───────────────── --- Run this block once on any Postgres/Supabase instance that was created from a --- pre-V043 version of this schema. New installations created from this file will --- never have the column, so the IF EXISTS guard makes this safe to run on both. --- --- Note: for Supabase deployments the user_id remains the GoTrue UUID (not the --- email). V043's PK rewrite (usr_{hex} → email) only affects SQLite standalone --- deployments. The only Postgres-visible change from V043 is the username drop. - -ALTER TABLE public.users DROP COLUMN IF EXISTS username; - --- CREATE POLICY session_summaries_owner ON session_summaries --- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); - --- CREATE POLICY learned_patterns_owner ON learned_patterns --- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); - --- CREATE POLICY instincts_owner ON instincts --- USING (owner_user_id = '' OR owner_user_id = auth.uid()::text); From ea5fa8e91a32e28d84cc9c13c2f0889f9800dbae Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 07:19:19 -0400 Subject: [PATCH 03/24] Move PostgresSchema.sql to db/postgres/ for consistent db layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All database files now live under db/: db/postgres/PostgresSchema.sql — standalone Postgres (embedded resource) db/supabase/migrations/... — Supabase CLI migrations Sovrant.Runtime.csproj updated to embed from new path (LogicalName preserved so PostgresSchemaInitializer lookup is unchanged). Co-Authored-By: Claude Sonnet 4.6 --- db/postgres/PostgresSchema.sql | 778 ++++++++++++++++++ .../20260625000000_initial_schema.sql | 2 +- src/Sovrant.Runtime/Sovrant.Runtime.csproj | 2 +- 3 files changed, 780 insertions(+), 2 deletions(-) create mode 100644 db/postgres/PostgresSchema.sql diff --git a/db/postgres/PostgresSchema.sql b/db/postgres/PostgresSchema.sql new file mode 100644 index 00000000..9bd4157c --- /dev/null +++ b/db/postgres/PostgresSchema.sql @@ -0,0 +1,778 @@ +-- Sovrant standalone PostgreSQL schema. +-- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). +-- For Supabase deployments use supabase/migrations/ instead (includes GoTrue auth triggers). +-- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. +-- BYTEA used for encrypted blobs (credentials table). +-- V035/V037 (built-in knowledge seed data) are handled by the app at startup, not here. + +-- ── Helper: UTC timestamp default ──────────────────────────────────────────── +-- All timestamp columns that had SQLite strftime defaults use this expression. + +-- ── V001 Foundation ─────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + email TEXT, + role TEXT NOT NULL DEFAULT 'user', + team TEXT, + status TEXT NOT NULL DEFAULT 'active', + password_hash TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT users_email_unique UNIQUE (email) +); + +CREATE TABLE IF NOT EXISTS workspaces ( + workspace_id TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT 'personal', + name TEXT NOT NULL, + slug TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users(user_id), + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT workspaces_slug_unique UNIQUE (slug) +); + +CREATE TABLE IF NOT EXISTS workspace_members ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + user_id TEXT NOT NULL REFERENCES users(user_id), + role TEXT NOT NULL DEFAULT 'member', + joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (workspace_id, user_id) +); + +CREATE TABLE IF NOT EXISTS workspace_config ( + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (workspace_id, key) +); + +CREATE TABLE IF NOT EXISTS workspace_invites ( + invite_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), + email TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'member', + token TEXT NOT NULL, + expires_at TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT workspace_invites_token_unique UNIQUE (token) +); + +CREATE TABLE IF NOT EXISTS projects ( + project_id TEXT PRIMARY KEY, + workspace_id TEXT REFERENCES workspaces(workspace_id), + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + archived_at TEXT, + CONSTRAINT projects_ws_slug_unique UNIQUE (workspace_id, slug) +); + +CREATE TABLE IF NOT EXISTS project_members ( + project_id TEXT NOT NULL REFERENCES projects(project_id), + user_id TEXT NOT NULL REFERENCES users(user_id), + role TEXT NOT NULL DEFAULT 'contributor', + joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (project_id, user_id) +); + +CREATE TABLE IF NOT EXISTS project_config ( + project_id TEXT NOT NULL REFERENCES projects(project_id), + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (project_id, key) +); + +CREATE TABLE IF NOT EXISTS config ( + scope TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (scope, key) +); + +CREATE TABLE IF NOT EXISTS api_tokens ( + token_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(user_id), + token_hash TEXT NOT NULL, + token_prefix TEXT NOT NULL, + name TEXT, + scopes TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + expires_at TEXT, + revoked_at TEXT, + last_used_at TEXT, + CONSTRAINT api_tokens_hash_unique UNIQUE (token_hash) +); + +CREATE TABLE IF NOT EXISTS roles ( + role_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + is_system INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT roles_name_unique UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS permissions ( + permission_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + CONSTRAINT permissions_name_unique UNIQUE (name) +); + +CREATE TABLE IF NOT EXISTS role_permissions ( + role_id TEXT NOT NULL REFERENCES roles(role_id), + permission_id TEXT NOT NULL REFERENCES permissions(permission_id), + PRIMARY KEY (role_id, permission_id) +); + +CREATE TABLE IF NOT EXISTS user_roles ( + user_id TEXT NOT NULL REFERENCES users(user_id), + role_id TEXT NOT NULL REFERENCES roles(role_id), + workspace_id TEXT NOT NULL DEFAULT '', + granted_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (user_id, role_id, workspace_id) +); + +CREATE TABLE IF NOT EXISTS audit_governance ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp TEXT NOT NULL, + phase TEXT NOT NULL, + tool TEXT NOT NULL, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + action TEXT NOT NULL, + rule TEXT NOT NULL, + reason TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS audit_bash ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp TEXT NOT NULL, + command TEXT NOT NULL, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + exit_code INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS ix_api_tokens_user ON api_tokens(user_id); +CREATE INDEX IF NOT EXISTS ix_api_tokens_hash ON api_tokens(token_hash); +CREATE INDEX IF NOT EXISTS ix_audit_governance_session ON audit_governance(session_id); +CREATE INDEX IF NOT EXISTS ix_audit_bash_session ON audit_bash(session_id); + +-- ── V002 Sessions ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + project_id TEXT, + model TEXT, + status TEXT NOT NULL DEFAULT 'active', + title TEXT, + mcp_servers TEXT, + started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + ended_at TEXT, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS session_entries ( + entry_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE, + entry_uid TEXT NOT NULL, + timestamp TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + model TEXT, + provider TEXT, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + tool_name TEXT, + tool_use_id TEXT, + is_error INTEGER NOT NULL DEFAULT 0, + -- Full-text search via PostgreSQL tsvector (replaces SQLite FTS5) + search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED, + CONSTRAINT uq_session_entries_uid UNIQUE (session_id, entry_uid) +); + +CREATE TABLE IF NOT EXISTS token_usage ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + project_id TEXT, + model TEXT NOT NULL, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cost_usd DOUBLE PRECISION, + recorded_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_sessions_user ON sessions(user_id); +CREATE INDEX IF NOT EXISTS ix_sessions_status ON sessions(status); +CREATE INDEX IF NOT EXISTS ix_sessions_workspace ON sessions(workspace_id); +CREATE INDEX IF NOT EXISTS ix_sessions_project ON sessions(project_id); +CREATE INDEX IF NOT EXISTS ix_session_entries_session ON session_entries(session_id); +CREATE INDEX IF NOT EXISTS ix_session_entries_fts ON session_entries USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS ix_token_usage_session ON token_usage(session_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_user ON token_usage(user_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_workspace ON token_usage(workspace_id); +CREATE INDEX IF NOT EXISTS ix_token_usage_project ON token_usage(project_id); + +-- ── V003 Memory ─────────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS session_summaries ( + session_id TEXT PRIMARY KEY, + project TEXT NOT NULL, + workspace_id TEXT, + started_at TEXT NOT NULL, + ended_at TEXT NOT NULL, + tasks TEXT NOT NULL DEFAULT '[]', + tools_used TEXT NOT NULL DEFAULT '[]', + files_modified TEXT NOT NULL DEFAULT '[]', + outcome TEXT NOT NULL DEFAULT 'Unknown', + total_input_tokens INTEGER NOT NULL DEFAULT 0, + total_output_tokens INTEGER NOT NULL DEFAULT 0, + turn_count INTEGER NOT NULL DEFAULT 0, + error_count INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS learned_patterns ( + id TEXT PRIMARY KEY, + pattern TEXT NOT NULL, + project TEXT NOT NULL, + source_session TEXT, + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + last_used TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS instincts ( + id TEXT PRIMARY KEY, + trigger TEXT NOT NULL, + action TEXT NOT NULL, + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, + evidence TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_session_summaries_project ON session_summaries(project); +CREATE INDEX IF NOT EXISTS ix_session_summaries_workspace ON session_summaries(workspace_id); +CREATE INDEX IF NOT EXISTS ix_learned_patterns_project ON learned_patterns(project); + +-- ── V004 Credentials ────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS credentials ( + key_hash TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT '', + workspace_id TEXT, + nonce BYTEA NOT NULL, + tag BYTEA NOT NULL, + ciphertext BYTEA NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V005 Swarm + Evals ──────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS swarm_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + swarm_id TEXT NOT NULL, + event_type TEXT NOT NULL, + agent_id TEXT, + workspace_id TEXT, + project_id TEXT, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + kind TEXT NOT NULL DEFAULT 'swarm', + run_id TEXT, + user_id TEXT, + parent_swarm_id TEXT +); + +CREATE TABLE IF NOT EXISTS eval_runs ( + run_id TEXT PRIMARY KEY, + suite_name TEXT NOT NULL, + workspace_id TEXT, + started_at TEXT NOT NULL, + duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, + pass_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + pass_at_1_rate DOUBLE PRECISION NOT NULL DEFAULT 0, + total_passed INTEGER NOT NULL DEFAULT 0, + total_failed INTEGER NOT NULL DEFAULT 0, + total_skipped INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS eval_results ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + run_id TEXT NOT NULL REFERENCES eval_runs(run_id) ON DELETE CASCADE, + eval_name TEXT NOT NULL, + category TEXT NOT NULL, + grader_type TEXT NOT NULL, + passed INTEGER NOT NULL DEFAULT 0, + pass_at_1 INTEGER NOT NULL DEFAULT 0, + pass_count INTEGER NOT NULL DEFAULT 0, + attempt_count INTEGER NOT NULL DEFAULT 0, + average_score DOUBLE PRECISION, + duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, + skipped INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS ix_swarm_events_swarm ON swarm_events(swarm_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_workspace ON swarm_events(workspace_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_project ON swarm_events(project_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_run_id ON swarm_events(run_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_kind ON swarm_events(kind); +CREATE INDEX IF NOT EXISTS ix_swarm_events_user ON swarm_events(user_id); +CREATE INDEX IF NOT EXISTS ix_swarm_events_parent ON swarm_events(parent_swarm_id); +CREATE INDEX IF NOT EXISTS ix_eval_runs_suite ON eval_runs(suite_name); +CREATE INDEX IF NOT EXISTS ix_eval_runs_workspace ON eval_runs(workspace_id); +CREATE INDEX IF NOT EXISTS ix_eval_results_run ON eval_results(run_id); + +-- ── V006 Workspace Memory ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS workspace_memory ( + memory_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + layer TEXT NOT NULL, + content TEXT NOT NULL, + confidence DOUBLE PRECISION, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_workspace_memory_workspace ON workspace_memory(workspace_id); +CREATE INDEX IF NOT EXISTS ix_workspace_memory_layer ON workspace_memory(workspace_id, layer); +CREATE INDEX IF NOT EXISTS ix_workspace_memory_project ON workspace_memory(workspace_id, project_id); + +-- ── V010 Runtime Traces ─────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS runtime_traces ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + runtime_run_id TEXT NOT NULL, + plan_id TEXT NOT NULL, + plan_version INTEGER NOT NULL, + step_index INTEGER, + entry_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + session_id TEXT, + workspace_id TEXT, + project_id TEXT +); + +CREATE TABLE IF NOT EXISTS mission_scratchpad ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + mission_id TEXT NOT NULL, + step_index INTEGER NOT NULL, + agent_id TEXT, + namespace TEXT NOT NULL DEFAULT 'default', + key TEXT NOT NULL, + value TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + workspace_id TEXT, + project_id TEXT +); + +CREATE INDEX IF NOT EXISTS ix_runtime_traces_run ON runtime_traces(runtime_run_id, id); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_entry_type ON runtime_traces(entry_type); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_workspace ON runtime_traces(workspace_id); +CREATE INDEX IF NOT EXISTS ix_runtime_traces_project ON runtime_traces(project_id); +CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_mission ON mission_scratchpad(mission_id, step_index); +CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_workspace ON mission_scratchpad(workspace_id); + +-- ── V011 Missions ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS missions ( + id TEXT PRIMARY KEY, + goal TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'planning', + plan_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + completed_at TEXT, + session_id TEXT, + workspace_id TEXT, + project_id TEXT, + owner_user_id TEXT +); + +CREATE TABLE IF NOT EXISTS mission_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + mission_id TEXT NOT NULL, + event_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + workspace_id TEXT, + project_id TEXT +); + +CREATE INDEX IF NOT EXISTS ix_missions_status ON missions(status); +CREATE INDEX IF NOT EXISTS ix_missions_workspace ON missions(workspace_id); +CREATE INDEX IF NOT EXISTS ix_missions_project ON missions(project_id); +CREATE INDEX IF NOT EXISTS ix_missions_owner ON missions(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_mission_events_mission ON mission_events(mission_id, id); +CREATE INDEX IF NOT EXISTS ix_mission_events_workspace ON mission_events(workspace_id); + +-- ── V012 Unified Orchestration ──────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS teams ( + team_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL, + project_id TEXT, + name TEXT NOT NULL, + description TEXT, + origin TEXT NOT NULL DEFAULT 'user', + created_by TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + run_mode TEXT NOT NULL DEFAULT 'sequential', + max_concurrent INTEGER NOT NULL DEFAULT 1, + file_locks_enabled INTEGER NOT NULL DEFAULT 0, + quality_gate_enabled INTEGER NOT NULL DEFAULT 0, + quality_gate_threshold INTEGER NOT NULL DEFAULT 7, + decomposition_mode TEXT NOT NULL DEFAULT 'off' +); + +CREATE TABLE IF NOT EXISTS team_members ( + member_id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(team_id) ON DELETE CASCADE, + workspace_id TEXT NOT NULL, + project_id TEXT, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'general', + template TEXT, + system_prompt TEXT, + allowed_tools TEXT, + model_level TEXT, + created_by TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + last_used_at TEXT, + status TEXT NOT NULL DEFAULT 'active' +); + +CREATE TABLE IF NOT EXISTS agent_runs ( + run_id TEXT PRIMARY KEY, + parent_run_id TEXT, + team_id TEXT, + member_id TEXT, + workspace_id TEXT NOT NULL, + project_id TEXT, + user_id TEXT NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'queued', + started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + ended_at TEXT, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cost_usd DOUBLE PRECISION, + prompt TEXT +); + +CREATE INDEX IF NOT EXISTS ix_teams_workspace ON teams(workspace_id); +CREATE INDEX IF NOT EXISTS ix_teams_project ON teams(project_id); +CREATE INDEX IF NOT EXISTS ix_teams_name ON teams(workspace_id, name); +CREATE INDEX IF NOT EXISTS ix_team_members_team ON team_members(team_id); +CREATE INDEX IF NOT EXISTS ix_team_members_workspace ON team_members(workspace_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_parent ON agent_runs(parent_run_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_team ON agent_runs(team_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_workspace ON agent_runs(workspace_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_user ON agent_runs(user_id); +CREATE INDEX IF NOT EXISTS ix_agent_runs_kind ON agent_runs(kind); +CREATE INDEX IF NOT EXISTS ix_agent_runs_status ON agent_runs(status); + +-- ── V013 Coordination Mailbox ───────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS coordination_events ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + source_group_id TEXT NOT NULL, + target_group_id TEXT NOT NULL, + event_type TEXT NOT NULL, + payload TEXT NOT NULL DEFAULT '{}', + status TEXT NOT NULL DEFAULT 'pending', + workspace_id TEXT NOT NULL, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + delivered_at TEXT, + acknowledged_at TEXT +); + +CREATE TABLE IF NOT EXISTS group_pm_assignments ( + group_id TEXT PRIMARY KEY, + group_type TEXT NOT NULL, + pm_template TEXT NOT NULL DEFAULT 'pm-coordinator', + workspace_id TEXT NOT NULL, + project_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS ix_coordination_events_target ON coordination_events(target_group_id, status); +CREATE INDEX IF NOT EXISTS ix_coordination_events_source ON coordination_events(source_group_id); +CREATE INDEX IF NOT EXISTS ix_coordination_events_ws ON coordination_events(workspace_id); +CREATE INDEX IF NOT EXISTS ix_group_pm_assignments_ws ON group_pm_assignments(workspace_id); + +-- ── V017 Hooks ──────────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS hooks ( + hook_id TEXT PRIMARY KEY, + event TEXT NOT NULL, + command TEXT NOT NULL, + matcher_tool TEXT, + matcher_glob TEXT, + timeout_ms INTEGER NOT NULL DEFAULT 30000, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS hooks_event_enabled_idx ON hooks(event) WHERE enabled = 1; + +-- ── V018 Workspace Settings ─────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS workspace_settings ( + workspace_id TEXT NOT NULL DEFAULT '', + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (workspace_id, key) +); + +CREATE INDEX IF NOT EXISTS workspace_settings_key_idx ON workspace_settings(key); + +-- ── V019 MCP/LSP Servers ────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS mcp_servers ( + id TEXT NOT NULL DEFAULT '', + name TEXT PRIMARY KEY, + command TEXT NOT NULL DEFAULT '', + args_json TEXT NOT NULL DEFAULT '[]', + env_json TEXT NOT NULL DEFAULT '{}', + oauth_config_json TEXT, + url TEXT, + headers_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; + +CREATE TABLE IF NOT EXISTS lsp_servers ( + language TEXT PRIMARY KEY, + command TEXT NOT NULL DEFAULT '', + args_json TEXT NOT NULL DEFAULT '[]', + env_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V020 User Preferences ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS user_preferences ( + user_id TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + PRIMARY KEY (user_id, key) +); + +CREATE INDEX IF NOT EXISTS user_preferences_key_idx ON user_preferences(key); + +-- ── V021 Provider Profiles ──────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS provider_profiles ( + profile_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + provider_kind TEXT NOT NULL, + base_url TEXT NOT NULL, + default_model TEXT, + max_tokens INTEGER, + credential_id TEXT NOT NULL, + workspace_id TEXT, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE INDEX IF NOT EXISTS provider_profiles_user_idx ON provider_profiles(user_id); +CREATE INDEX IF NOT EXISTS provider_profiles_workspace_idx ON provider_profiles(workspace_id) + WHERE workspace_id IS NOT NULL; + +-- ── V026 Auth Credentials ───────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS server_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +CREATE TABLE IF NOT EXISTS password_reset_tokens ( + token_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + token_hash TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + expires_at TEXT NOT NULL, + used_at TEXT, + CONSTRAINT prt_hash_unique UNIQUE (token_hash) +); + +CREATE INDEX IF NOT EXISTS ix_prt_user ON password_reset_tokens(user_id); +CREATE INDEX IF NOT EXISTS ix_prt_hash ON password_reset_tokens(token_hash); + +-- ── V030 Activity is Private ────────────────────────────────────────────────── + +ALTER TABLE missions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; +ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; +ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS ix_missions_is_private ON missions(is_private); +CREATE INDEX IF NOT EXISTS ix_agent_runs_is_private ON agent_runs(is_private); +CREATE INDEX IF NOT EXISTS ix_sessions_is_private ON sessions(is_private); + +-- ── V031 Session Agent Name ─────────────────────────────────────────────────── + +ALTER TABLE sessions ADD COLUMN IF NOT EXISTS agent_name TEXT; + +CREATE INDEX IF NOT EXISTS idx_sessions_agent_name ON sessions(agent_name) WHERE agent_name IS NOT NULL; + +-- ── V032 MCP Trust Rules ────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS mcp_trust_rules ( + rule_id TEXT NOT NULL PRIMARY KEY, + workspace_id TEXT NOT NULL, + server_name TEXT NOT NULL, -- exact server name or '*' (all servers) + tool_pattern TEXT NOT NULL, -- glob: 'delete_*', 'bulk_*', '*' + action TEXT NOT NULL, -- Allow | RequireConfirmation | Block + reason TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_workspace ON mcp_trust_rules(workspace_id); +CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_server ON mcp_trust_rules(server_name); + +-- ── V033/V034/V036 Knowledge Pages ─────────────────────────────────────────── +-- Combines V033 foundation, V034 agent columns, and V036 document-template +-- columns into one table definition (all columns present on fresh installs). +-- V035 and V037 are seed-data-only migrations handled by the app at startup. + +CREATE TABLE IF NOT EXISTS knowledge_pages ( + knowledge_id TEXT NOT NULL PRIMARY KEY, + kind TEXT NOT NULL, -- 'skills' | 'agents' | 'documents' | 'tools' | 'document-templates' + slug TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + tier TEXT NOT NULL DEFAULT 'User', + -- skills-specific + trigger TEXT, + agents TEXT, -- JSON array + tools TEXT, -- JSON array + -- documents-specific + industry TEXT, + default_format TEXT, + -- tool-templates-specific + category TEXT, + -- agent-specific (V034) + role TEXT, + recommended_level TEXT, + -- document-template-specific (V036) + fields_json TEXT, + filename_template TEXT, + -- content + body TEXT NOT NULL DEFAULT '', + -- ownership / scope + workspace_id TEXT NOT NULL DEFAULT '', + -- audit + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + UNIQUE (kind, slug, workspace_id) +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_pages_kind ON knowledge_pages(kind); +CREATE INDEX IF NOT EXISTS idx_knowledge_pages_workspace ON knowledge_pages(workspace_id); + +-- Additive guards for databases that were initialised before this combined +-- definition — safe no-ops on fresh installs. +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS role TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS recommended_level TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS fields_json TEXT; +ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS filename_template TEXT; + +-- ── V038 Knowledge Attributions ─────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS knowledge_attributions ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + session_id TEXT NOT NULL, + turn_index INTEGER NOT NULL, + kind TEXT NOT NULL, -- 'skills', 'agents', 'document-templates', 'tools' + slug TEXT NOT NULL, + used_at TEXT NOT NULL -- ISO 8601 +); + +CREATE INDEX IF NOT EXISTS idx_knowledge_attributions_session ON knowledge_attributions(session_id); + +-- ── V039 Keystore ───────────────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS keystore ( + scope TEXT NOT NULL PRIMARY KEY, -- 'default' (reserved for future per-workspace keys) + key_hex TEXT NOT NULL, -- 64-char lowercase hex (256-bit AES master key) + created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) +); + +-- ── V040 MCP Server stable ID ──────────────────────────────────────────────── +-- Fresh installs already have id + index via the V019 table definition above. +-- These statements are no-ops on fresh installs; they fix upgrade installs that +-- ran before V040 and have id = '' on all existing mcp_servers rows. + +ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS id TEXT NOT NULL DEFAULT ''; +CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; +UPDATE mcp_servers SET id = gen_random_uuid()::text WHERE id = ''; + +-- ── V041 Workspace Memory Privacy ──────────────────────────────────────────── +-- owner_user_id = '' means unowned/legacy: visible to ALL authenticated users +-- via the load filter (owner_user_id = '' OR owner_user_id = $uid). +-- Non-empty means scoped to that specific user only. + +ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX IF NOT EXISTS ix_workspace_memory_owner ON workspace_memory(owner_user_id); + +-- ── V042 Memory Owner User ID ───────────────────────────────────────────────── +-- Scopes auto-generated memories (session summaries, patterns, instincts) to +-- the session owner so they are not mixed across users in a multi-user deployment. +-- Same owner_user_id = '' convention as V041: empty = legacy/global, non-empty = user-scoped. + +ALTER TABLE session_summaries ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE learned_patterns ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; +ALTER TABLE instincts ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; + +CREATE INDEX IF NOT EXISTS ix_session_summaries_owner ON session_summaries(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_learned_patterns_owner ON learned_patterns(owner_user_id); +CREATE INDEX IF NOT EXISTS ix_instincts_owner ON instincts(owner_user_id); + +-- ── V043 Drop username column ───────────────────────────────────────────────── +-- For instances initialized before V043. Fresh installs never had this column. +-- V043's PK rewrite (usr_{hex} → email) applies to SQLite standalone only. + +ALTER TABLE public.users DROP COLUMN IF EXISTS username; + +-- ── Schema version tracking ─────────────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS sovrant_schema_version ( + id INTEGER PRIMARY KEY DEFAULT 1, + version INTEGER NOT NULL, + applied_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), + CONSTRAINT single_row CHECK (id = 1) +); + +INSERT INTO sovrant_schema_version (id, version) +VALUES (1, 43) +ON CONFLICT (id) DO UPDATE SET version = EXCLUDED.version, applied_at = EXCLUDED.applied_at; diff --git a/db/supabase/migrations/20260625000000_initial_schema.sql b/db/supabase/migrations/20260625000000_initial_schema.sql index b649068e..2cee1e1c 100644 --- a/db/supabase/migrations/20260625000000_initial_schema.sql +++ b/db/supabase/migrations/20260625000000_initial_schema.sql @@ -1,6 +1,6 @@ -- Sovrant initial schema for Supabase deployments. -- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). --- For standalone PostgreSQL use src/Sovrant.Runtime/Storage/PostgresSchema.sql instead. +-- For standalone PostgreSQL use db/postgres/PostgresSchema.sql instead. -- -- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. -- BYTEA used for encrypted blobs (credentials table). diff --git a/src/Sovrant.Runtime/Sovrant.Runtime.csproj b/src/Sovrant.Runtime/Sovrant.Runtime.csproj index b56fa2e8..c40b83bd 100644 --- a/src/Sovrant.Runtime/Sovrant.Runtime.csproj +++ b/src/Sovrant.Runtime/Sovrant.Runtime.csproj @@ -26,7 +26,7 @@ - + From eeabea3fbb8c143008acd881af015e4c58a59438 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 08:49:01 -0400 Subject: [PATCH 04/24] Update persistence.md for V043, db/ layout, and admin schema customization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schema version V042 → V043 throughout - All PostgresSchema.sql path references → db/postgres/ and db/supabase/migrations/ - File layout section replaced with db/ directory structure explanation - Admin customization workflow added to Supabase setup guide - Standalone Postgres setup guide no longer warns about Supabase section - Trigger pseudocode: removed stale username from INSERT - Pending implementation table updated to reflect db/ split as done Co-Authored-By: Claude Sonnet 4.6 --- docs/persistence.md | 86 ++++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/docs/persistence.md b/docs/persistence.md index 6a48a267..ffadc12b 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -1,6 +1,6 @@ # Sovrant — Persistence Layer -**Phases 32–42.5, 51, 52, 55, 57, 78, 85, 87, 88, 90, 93, 98, 108–116, 123, 124** | **Last updated:** 2026-06-18 | **Current schema:** V042 +**Phases 32–42.5, 51, 52, 55, 57, 78, 85, 87, 88, 90, 93, 98, 108–116, 123–126** | **Last updated:** 2026-06-25 | **Current schema:** V043 This document describes how Sovrant stores durable operational data. All persistent state (sessions, memory, audit, credentials, token usage, workspaces, projects, users, knowledge, hooks, MCP/LSP config) is managed by a relational database. Three deployment modes are supported: @@ -107,8 +107,9 @@ Migrations are embedded SQL resources named `V{NNN}__{description}.sql` inside t | V040 | `V040__mcp_server_id.sql` | Adds stable `id` column to `mcp_servers` (UUID surrogate that survives renames; `name` remains the routing key) | | V041 | `V041__workspace_memory_privacy.sql` | Adds `owner_user_id` + `is_private` to `workspace_memory` for per-user note privacy | | V042 | `V042__memory_owner_user_id.sql` | Adds `owner_user_id` to `session_summaries`, `learned_patterns`, `instincts` so auto-generated memories are scoped to their session owner | +| V043 | `V043__email_as_user_id.sql` | Rewrites `usr_{hex}` primary keys to email addresses; drops `username` column via table recreation | -V008, V009, V022, V035, V037 ship no new tables — they are data backfills or seed inserts. V014–V016, V023–V025, V027–V031, V034, V036, V040–V042 add only columns to existing tables. +V008, V009, V022, V035, V037 ship no new tables — they are data backfills or seed inserts. V014–V016, V023–V025, V027–V031, V034, V036, V040–V043 add only columns to existing tables. Migrations are idempotent — running `InitializeAsync` multiple times is safe. The runner skips already-applied versions and records the SHA-256 checksum of each script in `schema_version.checksum`. Checksum drift is enforced: if a previously-applied `V00X__*.sql` file has been edited in place, `InitializeAsync` throws `MigrationDriftException` on the next boot. Legacy rows with `checksum = NULL` are tolerated so pre-42.5 installs upgrade cleanly. @@ -191,13 +192,13 @@ A failing probe flips `db.status` to `"error"` and overall `status` to `"degrade ## Database Inventory (authoritative) -The current schema spans **42 migrations (V001–V042)**. The tables below reflect the schema as of V042. +The current schema spans **43 migrations (V001–V043)**. The tables below reflect the schema as of V043. ### Tables by purpose | Category | Tables | First migration | Notes | |---|---|---|---| -| **Identity & access** | `users`, `api_tokens`, `roles`, `permissions`, `role_permissions`, `user_roles` | V001 | `password_hash` added V026. `api_tokens` is live (Phase 38). RBAC tables populated (Phase 40). | +| **Identity & access** | `users`, `api_tokens`, `roles`, `permissions`, `role_permissions`, `user_roles` | V001 | `password_hash` added V026. `username` column dropped V043 (email is now the PK). `api_tokens` is live (Phase 38). RBAC tables populated (Phase 40). | | **Auth extras** | `server_settings`, `password_reset_tokens` | V026 | `server_settings`: key/value store for auth bootstrap. `password_reset_tokens`: local-auth only; inert in Supabase mode. | | **Workspaces** | `workspaces`, `workspace_members`, `workspace_config`, `workspace_invites`, `workspace_memory` | V001 + V006 | `workspace_memory` gained `owner_user_id` + `is_private` (V041) for per-user note privacy. | | **Projects** | `projects`, `project_members`, `project_config` | V001 | V007 adds indexes only. | @@ -449,14 +450,14 @@ Sovrant supports three storage deployment modes. The SQLite schema described abo | Token resolution | `SqliteTokenService` | `PostgresTokenService` *(planned)* | JWT JWKS validation + `svt_*` fallback | | `password_reset_tokens` | used | used | inert (Supabase handles resets) | | FK tables | reference `public.users` | same | same — mirror trigger ensures row exists first | -| Schema bootstrap | V-series migration runner | `PostgresSchema.sql` (base section, manual) | `PostgresSchema.sql` (full file inc. Supabase Auth Extension section) | -| Row-level security | n/a | optional | Commented-out policies in `PostgresSchema.sql` Supabase Auth Extension section | +| Schema bootstrap | V-series migration runner | `db/postgres/PostgresSchema.sql` (manual `psql`) | `db/supabase/migrations/` via Supabase CLI | +| Row-level security | n/a | optional | Commented-out policies in `db/supabase/migrations/20260625000000_initial_schema.sql` | ### How Supabase Auth fits Supabase Auth stores identities in `auth.users` (UUID PK, managed by GoTrue). The canonical user identity throughout the app is a `user_id TEXT` column. In Supabase mode this TEXT value is `auth.users.id::TEXT` (a UUID string) — fully compatible with existing FK columns and `owner_user_id` memory columns without any type changes. -Two mirror triggers (in the **SUPABASE AUTH EXTENSION** section at the bottom of `PostgresSchema.sql`) propagate changes from `auth.users` to `public.users`: +Two mirror triggers (in `db/supabase/migrations/20260625000000_initial_schema.sql`) propagate changes from `auth.users` to `public.users`: ```sql -- on_auth_user_created: fires on new sign-up @@ -469,8 +470,8 @@ BEGIN WHEN NEW.raw_app_meta_data->>'sovrant_role' = 'admin' THEN 'admin' ELSE 'user' END; - INSERT INTO public.users (user_id, username, email, role, ...) - VALUES (NEW.id::TEXT, ..., _role, ...) + INSERT INTO public.users (user_id, email, role, ...) + VALUES (NEW.id::TEXT, NEW.email, _role, ...) ON CONFLICT (user_id) DO NOTHING; END; $$; @@ -494,13 +495,29 @@ This means every FK reference to `users(user_id)` continues to work without sche ### File layout -`src/Sovrant.Runtime/Storage/PostgresSchema.sql` — **single file for all Postgres deployments**. Divided into two sections: +All database files live under `db/` in the repo root: -1. **Base schema** (always run) — full `public.users` with `password_hash`, all FK/memory/knowledge tables, V041/V042 `ADD COLUMN IF NOT EXISTS` guards, V040 `mcp_servers.id` backfill. +``` +db/ + postgres/ + PostgresSchema.sql ← standalone Postgres (also embedded in Runtime DLL) + supabase/ + config.toml ← Supabase CLI project config + migrations/ + 20260625000000_initial_schema.sql ← full schema + GoTrue mirror triggers + RLS stubs +``` + +**Standalone Postgres** (`db/postgres/PostgresSchema.sql`) — base schema only; no Supabase-specific sections. Run once with `psql` to bootstrap; re-run on upgrades (idempotent). Also embedded in `Sovrant.Runtime.dll` so the runtime can auto-initialize a fresh Postgres database without external files present. -2. **SUPABASE AUTH EXTENSION** (Supabase only, clearly marked at the bottom) — the two mirror triggers (`on_auth_user_created`, `on_auth_user_updated`), commented-out RLS policies, and deployment notes. **Standalone Postgres deployments skip this section.** +**Supabase** (`db/supabase/migrations/`) — full schema plus the GoTrue mirror triggers (`on_auth_user_created`, `on_auth_user_updated`) and commented-out RLS policies. Managed by the Supabase CLI — run `supabase db push` from `db/supabase/`. Admins can layer their own customizations by adding new numbered migration files after the initial one: + +``` +db/supabase/migrations/ + 20260625000000_initial_schema.sql ← Sovrant base (don't edit) + 20260625000001_my_org_additions.sql ← admin customizations layered on top +``` -There is no separate `SupabaseAuth.sql` file — the Supabase-specific SQL lives inline in `PostgresSchema.sql` behind the section header. When running in the Supabase SQL editor, run the entire file; Supabase-only objects only apply inside a Supabase project where `auth.users` exists. +The Supabase CLI applies migrations in timestamp order so admin additions never conflict with future Sovrant upgrades as long as they stay additive. ### What changes in auth middleware @@ -522,9 +539,9 @@ Because the role is embedded in the JWT by Supabase (from `raw_app_meta_data`), | Item | Status | |---|---| -| `PostgresSchema.sql` base schema (V001–V042 parity + V040 backfill) | **Done** | -| Supabase mirror triggers (`on_auth_user_created`, `on_auth_user_updated`) with Option A role from `app_metadata` | **Done** (SUPABASE AUTH EXTENSION section in `PostgresSchema.sql`) | -| RLS policies for memory privacy at DB layer | Skeleton commented out in `PostgresSchema.sql` — **Planned** to enable | +| `db/postgres/PostgresSchema.sql` base schema (V001–V043 parity + V040 backfill) | **Done** | +| `db/supabase/migrations/` Supabase CLI migration with GoTrue mirror triggers + RLS stubs | **Done** | +| RLS policies for memory privacy at DB layer | Skeleton commented out in Supabase migration — **Planned** to enable | | `PostgresTokenService` (mirrors `SqliteTokenService` for standalone Postgres) | **Planned** | | Postgres V-series migration runner (equivalent to SQLite `MigrationRunner`) | **Planned** | | JWT validation in `BearerTokenMiddleware` | **Planned** | @@ -571,10 +588,10 @@ export SOVRANT_POSTGRES_URL="postgres://sovrant_app:yourpassword@localhost:5432/ **3. Bootstrap the schema (one-time, run from the repo):** ```bash -psql $SOVRANT_POSTGRES_URL -f src/Sovrant.Runtime/Storage/PostgresSchema.sql +psql $SOVRANT_POSTGRES_URL -f db/postgres/PostgresSchema.sql ``` -> **Stop before the SUPABASE AUTH EXTENSION section.** Standalone Postgres skips everything after the `══ SUPABASE AUTH EXTENSION ══` divider at the bottom of the file. You can verify this by checking that `auth.users` does not exist in your database — the section's `CREATE TRIGGER ... ON auth.users` will error if run on a plain Postgres instance. +The standalone schema has no Supabase-specific sections — it is safe to run in full against any plain Postgres instance. **4. Start Sovrant.** It detects Postgres via `SOVRANT_POSTGRES_URL`. On first boot, `SeedDefaultUser` and `SeedPersonalWorkspace` run automatically (same as SQLite). @@ -588,11 +605,22 @@ psql $SOVRANT_POSTGRES_URL -f src/Sovrant.Runtime/Storage/PostgresSchema.sql Prerequisites: Supabase project (cloud at [supabase.com](https://supabase.com) or self-hosted via `supabase start`). -**1. Run `PostgresSchema.sql` in the Supabase SQL editor.** +**1. Apply the Sovrant migration via the Supabase CLI (recommended):** +```bash +cd db/supabase +supabase link --project-ref +supabase db push +``` + +Or paste the contents of `db/supabase/migrations/20260625000000_initial_schema.sql` directly into the Supabase SQL Editor. `auth.users` exists in every Supabase project so there are no errors. -Open your project → SQL Editor → New query → paste the full contents of `src/Sovrant.Runtime/Storage/PostgresSchema.sql` → Run. This includes the SUPABASE AUTH EXTENSION section at the bottom which creates the mirror triggers. `auth.users` exists in every Supabase project so there are no errors. +**Customizing the schema** — add your own migrations after the initial one: +``` +db/supabase/migrations/20260625000001_my_org_additions.sql +``` +The CLI applies them in timestamp order. Keep customizations additive so future Sovrant upgrades layer cleanly on top. -**2. (Optional) Enable Row Level Security** — uncomment and run the RLS block at the very end of the file. This enforces `owner_user_id` privacy at the database layer (not just application layer), which is the recommended posture for multi-user Supabase deployments. The commented policies are: +**2. (Optional) Enable Row Level Security** — uncomment the RLS block at the end of `20260625000000_initial_schema.sql`. This enforces `owner_user_id` privacy at the database layer (not just application layer), which is the recommended posture for multi-user Supabase deployments. The commented policies are: - `workspace_memory`: `USING (is_private = 0 OR owner_user_id = auth.uid()::text)` - `session_summaries`, `learned_patterns`, `instincts`: `USING (owner_user_id = '' OR owner_user_id = auth.uid()::text)` @@ -662,7 +690,7 @@ export SUPABASE_SERVICE_ROLE_KEY="your-service-key" # secret — server-side o | Credential at rest | AES-256-GCM encryption; master key in `keystore` table (DB) since V039 | | Concurrent access | WAL mode + `busy_timeout=5000` allows CLI and server to share the same DB file | | Server auth | All HTTP endpoints require `Authorization: Bearer`; SQLite DB never directly exposed | -| Memory privacy | `owner_user_id` scoping enforced at app layer (query filter); no DB-level RLS on SQLite. Supabase deployments can enable RLS policies (commented out in `PostgresSchema.sql`) to enforce privacy at the DB layer. | +| Memory privacy | `owner_user_id` scoping enforced at app layer (query filter); no DB-level RLS on SQLite. Supabase deployments can enable RLS policies (commented out in `db/supabase/migrations/20260625000000_initial_schema.sql`) to enforce privacy at the DB layer. | | Role elevation (Supabase) | `raw_app_meta_data.sovrant_role` is writable only by service-role callers (GoTrue enforces); users cannot self-elevate. Role is embedded in the signed JWT — tamper-proof in transit. | --- @@ -708,13 +736,13 @@ export SUPABASE_SERVICE_ROLE_KEY="your-service-key" # secret — server-side o | 5 | **No backup-before-migrate.** Migration applied with no snapshot. | **✓ Resolved (Phase 42.5).** `SOVRANT_DB_BACKUP_ON_UPGRADE=true` checkpoints + copies before migrations. | | 6 | **Migration checksum drift not enforced.** | **✓ Resolved (Phase 42.5).** `MigrationDriftException` thrown on any mismatch. | | 8 | **Empty `user_id` defaults.** Sessions written with `user_id = ''`. | **✓ Resolved (Phase 38 + V009 backfill).** | -| 13 | **`PostgresSchema.sql` has no migration runner.** One-shot manual script; Supabase instances bootstrapped before V041/V042 silently lack the privacy columns. Existing Postgres instances not re-run against the updated script will have no `owner_user_id` on memory tables → INSERT failures at runtime. | **Planned** — Postgres V-series migration runner needed; or publish discrete upgrade scripts per release (e.g. `V041_upgrade.sql`). | -| 14 | **Memory privacy enforced at app layer only.** `owner_user_id` filter is in application query logic, not DB RLS. Direct Supabase dashboard, service-role queries, or edge functions bypass it entirely. | **Partially done** — RLS policy skeletons are in the `PostgresSchema.sql` Supabase Auth Extension section (commented out). Uncomment and run to enforce. Full enforcement requires all Supabase Edge Functions to also use `auth.uid()`. | -| 15 | **Six tables with `REFERENCES users(user_id)` block Supabase Auth adoption.** `workspaces`, `workspace_members`, `project_members`, `api_tokens`, `user_roles`, `password_reset_tokens`. Without the mirror trigger these FK inserts fail because `public.users` has no row. | **✓ Resolved (2026-06-18).** `on_auth_user_created` mirror trigger in `PostgresSchema.sql` SUPABASE AUTH EXTENSION section ensures `public.users` row is created before any FK-dependent inserts. | +| 13 | **`db/postgres/PostgresSchema.sql` has no migration runner.** One-shot manual script; Supabase instances bootstrapped before V041/V042 silently lack the privacy columns. Existing Postgres instances not re-run against the updated script will have no `owner_user_id` on memory tables → INSERT failures at runtime. | **Planned** — Postgres V-series migration runner needed; or publish discrete upgrade scripts per release (e.g. `V041_upgrade.sql`). | +| 14 | **Memory privacy enforced at app layer only.** `owner_user_id` filter is in application query logic, not DB RLS. Direct Supabase dashboard, service-role queries, or edge functions bypass it entirely. | **Partially done** — RLS policy skeletons are in `db/supabase/migrations/20260625000000_initial_schema.sql` (commented out). Uncomment and run to enforce. Full enforcement requires all Supabase Edge Functions to also use `auth.uid()`. | +| 15 | **Six tables with `REFERENCES users(user_id)` block Supabase Auth adoption.** `workspaces`, `workspace_members`, `project_members`, `api_tokens`, `user_roles`, `password_reset_tokens`. Without the mirror trigger these FK inserts fail because `public.users` has no row. | **✓ Resolved (2026-06-18).** `on_auth_user_created` mirror trigger in the Supabase migration ensures `public.users` row is created before any FK-dependent inserts. | | 16 | **`/remember` command writes patterns/instincts with `owner_user_id = ''`** making them visible to all users. | **✓ Resolved (2026-06-18).** `ownerUserId` now threaded through `SlashCommandDispatcher.TryDispatchAsync` → `RememberCommand`. | | 17 | **`GET /workspaces/{id}/memory` returned private entries to any member.** `viewerUserId` was not passed to `ListMemoryAsync`. | **✓ Resolved (2026-06-18).** Route now passes `viewerUserId: ctx.GetUserId()`. | | 18 | **SQL query fragments built via string interpolation** in `SqliteMemoryStore` and `SqliteWorkspaceStore` for optional owner filters. Currently safe (hardcoded literal strings) but establishes a risky pattern. | Deferred — replace with static tautology queries: `AND ($uid IS NULL OR owner_user_id = '' OR owner_user_id = $uid)`. | -| 19 | **`PostgresSchema.sql` V040 backfill gap.** V040 backfills `mcp_servers.id` with `randomblob(16)` in SQLite. No equivalent UPDATE in `PostgresSchema.sql` for upgrade installs — existing Postgres rows keep `id=''`. | **✓ Resolved (2026-06-18).** `UPDATE mcp_servers SET id = gen_random_uuid()::text WHERE id = '';` added to the V040 section of `PostgresSchema.sql`. | +| 19 | **`db/postgres/PostgresSchema.sql` V040 backfill gap.** V040 backfills `mcp_servers.id` with `randomblob(16)` in SQLite. No equivalent UPDATE in the Postgres schema for upgrade installs — existing Postgres rows kept `id=''`. | **✓ Resolved (2026-06-18).** `UPDATE mcp_servers SET id = gen_random_uuid()::text WHERE id = '';` added to the V040 section of both Postgres schema files. | | 9 | **No shared bootstrap helper.** Test fixtures re-implement parts of the boot flow. | Deferred. | | 10 | **Connection-per-call with no pool.** | Deferred — benchmark before adding pooling. | | 11 | **No `sovrant init` first-boot UX.** | Partially addressed — `sovrant db status` covers it. | @@ -732,7 +760,7 @@ The persistence layer is exercised by the full solution test suite (**2,222 test | `SqliteMemoryStoreTests` | Summaries, patterns, instincts, reinforcement, correction, pruning, owner scoping | | `SqliteAuditStoreTests` | Governance events, bash commands, batch writes | | `SqliteTokenUsageStoreTests` | Record/aggregate, empty session, cost tracking | -| `MigrationRunnerTests` | All V001–V042 migrations apply in order; idempotency; expected tables present; backfill behavior | +| `MigrationRunnerTests` | All V001–V043 migrations apply in order; idempotency; expected tables present; backfill behavior | | `SqliteWorkspaceStoreTests` | Workspace CRUD, personal-workspace idempotency, members, invites, config, memory with privacy filtering | | `SqliteProjectStoreTests` | Project CRUD, archive/unarchive, open-by-default access, member roles, 3-tier config inheritance | | `SqliteUserStoreTests` | Server-generated IDs, validation, duplicate detection, soft-delete, FK preservation, usage aggregation | @@ -748,7 +776,7 @@ After a fresh install and first run, `~/.sovrant/` contains: ``` ~/.sovrant/ ├── data/ -│ └── sovrant.db ← SQLite database — all persistent state (V042 schema) +│ └── sovrant.db ← SQLite database — all persistent state (V043 schema) │ sessions, memory, audit, credentials, keystore, │ workspaces, projects, users, knowledge, hooks, │ MCP/LSP config, teams, missions, swarm, evals @@ -763,7 +791,7 @@ After a fresh install and first run, `~/.sovrant/` contains: Note: `credentials/.keystore` no longer exists on fresh installs (V039 moved the master key into `sovrant.db`'s `keystore` table). Existing installs had the file migrated and deleted on first V039 boot. A fresh boot with no existing DB produces: -- `data/sovrant.db` at schema version 42 (V001–V042 applied in order) +- `data/sovrant.db` at schema version 43 (V001–V043 applied in order) - A `users` row for `SOVRANT_USER_ID` (or OS username) via `SeedDefaultUser` - A `workspaces` row `ws-personal-{userId}` via `SeedPersonalWorkspace` - A `workspace_members` row linking the seeded user as `owner` From 29abb209e32b7c7a8c20ff1288784cb02de2d4ba Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 09:19:12 -0400 Subject: [PATCH 05/24] Remove PostgresSchema.sql from src/ (moved to db/postgres/ in prior commit) Co-Authored-By: Claude Sonnet 4.6 --- .../Storage/PostgresSchema.sql | 778 ------------------ 1 file changed, 778 deletions(-) delete mode 100644 src/Sovrant.Runtime/Storage/PostgresSchema.sql diff --git a/src/Sovrant.Runtime/Storage/PostgresSchema.sql b/src/Sovrant.Runtime/Storage/PostgresSchema.sql deleted file mode 100644 index 9bd4157c..00000000 --- a/src/Sovrant.Runtime/Storage/PostgresSchema.sql +++ /dev/null @@ -1,778 +0,0 @@ --- Sovrant standalone PostgreSQL schema. --- Mirrors V001–V043 SQLite migrations. Safe to run multiple times (idempotent). --- For Supabase deployments use supabase/migrations/ instead (includes GoTrue auth triggers). --- Timestamps are stored as TEXT (ISO 8601) for wire-compatibility with SQLite stores. --- BYTEA used for encrypted blobs (credentials table). --- V035/V037 (built-in knowledge seed data) are handled by the app at startup, not here. - --- ── Helper: UTC timestamp default ──────────────────────────────────────────── --- All timestamp columns that had SQLite strftime defaults use this expression. - --- ── V001 Foundation ─────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS users ( - user_id TEXT PRIMARY KEY, - email TEXT, - role TEXT NOT NULL DEFAULT 'user', - team TEXT, - status TEXT NOT NULL DEFAULT 'active', - password_hash TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT users_email_unique UNIQUE (email) -); - -CREATE TABLE IF NOT EXISTS workspaces ( - workspace_id TEXT PRIMARY KEY, - type TEXT NOT NULL DEFAULT 'personal', - name TEXT NOT NULL, - slug TEXT NOT NULL, - owner_id TEXT NOT NULL REFERENCES users(user_id), - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT workspaces_slug_unique UNIQUE (slug) -); - -CREATE TABLE IF NOT EXISTS workspace_members ( - workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), - user_id TEXT NOT NULL REFERENCES users(user_id), - role TEXT NOT NULL DEFAULT 'member', - joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (workspace_id, user_id) -); - -CREATE TABLE IF NOT EXISTS workspace_config ( - workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), - key TEXT NOT NULL, - value TEXT NOT NULL, - PRIMARY KEY (workspace_id, key) -); - -CREATE TABLE IF NOT EXISTS workspace_invites ( - invite_id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id), - email TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'member', - token TEXT NOT NULL, - expires_at TEXT NOT NULL, - accepted_at TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT workspace_invites_token_unique UNIQUE (token) -); - -CREATE TABLE IF NOT EXISTS projects ( - project_id TEXT PRIMARY KEY, - workspace_id TEXT REFERENCES workspaces(workspace_id), - name TEXT NOT NULL, - slug TEXT NOT NULL, - description TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - archived_at TEXT, - CONSTRAINT projects_ws_slug_unique UNIQUE (workspace_id, slug) -); - -CREATE TABLE IF NOT EXISTS project_members ( - project_id TEXT NOT NULL REFERENCES projects(project_id), - user_id TEXT NOT NULL REFERENCES users(user_id), - role TEXT NOT NULL DEFAULT 'contributor', - joined_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (project_id, user_id) -); - -CREATE TABLE IF NOT EXISTS project_config ( - project_id TEXT NOT NULL REFERENCES projects(project_id), - key TEXT NOT NULL, - value TEXT NOT NULL, - PRIMARY KEY (project_id, key) -); - -CREATE TABLE IF NOT EXISTS config ( - scope TEXT NOT NULL, - key TEXT NOT NULL, - value TEXT NOT NULL, - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (scope, key) -); - -CREATE TABLE IF NOT EXISTS api_tokens ( - token_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(user_id), - token_hash TEXT NOT NULL, - token_prefix TEXT NOT NULL, - name TEXT, - scopes TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - expires_at TEXT, - revoked_at TEXT, - last_used_at TEXT, - CONSTRAINT api_tokens_hash_unique UNIQUE (token_hash) -); - -CREATE TABLE IF NOT EXISTS roles ( - role_id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - is_system INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT roles_name_unique UNIQUE (name) -); - -CREATE TABLE IF NOT EXISTS permissions ( - permission_id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - CONSTRAINT permissions_name_unique UNIQUE (name) -); - -CREATE TABLE IF NOT EXISTS role_permissions ( - role_id TEXT NOT NULL REFERENCES roles(role_id), - permission_id TEXT NOT NULL REFERENCES permissions(permission_id), - PRIMARY KEY (role_id, permission_id) -); - -CREATE TABLE IF NOT EXISTS user_roles ( - user_id TEXT NOT NULL REFERENCES users(user_id), - role_id TEXT NOT NULL REFERENCES roles(role_id), - workspace_id TEXT NOT NULL DEFAULT '', - granted_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (user_id, role_id, workspace_id) -); - -CREATE TABLE IF NOT EXISTS audit_governance ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - timestamp TEXT NOT NULL, - phase TEXT NOT NULL, - tool TEXT NOT NULL, - session_id TEXT, - workspace_id TEXT, - project_id TEXT, - action TEXT NOT NULL, - rule TEXT NOT NULL, - reason TEXT NOT NULL -); - -CREATE TABLE IF NOT EXISTS audit_bash ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - timestamp TEXT NOT NULL, - command TEXT NOT NULL, - session_id TEXT, - workspace_id TEXT, - project_id TEXT, - exit_code INTEGER NOT NULL -); - -CREATE INDEX IF NOT EXISTS ix_api_tokens_user ON api_tokens(user_id); -CREATE INDEX IF NOT EXISTS ix_api_tokens_hash ON api_tokens(token_hash); -CREATE INDEX IF NOT EXISTS ix_audit_governance_session ON audit_governance(session_id); -CREATE INDEX IF NOT EXISTS ix_audit_bash_session ON audit_bash(session_id); - --- ── V002 Sessions ───────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL DEFAULT '', - workspace_id TEXT, - project_id TEXT, - model TEXT, - status TEXT NOT NULL DEFAULT 'active', - title TEXT, - mcp_servers TEXT, - started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - ended_at TEXT, - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE TABLE IF NOT EXISTS session_entries ( - entry_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE, - entry_uid TEXT NOT NULL, - timestamp TEXT NOT NULL, - role TEXT NOT NULL, - content TEXT NOT NULL, - model TEXT, - provider TEXT, - input_tokens INTEGER NOT NULL DEFAULT 0, - output_tokens INTEGER NOT NULL DEFAULT 0, - tool_name TEXT, - tool_use_id TEXT, - is_error INTEGER NOT NULL DEFAULT 0, - -- Full-text search via PostgreSQL tsvector (replaces SQLite FTS5) - search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED, - CONSTRAINT uq_session_entries_uid UNIQUE (session_id, entry_uid) -); - -CREATE TABLE IF NOT EXISTS token_usage ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - session_id TEXT NOT NULL, - user_id TEXT NOT NULL DEFAULT '', - workspace_id TEXT, - project_id TEXT, - model TEXT NOT NULL, - input_tokens INTEGER NOT NULL DEFAULT 0, - output_tokens INTEGER NOT NULL DEFAULT 0, - cost_usd DOUBLE PRECISION, - recorded_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS ix_sessions_user ON sessions(user_id); -CREATE INDEX IF NOT EXISTS ix_sessions_status ON sessions(status); -CREATE INDEX IF NOT EXISTS ix_sessions_workspace ON sessions(workspace_id); -CREATE INDEX IF NOT EXISTS ix_sessions_project ON sessions(project_id); -CREATE INDEX IF NOT EXISTS ix_session_entries_session ON session_entries(session_id); -CREATE INDEX IF NOT EXISTS ix_session_entries_fts ON session_entries USING GIN(search_vector); -CREATE INDEX IF NOT EXISTS ix_token_usage_session ON token_usage(session_id); -CREATE INDEX IF NOT EXISTS ix_token_usage_user ON token_usage(user_id); -CREATE INDEX IF NOT EXISTS ix_token_usage_workspace ON token_usage(workspace_id); -CREATE INDEX IF NOT EXISTS ix_token_usage_project ON token_usage(project_id); - --- ── V003 Memory ─────────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS session_summaries ( - session_id TEXT PRIMARY KEY, - project TEXT NOT NULL, - workspace_id TEXT, - started_at TEXT NOT NULL, - ended_at TEXT NOT NULL, - tasks TEXT NOT NULL DEFAULT '[]', - tools_used TEXT NOT NULL DEFAULT '[]', - files_modified TEXT NOT NULL DEFAULT '[]', - outcome TEXT NOT NULL DEFAULT 'Unknown', - total_input_tokens INTEGER NOT NULL DEFAULT 0, - total_output_tokens INTEGER NOT NULL DEFAULT 0, - turn_count INTEGER NOT NULL DEFAULT 0, - error_count INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS learned_patterns ( - id TEXT PRIMARY KEY, - pattern TEXT NOT NULL, - project TEXT NOT NULL, - source_session TEXT, - confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - last_used TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE TABLE IF NOT EXISTS instincts ( - id TEXT PRIMARY KEY, - trigger TEXT NOT NULL, - action TEXT NOT NULL, - confidence DOUBLE PRECISION NOT NULL DEFAULT 0.5, - evidence TEXT NOT NULL DEFAULT '[]', - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS ix_session_summaries_project ON session_summaries(project); -CREATE INDEX IF NOT EXISTS ix_session_summaries_workspace ON session_summaries(workspace_id); -CREATE INDEX IF NOT EXISTS ix_learned_patterns_project ON learned_patterns(project); - --- ── V004 Credentials ────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS credentials ( - key_hash TEXT PRIMARY KEY, - user_id TEXT NOT NULL DEFAULT '', - workspace_id TEXT, - nonce BYTEA NOT NULL, - tag BYTEA NOT NULL, - ciphertext BYTEA NOT NULL, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - --- ── V005 Swarm + Evals ──────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS swarm_events ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - swarm_id TEXT NOT NULL, - event_type TEXT NOT NULL, - agent_id TEXT, - workspace_id TEXT, - project_id TEXT, - payload TEXT NOT NULL DEFAULT '{}', - timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - kind TEXT NOT NULL DEFAULT 'swarm', - run_id TEXT, - user_id TEXT, - parent_swarm_id TEXT -); - -CREATE TABLE IF NOT EXISTS eval_runs ( - run_id TEXT PRIMARY KEY, - suite_name TEXT NOT NULL, - workspace_id TEXT, - started_at TEXT NOT NULL, - duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, - pass_rate DOUBLE PRECISION NOT NULL DEFAULT 0, - pass_at_1_rate DOUBLE PRECISION NOT NULL DEFAULT 0, - total_passed INTEGER NOT NULL DEFAULT 0, - total_failed INTEGER NOT NULL DEFAULT 0, - total_skipped INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS eval_results ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - run_id TEXT NOT NULL REFERENCES eval_runs(run_id) ON DELETE CASCADE, - eval_name TEXT NOT NULL, - category TEXT NOT NULL, - grader_type TEXT NOT NULL, - passed INTEGER NOT NULL DEFAULT 0, - pass_at_1 INTEGER NOT NULL DEFAULT 0, - pass_count INTEGER NOT NULL DEFAULT 0, - attempt_count INTEGER NOT NULL DEFAULT 0, - average_score DOUBLE PRECISION, - duration_seconds DOUBLE PRECISION NOT NULL DEFAULT 0, - skipped INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS ix_swarm_events_swarm ON swarm_events(swarm_id); -CREATE INDEX IF NOT EXISTS ix_swarm_events_workspace ON swarm_events(workspace_id); -CREATE INDEX IF NOT EXISTS ix_swarm_events_project ON swarm_events(project_id); -CREATE INDEX IF NOT EXISTS ix_swarm_events_run_id ON swarm_events(run_id); -CREATE INDEX IF NOT EXISTS ix_swarm_events_kind ON swarm_events(kind); -CREATE INDEX IF NOT EXISTS ix_swarm_events_user ON swarm_events(user_id); -CREATE INDEX IF NOT EXISTS ix_swarm_events_parent ON swarm_events(parent_swarm_id); -CREATE INDEX IF NOT EXISTS ix_eval_runs_suite ON eval_runs(suite_name); -CREATE INDEX IF NOT EXISTS ix_eval_runs_workspace ON eval_runs(workspace_id); -CREATE INDEX IF NOT EXISTS ix_eval_results_run ON eval_results(run_id); - --- ── V006 Workspace Memory ───────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS workspace_memory ( - memory_id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE, - layer TEXT NOT NULL, - content TEXT NOT NULL, - confidence DOUBLE PRECISION, - project_id TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS ix_workspace_memory_workspace ON workspace_memory(workspace_id); -CREATE INDEX IF NOT EXISTS ix_workspace_memory_layer ON workspace_memory(workspace_id, layer); -CREATE INDEX IF NOT EXISTS ix_workspace_memory_project ON workspace_memory(workspace_id, project_id); - --- ── V010 Runtime Traces ─────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS runtime_traces ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - runtime_run_id TEXT NOT NULL, - plan_id TEXT NOT NULL, - plan_version INTEGER NOT NULL, - step_index INTEGER, - entry_type TEXT NOT NULL, - payload TEXT NOT NULL DEFAULT '{}', - timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - session_id TEXT, - workspace_id TEXT, - project_id TEXT -); - -CREATE TABLE IF NOT EXISTS mission_scratchpad ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - mission_id TEXT NOT NULL, - step_index INTEGER NOT NULL, - agent_id TEXT, - namespace TEXT NOT NULL DEFAULT 'default', - key TEXT NOT NULL, - value TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - workspace_id TEXT, - project_id TEXT -); - -CREATE INDEX IF NOT EXISTS ix_runtime_traces_run ON runtime_traces(runtime_run_id, id); -CREATE INDEX IF NOT EXISTS ix_runtime_traces_entry_type ON runtime_traces(entry_type); -CREATE INDEX IF NOT EXISTS ix_runtime_traces_workspace ON runtime_traces(workspace_id); -CREATE INDEX IF NOT EXISTS ix_runtime_traces_project ON runtime_traces(project_id); -CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_mission ON mission_scratchpad(mission_id, step_index); -CREATE INDEX IF NOT EXISTS ix_mission_scratchpad_workspace ON mission_scratchpad(workspace_id); - --- ── V011 Missions ───────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS missions ( - id TEXT PRIMARY KEY, - goal TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'planning', - plan_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - completed_at TEXT, - session_id TEXT, - workspace_id TEXT, - project_id TEXT, - owner_user_id TEXT -); - -CREATE TABLE IF NOT EXISTS mission_events ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - mission_id TEXT NOT NULL, - event_type TEXT NOT NULL, - payload TEXT NOT NULL DEFAULT '{}', - timestamp TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - workspace_id TEXT, - project_id TEXT -); - -CREATE INDEX IF NOT EXISTS ix_missions_status ON missions(status); -CREATE INDEX IF NOT EXISTS ix_missions_workspace ON missions(workspace_id); -CREATE INDEX IF NOT EXISTS ix_missions_project ON missions(project_id); -CREATE INDEX IF NOT EXISTS ix_missions_owner ON missions(owner_user_id); -CREATE INDEX IF NOT EXISTS ix_mission_events_mission ON mission_events(mission_id, id); -CREATE INDEX IF NOT EXISTS ix_mission_events_workspace ON mission_events(workspace_id); - --- ── V012 Unified Orchestration ──────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS teams ( - team_id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL, - project_id TEXT, - name TEXT NOT NULL, - description TEXT, - origin TEXT NOT NULL DEFAULT 'user', - created_by TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - run_mode TEXT NOT NULL DEFAULT 'sequential', - max_concurrent INTEGER NOT NULL DEFAULT 1, - file_locks_enabled INTEGER NOT NULL DEFAULT 0, - quality_gate_enabled INTEGER NOT NULL DEFAULT 0, - quality_gate_threshold INTEGER NOT NULL DEFAULT 7, - decomposition_mode TEXT NOT NULL DEFAULT 'off' -); - -CREATE TABLE IF NOT EXISTS team_members ( - member_id TEXT PRIMARY KEY, - team_id TEXT NOT NULL REFERENCES teams(team_id) ON DELETE CASCADE, - workspace_id TEXT NOT NULL, - project_id TEXT, - name TEXT NOT NULL, - role TEXT NOT NULL DEFAULT 'general', - template TEXT, - system_prompt TEXT, - allowed_tools TEXT, - model_level TEXT, - created_by TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - last_used_at TEXT, - status TEXT NOT NULL DEFAULT 'active' -); - -CREATE TABLE IF NOT EXISTS agent_runs ( - run_id TEXT PRIMARY KEY, - parent_run_id TEXT, - team_id TEXT, - member_id TEXT, - workspace_id TEXT NOT NULL, - project_id TEXT, - user_id TEXT NOT NULL, - kind TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'queued', - started_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - ended_at TEXT, - input_tokens INTEGER NOT NULL DEFAULT 0, - output_tokens INTEGER NOT NULL DEFAULT 0, - cost_usd DOUBLE PRECISION, - prompt TEXT -); - -CREATE INDEX IF NOT EXISTS ix_teams_workspace ON teams(workspace_id); -CREATE INDEX IF NOT EXISTS ix_teams_project ON teams(project_id); -CREATE INDEX IF NOT EXISTS ix_teams_name ON teams(workspace_id, name); -CREATE INDEX IF NOT EXISTS ix_team_members_team ON team_members(team_id); -CREATE INDEX IF NOT EXISTS ix_team_members_workspace ON team_members(workspace_id); -CREATE INDEX IF NOT EXISTS ix_agent_runs_parent ON agent_runs(parent_run_id); -CREATE INDEX IF NOT EXISTS ix_agent_runs_team ON agent_runs(team_id); -CREATE INDEX IF NOT EXISTS ix_agent_runs_workspace ON agent_runs(workspace_id); -CREATE INDEX IF NOT EXISTS ix_agent_runs_user ON agent_runs(user_id); -CREATE INDEX IF NOT EXISTS ix_agent_runs_kind ON agent_runs(kind); -CREATE INDEX IF NOT EXISTS ix_agent_runs_status ON agent_runs(status); - --- ── V013 Coordination Mailbox ───────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS coordination_events ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - source_group_id TEXT NOT NULL, - target_group_id TEXT NOT NULL, - event_type TEXT NOT NULL, - payload TEXT NOT NULL DEFAULT '{}', - status TEXT NOT NULL DEFAULT 'pending', - workspace_id TEXT NOT NULL, - project_id TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - delivered_at TEXT, - acknowledged_at TEXT -); - -CREATE TABLE IF NOT EXISTS group_pm_assignments ( - group_id TEXT PRIMARY KEY, - group_type TEXT NOT NULL, - pm_template TEXT NOT NULL DEFAULT 'pm-coordinator', - workspace_id TEXT NOT NULL, - project_id TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS ix_coordination_events_target ON coordination_events(target_group_id, status); -CREATE INDEX IF NOT EXISTS ix_coordination_events_source ON coordination_events(source_group_id); -CREATE INDEX IF NOT EXISTS ix_coordination_events_ws ON coordination_events(workspace_id); -CREATE INDEX IF NOT EXISTS ix_group_pm_assignments_ws ON group_pm_assignments(workspace_id); - --- ── V017 Hooks ──────────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS hooks ( - hook_id TEXT PRIMARY KEY, - event TEXT NOT NULL, - command TEXT NOT NULL, - matcher_tool TEXT, - matcher_glob TEXT, - timeout_ms INTEGER NOT NULL DEFAULT 30000, - enabled INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS hooks_event_enabled_idx ON hooks(event) WHERE enabled = 1; - --- ── V018 Workspace Settings ─────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS workspace_settings ( - workspace_id TEXT NOT NULL DEFAULT '', - key TEXT NOT NULL, - value TEXT NOT NULL, - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (workspace_id, key) -); - -CREATE INDEX IF NOT EXISTS workspace_settings_key_idx ON workspace_settings(key); - --- ── V019 MCP/LSP Servers ────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS mcp_servers ( - id TEXT NOT NULL DEFAULT '', - name TEXT PRIMARY KEY, - command TEXT NOT NULL DEFAULT '', - args_json TEXT NOT NULL DEFAULT '[]', - env_json TEXT NOT NULL DEFAULT '{}', - oauth_config_json TEXT, - url TEXT, - headers_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; - -CREATE TABLE IF NOT EXISTS lsp_servers ( - language TEXT PRIMARY KEY, - command TEXT NOT NULL DEFAULT '', - args_json TEXT NOT NULL DEFAULT '[]', - env_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - --- ── V020 User Preferences ───────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS user_preferences ( - user_id TEXT NOT NULL, - key TEXT NOT NULL, - value TEXT NOT NULL, - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - PRIMARY KEY (user_id, key) -); - -CREATE INDEX IF NOT EXISTS user_preferences_key_idx ON user_preferences(key); - --- ── V021 Provider Profiles ──────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS provider_profiles ( - profile_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - provider_kind TEXT NOT NULL, - base_url TEXT NOT NULL, - default_model TEXT, - max_tokens INTEGER, - credential_id TEXT NOT NULL, - workspace_id TEXT, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE INDEX IF NOT EXISTS provider_profiles_user_idx ON provider_profiles(user_id); -CREATE INDEX IF NOT EXISTS provider_profiles_workspace_idx ON provider_profiles(workspace_id) - WHERE workspace_id IS NOT NULL; - --- ── V026 Auth Credentials ───────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS server_settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - -CREATE TABLE IF NOT EXISTS password_reset_tokens ( - token_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, - token_hash TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - expires_at TEXT NOT NULL, - used_at TEXT, - CONSTRAINT prt_hash_unique UNIQUE (token_hash) -); - -CREATE INDEX IF NOT EXISTS ix_prt_user ON password_reset_tokens(user_id); -CREATE INDEX IF NOT EXISTS ix_prt_hash ON password_reset_tokens(token_hash); - --- ── V030 Activity is Private ────────────────────────────────────────────────── - -ALTER TABLE missions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; -ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; -ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; - -CREATE INDEX IF NOT EXISTS ix_missions_is_private ON missions(is_private); -CREATE INDEX IF NOT EXISTS ix_agent_runs_is_private ON agent_runs(is_private); -CREATE INDEX IF NOT EXISTS ix_sessions_is_private ON sessions(is_private); - --- ── V031 Session Agent Name ─────────────────────────────────────────────────── - -ALTER TABLE sessions ADD COLUMN IF NOT EXISTS agent_name TEXT; - -CREATE INDEX IF NOT EXISTS idx_sessions_agent_name ON sessions(agent_name) WHERE agent_name IS NOT NULL; - --- ── V032 MCP Trust Rules ────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS mcp_trust_rules ( - rule_id TEXT NOT NULL PRIMARY KEY, - workspace_id TEXT NOT NULL, - server_name TEXT NOT NULL, -- exact server name or '*' (all servers) - tool_pattern TEXT NOT NULL, -- glob: 'delete_*', 'bulk_*', '*' - action TEXT NOT NULL, -- Allow | RequireConfirmation | Block - reason TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_workspace ON mcp_trust_rules(workspace_id); -CREATE INDEX IF NOT EXISTS idx_mcp_trust_rules_server ON mcp_trust_rules(server_name); - --- ── V033/V034/V036 Knowledge Pages ─────────────────────────────────────────── --- Combines V033 foundation, V034 agent columns, and V036 document-template --- columns into one table definition (all columns present on fresh installs). --- V035 and V037 are seed-data-only migrations handled by the app at startup. - -CREATE TABLE IF NOT EXISTS knowledge_pages ( - knowledge_id TEXT NOT NULL PRIMARY KEY, - kind TEXT NOT NULL, -- 'skills' | 'agents' | 'documents' | 'tools' | 'document-templates' - slug TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '', - tier TEXT NOT NULL DEFAULT 'User', - -- skills-specific - trigger TEXT, - agents TEXT, -- JSON array - tools TEXT, -- JSON array - -- documents-specific - industry TEXT, - default_format TEXT, - -- tool-templates-specific - category TEXT, - -- agent-specific (V034) - role TEXT, - recommended_level TEXT, - -- document-template-specific (V036) - fields_json TEXT, - filename_template TEXT, - -- content - body TEXT NOT NULL DEFAULT '', - -- ownership / scope - workspace_id TEXT NOT NULL DEFAULT '', - -- audit - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - updated_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - UNIQUE (kind, slug, workspace_id) -); - -CREATE INDEX IF NOT EXISTS idx_knowledge_pages_kind ON knowledge_pages(kind); -CREATE INDEX IF NOT EXISTS idx_knowledge_pages_workspace ON knowledge_pages(workspace_id); - --- Additive guards for databases that were initialised before this combined --- definition — safe no-ops on fresh installs. -ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS role TEXT; -ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS recommended_level TEXT; -ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS fields_json TEXT; -ALTER TABLE knowledge_pages ADD COLUMN IF NOT EXISTS filename_template TEXT; - --- ── V038 Knowledge Attributions ─────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS knowledge_attributions ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - session_id TEXT NOT NULL, - turn_index INTEGER NOT NULL, - kind TEXT NOT NULL, -- 'skills', 'agents', 'document-templates', 'tools' - slug TEXT NOT NULL, - used_at TEXT NOT NULL -- ISO 8601 -); - -CREATE INDEX IF NOT EXISTS idx_knowledge_attributions_session ON knowledge_attributions(session_id); - --- ── V039 Keystore ───────────────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS keystore ( - scope TEXT NOT NULL PRIMARY KEY, -- 'default' (reserved for future per-workspace keys) - key_hex TEXT NOT NULL, -- 64-char lowercase hex (256-bit AES master key) - created_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')) -); - --- ── V040 MCP Server stable ID ──────────────────────────────────────────────── --- Fresh installs already have id + index via the V019 table definition above. --- These statements are no-ops on fresh installs; they fix upgrade installs that --- ran before V040 and have id = '' on all existing mcp_servers rows. - -ALTER TABLE mcp_servers ADD COLUMN IF NOT EXISTS id TEXT NOT NULL DEFAULT ''; -CREATE UNIQUE INDEX IF NOT EXISTS idx_mcp_servers_id ON mcp_servers(id) WHERE id <> ''; -UPDATE mcp_servers SET id = gen_random_uuid()::text WHERE id = ''; - --- ── V041 Workspace Memory Privacy ──────────────────────────────────────────── --- owner_user_id = '' means unowned/legacy: visible to ALL authenticated users --- via the load filter (owner_user_id = '' OR owner_user_id = $uid). --- Non-empty means scoped to that specific user only. - -ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; -ALTER TABLE workspace_memory ADD COLUMN IF NOT EXISTS is_private INTEGER NOT NULL DEFAULT 0; - -CREATE INDEX IF NOT EXISTS ix_workspace_memory_owner ON workspace_memory(owner_user_id); - --- ── V042 Memory Owner User ID ───────────────────────────────────────────────── --- Scopes auto-generated memories (session summaries, patterns, instincts) to --- the session owner so they are not mixed across users in a multi-user deployment. --- Same owner_user_id = '' convention as V041: empty = legacy/global, non-empty = user-scoped. - -ALTER TABLE session_summaries ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; -ALTER TABLE learned_patterns ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; -ALTER TABLE instincts ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''; - -CREATE INDEX IF NOT EXISTS ix_session_summaries_owner ON session_summaries(owner_user_id); -CREATE INDEX IF NOT EXISTS ix_learned_patterns_owner ON learned_patterns(owner_user_id); -CREATE INDEX IF NOT EXISTS ix_instincts_owner ON instincts(owner_user_id); - --- ── V043 Drop username column ───────────────────────────────────────────────── --- For instances initialized before V043. Fresh installs never had this column. --- V043's PK rewrite (usr_{hex} → email) applies to SQLite standalone only. - -ALTER TABLE public.users DROP COLUMN IF EXISTS username; - --- ── Schema version tracking ─────────────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS sovrant_schema_version ( - id INTEGER PRIMARY KEY DEFAULT 1, - version INTEGER NOT NULL, - applied_at TEXT NOT NULL DEFAULT (to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')), - CONSTRAINT single_row CHECK (id = 1) -); - -INSERT INTO sovrant_schema_version (id, version) -VALUES (1, 43) -ON CONFLICT (id) DO UPDATE SET version = EXCLUDED.version, applied_at = EXCLUDED.applied_at; From cca9b52a5196faa4a87e36e687ae6c0101fe49e9 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Thu, 25 Jun 2026 09:26:08 -0400 Subject: [PATCH 06/24] Add Phase 127: Supabase Row Level Security to roadmap Activates the commented-out RLS policy skeletons via a second Supabase migration (20260625000001_enable_rls.sql). Service-role key retains full unrestricted access; JWT-authenticated direct-DB callers are scoped to their own data at the DB layer, closing the dashboard/Edge Function gap. Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index ecf523ca..b06bbebb 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Sovrant — Roadmap **Branch:** `development` -**Last updated:** 2026-06-25 (Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) +**Last updated:** 2026-06-25 (Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) This document tracks planned features, architectural decisions, and the reasoning behind them. @@ -213,6 +213,7 @@ The engine is fully functional across five delivery modes with enterprise multi- | File system access controls — admin-configurable directory allowlist and blocklist so Sovrant agents can only operate within declared paths; enforced at the tool level before Read/Write/Edit/Glob/Grep/Bash execute; denied accesses logged as `directory_access_denied` governance audit events; Governance page gains a "File System Access" section (Web + Desktop); V044 migration stores rules in `server_settings`; path traversal and symlink escapes are normalised before evaluation | Phase 124 | Planned | | Chat conversation UX — collapsed work strips replace per-tool boxes; two-level expand (strip → tool list → full detail); live "doing X" in-progress indicator while agent works; clean visual hierarchy where the agent's answer is prominent and tool work is subordinate; consistent Web + Desktop parity | Phase 126 | Planned | | Web search via integrations — move web search out of the hard-coded `WebSearchBackend` enum and into the Integration Gallery; add `IntegrationKind.HttpApi` for direct REST adapters (no MCP process); define `IWebSearchProvider` interface; ship DuckDuckGo (built-in free default), Brave, FireCrawl, Exa, Tavily as `HttpApi` catalog entries; add Crawl4AI as a scraper/fetcher integration; admin picks active search provider from Integrations page; remove `WebSearchBackend` enum; existing MCP search entries remain as alternatives; unit test coverage for WebFetchTool, search providers, and dispatch | Phase 125 | Planned | +| Supabase Row Level Security — enable RLS on all privacy-sensitive tables in the Supabase migration and write policies for the `owner_user_id` model; service-role key retains full unrestricted access (Supabase bypasses RLS for service role by design); anon/authenticated JWT callers are scoped to their own data at the database layer; complements the existing application-layer query filters | Phase 127 | Planned | ### v1.0 release polish ✅ @@ -11516,3 +11517,60 @@ Both surfaces share the same visual language (strip line format, icon set, statu - The answer text is readable without scrolling past tool blocks on a typical 1080p screen - Web and Desktop render the strip with the same visual language - No regression in tool confirmation flow (Allow once / Allow for turn / Deny buttons still work within the strip) + +--- + +## Phase 127 — Supabase Row Level Security + +**Status:** Planned + +### Why + +Sovrant enforces the `owner_user_id` privacy model at the application layer — query filters in `SqliteWorkspaceStore`, `SqliteMemoryStore`, and similar stores ensure users only see their own private data. In a Supabase deployment this is sufficient for all traffic that goes through the Sovrant API, but it leaves a gap: + +- **Supabase dashboard queries** (developers, DBAs) run directly against the database and bypass the application layer entirely +- **Edge Functions** calling `supabase.from(...)` use the user's JWT and get no automatic scoping unless RLS is enabled +- **Service-role callers** (the Sovrant server itself, admin scripts) need unrestricted access and must not be blocked + +RLS closes the first two gaps without touching the third — Supabase's service role bypasses RLS by design. This means the Sovrant API server, admin tooling, and migration scripts all continue to work with zero changes; only JWT-authenticated direct-DB callers gain a scoping boundary. + +The commented-out policy skeletons already exist in `db/supabase/migrations/20260625000000_initial_schema.sql`. This phase activates them properly with a second migration file admins can apply when they are ready. + +### What ships + +**A new Supabase migration** (`db/supabase/migrations/20260625000001_enable_rls.sql`) that: + +1. Enables RLS on the four privacy-sensitive tables: + - `workspace_memory` + - `session_summaries` + - `learned_patterns` + - `instincts` + +2. Creates read policies scoped to the calling JWT's `auth.uid()`: + +| Table | Policy | Rule | +|---|---|---| +| `workspace_memory` | `workspace_memory_read` | `is_private = 0 OR owner_user_id = auth.uid()::text` | +| `session_summaries` | `session_summaries_read` | `owner_user_id = '' OR owner_user_id = auth.uid()::text` | +| `learned_patterns` | `learned_patterns_read` | `owner_user_id = '' OR owner_user_id = auth.uid()::text` | +| `instincts` | `instincts_read` | `owner_user_id = '' OR owner_user_id = auth.uid()::text` | + +3. Creates permissive write policies that allow authenticated callers to insert/update/delete their own rows and rows with `owner_user_id = ''` (shared/legacy). + +**Service-role key is unaffected** — Supabase's service role always bypasses RLS. The Sovrant API server uses the service role key for all database operations, so no application code changes are needed. + +**Docs** — `docs/persistence.md` updated to note that the RLS migration is available as an optional `supabase db push` step; the commented-out stubs in the initial migration file are removed now that a proper migration exists. + +### What stays out of scope + +- RLS on session or audit tables — those are queried by the application with scoped filters already; RLS on high-write tables adds overhead without meaningful security gain for service-role deployments +- Per-workspace RLS policies — the current `owner_user_id` model is user-scoped; workspace-level isolation is a separate governance layer +- Standalone PostgreSQL RLS — standalone deployments use application-layer auth exclusively; service-role bypass does not apply in the same way + +### Acceptance criteria + +- After applying the migration, a direct Supabase client authenticated as user A cannot read user B's private `workspace_memory` rows via the dashboard or an Edge Function +- User A can read all `workspace_memory` rows where `is_private = 0` regardless of `owner_user_id` +- The Sovrant API server (service-role key) continues to read and write all rows without restriction +- `supabase db push` applies the migration cleanly with no errors on a fresh project and on an existing project that already has the initial schema +- All existing Sovrant API tests pass unchanged — no application code is modified by this phase From 76377cfbf42c76fd92fc62de79debecc30e26179 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Fri, 26 Jun 2026 11:12:55 -0400 Subject: [PATCH 07/24] Make API key optional for Ollama and LM Studio on provider add form Both providers run locally with no authentication. Placing a dummy key was required before; now the validation guard is skipped for local providers and an empty string is stored in the credential (the runtime OllamaProvider already uses string.Empty for auth). Web: label shows (optional) and placeholder changes when Ollama/LM Studio is selected. Desktop: ApiKeyLabel/ApiKeyWatermark computed properties bound in SettingsView.axaml; SelectedProvider change notifies both via NotifyPropertyChangedFor. Co-Authored-By: Claude Sonnet 4.6 --- src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs | 13 ++++++++++++- src/Sovrant.Desktop/Views/SettingsView.axaml | 4 ++-- .../Components/Pages/AdminProviders.razor | 13 ++++++++++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs index de93ad83..9615a32a 100644 --- a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs +++ b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs @@ -76,6 +76,8 @@ public partial class SettingsViewModel : ViewModelBase private bool _isDarkMode = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ApiKeyLabel))] + [NotifyPropertyChangedFor(nameof(ApiKeyWatermark))] private string _selectedProvider = "OpenAI"; [ObservableProperty] @@ -427,10 +429,19 @@ partial void OnPermissionModeChanged(PermissionMode value) // ─── Provider Profiles ───────────────────────────── + private static bool IsLocalProvider(string provider) => + provider is "Ollama" or "LM Studio"; + + public string ApiKeyLabel => + IsLocalProvider(SelectedProvider) ? "API Key (optional)" : "API Key"; + + public string ApiKeyWatermark => + IsLocalProvider(SelectedProvider) ? "Not required for local providers" : "sk-..."; + [RelayCommand] private async Task AddProviderAsync() { - if (string.IsNullOrWhiteSpace(ApiKey)) + if (string.IsNullOrWhiteSpace(ApiKey) && !IsLocalProvider(SelectedProvider)) { StatusMessage = "Please enter an API key."; return; diff --git a/src/Sovrant.Desktop/Views/SettingsView.axaml b/src/Sovrant.Desktop/Views/SettingsView.axaml index 10a51427..100a8d68 100644 --- a/src/Sovrant.Desktop/Views/SettingsView.axaml +++ b/src/Sovrant.Desktop/Views/SettingsView.axaml @@ -107,11 +107,11 @@ - diff --git a/src/Sovrant.Web/Components/Pages/AdminProviders.razor b/src/Sovrant.Web/Components/Pages/AdminProviders.razor index 6e9eb864..9708a767 100644 --- a/src/Sovrant.Web/Components/Pages/AdminProviders.razor +++ b/src/Sovrant.Web/Components/Pages/AdminProviders.razor @@ -60,8 +60,8 @@
- - + +
@@ -169,9 +169,16 @@ _baseUrl = profile.BaseUrl; } + private static bool IsLocalProvider(string provider) => + provider is "Ollama" or "LM Studio"; + private async Task AddProvider() { - if (string.IsNullOrWhiteSpace(_apiKey)) { _statusMessage = "Please enter an API key."; return; } + if (string.IsNullOrWhiteSpace(_apiKey) && !IsLocalProvider(_selectedProvider)) + { + _statusMessage = "Please enter an API key."; + return; + } var displayName = string.IsNullOrWhiteSpace(_newProfileName) ? _selectedProvider : _newProfileName.Trim(); From aa4c0da2476ce5c5cb4f0cf1f17f6e7bdc48e551 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Fri, 26 Jun 2026 11:20:20 -0400 Subject: [PATCH 08/24] Fix model list not loading when switching providers on Settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OnSelectedProviderChanged clears ApiKey before LoadModelsForProviderAsync fires, so OpenRouter and other key-gated providers always fetched with an empty key and returned nothing. Fix: resolve an effectiveKey — form field when populated, otherwise the saved credential from the matching SavedProfiles entry. Ollama and LM Studio are unaffected (they use local HTTP with no auth). Co-Authored-By: Claude Sonnet 4.6 --- .../ViewModels/SettingsViewModel.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs index 9615a32a..b6616cbc 100644 --- a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs +++ b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs @@ -300,9 +300,18 @@ private async Task LoadModelsForProviderAsync(string provider) { List models; + // The form ApiKey is cleared on provider switch before this fires. + // Fall back to the saved credential for any existing profile that + // matches the selected provider so the model list still loads. + var effectiveKey = !string.IsNullOrWhiteSpace(ApiKey) + ? ApiKey + : SavedProfiles.FirstOrDefault(p => + p.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase))?.ApiKey + ?? string.Empty; + if (provider == "OpenRouter") { - models = await FetchAuthenticatedModelIdsAsync("https://openrouter.ai/api/v1", ApiKey); + models = await FetchAuthenticatedModelIdsAsync("https://openrouter.ai/api/v1", effectiveKey); } else if (provider == "Ollama") { @@ -316,8 +325,8 @@ private async Task LoadModelsForProviderAsync(string provider) { // Try fetching from the provider's /models endpoint (OpenAI, DeepSeek, Groq, etc.) var baseUrl = ProviderBaseUrls.GetValueOrDefault(provider, string.Empty); - models = !string.IsNullOrEmpty(baseUrl) && !string.IsNullOrWhiteSpace(ApiKey) - ? await FetchAuthenticatedModelIdsAsync(baseUrl, ApiKey) + models = !string.IsNullOrEmpty(baseUrl) && !string.IsNullOrWhiteSpace(effectiveKey) + ? await FetchAuthenticatedModelIdsAsync(baseUrl, effectiveKey) : []; // Fall back to static list if API fetch returned nothing. From 159c8a26e7b64ea7963dc56f0a259d940b12c495 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Fri, 26 Jun 2026 11:27:59 -0400 Subject: [PATCH 09/24] Fix model list not loading for Ollama on web and desktop FetchModelIdsAsync and FetchAuthenticatedModelIdsAsync unconditionally set Authorization: Bearer {key}, producing a malformed header when the key is empty. Ollama rejects this and returns no models. Only set the Authorization header when the sanitized key is non-empty. Co-Authored-By: Claude Sonnet 4.6 --- src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs | 4 +++- src/Sovrant.Web/Components/Layout/TopContextBar.razor | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs index b6616cbc..9e6c04c0 100644 --- a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs +++ b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs @@ -364,7 +364,9 @@ private async Task> FetchAuthenticatedModelIdsAsync(string baseUrl, var modelsUrl = baseUrl.TrimEnd('/') + "/models"; using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(modelsUrl)); - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", new string(apiKey.Where(c => c < 128).ToArray()).Trim()); + var safeKey = new string(apiKey.Where(c => c < 128).ToArray()).Trim(); + if (!string.IsNullOrEmpty(safeKey)) + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", safeKey); var response = await http.SendAsync(request, cts.Token); response.EnsureSuccessStatusCode(); diff --git a/src/Sovrant.Web/Components/Layout/TopContextBar.razor b/src/Sovrant.Web/Components/Layout/TopContextBar.razor index 9386a95c..203ff486 100644 --- a/src/Sovrant.Web/Components/Layout/TopContextBar.razor +++ b/src/Sovrant.Web/Components/Layout/TopContextBar.razor @@ -608,7 +608,8 @@ var modelsUrl = baseUrl.TrimEnd('/') + "/models"; using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(modelsUrl)); var safeKey = new string(apiKey.Where(c => c < 128).ToArray()).Trim(); - request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", safeKey); + if (!string.IsNullOrEmpty(safeKey)) + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", safeKey); var response = await http.SendAsync(request, cts.Token); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(cts.Token); From 899ebe51f3e4d29b7cc265ba425bed7d90f930c2 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Fri, 26 Jun 2026 11:30:38 -0400 Subject: [PATCH 10/24] Pre-select personal workspace when adding a provider (web and desktop) Personal workspace is now checked by default; all other workspaces are opt-in. Matches user expectation that adding a provider makes it available to yourself immediately without any extra clicks. Co-Authored-By: Claude Sonnet 4.6 --- src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs | 6 +++++- src/Sovrant.Web/Components/Pages/AdminProviders.razor | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs index 9e6c04c0..8b70bec1 100644 --- a/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs +++ b/src/Sovrant.Desktop/ViewModels/SettingsViewModel.cs @@ -197,7 +197,11 @@ await Dispatcher.UIThread.InvokeAsync(() => { WorkspaceItems.Clear(); foreach (var ws in workspaces) - WorkspaceItems.Add(new WorkspaceSelectItem { WorkspaceId = ws.WorkspaceId, Name = ws.Name }); + { + // Personal workspace is pre-selected; all others are opt-in. + var isPersonal = ws.Type == Runtime.Workspaces.WorkspaceType.Personal; + WorkspaceItems.Add(new WorkspaceSelectItem { WorkspaceId = ws.WorkspaceId, Name = ws.Name, IsSelected = isPersonal }); + } }); } catch { /* workspaces unavailable — omit the picker */ } diff --git a/src/Sovrant.Web/Components/Pages/AdminProviders.razor b/src/Sovrant.Web/Components/Pages/AdminProviders.razor index 9708a767..c956fa87 100644 --- a/src/Sovrant.Web/Components/Pages/AdminProviders.razor +++ b/src/Sovrant.Web/Components/Pages/AdminProviders.razor @@ -154,6 +154,11 @@ await LoadProfilesAsync(); _allWorkspaces = (await WorkspaceService.ListAllAsync()).ToList(); + + // Pre-select the personal workspace; all others are opt-in. + var personal = _allWorkspaces.FirstOrDefault(w => w.Type == Sovrant.Runtime.Workspaces.WorkspaceType.Personal); + if (personal is not null) + _selectedWorkspaceIds.Add(personal.WorkspaceId); } private void OnProviderChanged() From d787f4570c3863445cf1e6207dbee5e6635d8eee Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 10:31:38 -0400 Subject: [PATCH 11/24] =?UTF-8?q?docs(roadmap):=20audit=20pass=20=E2=80=94?= =?UTF-8?q?=20mark=20Phase=20123=20done,=20update=20migration=20count=20to?= =?UTF-8?q?=20V043?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header: bump Last updated to 2026-06-29; add Phase 123, V040-V043, schema split (db/postgres + db/supabase/migrations) notes - Current State: 39 versioned migrations → 43 (V001–V043); add V040–V043 descriptions (stable MCP IDs, workspace memory privacy, memory owner scoping, email-as-user-id) - Current Focus table: add v1.3 wave rows for Phase 105 (MCP workspace gating) and Phase 123 (Memory System), both ✅ - Still pending Last audited: 2026-05-26 → 2026-06-29; list newly shipped phases (96, 105-partial, 123, V043, 40C schema split) - Phase 91 Knowledge Authoring: Deferred → Partial ✅ (Guidelines/Documents done; Skills Duplicate button + AvaloniaEdit Desktop fixes remain) Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index b06bbebb..4aa3955c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Sovrant — Roadmap **Branch:** `development` -**Last updated:** 2026-06-25 (Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) +**Last updated:** 2026-06-29 (Phase 123 ✅ — Memory System: workspace memory with public/private scoping, "+Remember" button in Chat (Web + Desktop), V041/V042 migrations, per-user injection in multi-user deployments. V040 MCP stable IDs (Phase 105 workspace-level gating). V043 email-as-user-id (replaces `usr_{hex}` PKs). PostgresSchema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/` (Phase 40C documentation update). Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) This document tracks planned features, architectural decisions, and the reasoning behind them. @@ -25,6 +25,8 @@ What we are actively working on and shipping next, in priority order. | **v1.2 — done** | Phase 98 | User Dashboard — cross-workspace activity view; own public (Shared) + own private + teammates' public; V030 `is_private`; Web + Desktop rail nav; pagination, timestamps, 30s poll, guide panels, page-preserve on refresh ✅ | | **v1.2 — done** | Phase 99 | Private sessions and agent runs — per-record privacy toggle on sessions, agent runs, and missions; masked in Command Center, excluded from User Dashboard; server-side enforcement via V030 `is_private` ✅ | | **v1.2 — done** | Phase 96 | MCP runtime variables — inline env var editor on Web + Desktop; KEY=VALUE textarea in stdio add form; JSON paste auto-populates env vars; master key moved into DB (V039 `keystore` table); AES-256-GCM encrypted at rest ✅ | +| **v1.3 — done** | Phase 105 | MCP server permissions — workspace-level gating via V040 stable MCP IDs; `GetEnabledEntriesAsync` workspace filter; admin toggle UI on Web + Desktop; memory-gate guards at selection point ✅ (project-level deferred) | +| **v1.3 — done** | Phase 123 | Memory System — workspace memory with public/private scoping; "+Remember" button in Chat (Web + Desktop); Workspace tab on Memory page; V041/V042 migrations; per-user injection in multi-user deployments ✅ | > Items below v1.0 are planned but not yet scheduled. See [Still pending](#still-pending) for the full gap list. @@ -39,7 +41,7 @@ The engine is fully functional across five delivery modes with enterprise multi- - **141 server endpoints** + 1 SignalR hub (chat, sessions, config, status, models, usage, cost, command-center, webhooks, workspaces, projects, users, teams, runs, missions, engine, artifacts, evals, swarm, tools, skills, agents, MCP auth, knowledge, trust-rules, attributions) - **5 delivery modes:** CLI REPL, HTTP server (:5200), desktop app (Avalonia), web app (Blazor :5100), MCP server (stdio) - Agentic loop with up to 20 tool rounds per turn -- SQLite persistence layer with 39 versioned migrations (V001–V039) — V038 `mcp_servers`→credential store migration (Phase 95), V039 `keystore` table — master AES key in DB (Phase 96), V030 `is_private` (Phase 98), V031 `agent_name` (Phase 106), V032–V033 `knowledge_pages` + `knowledge_attributions` (Phase 108/116F), V034–V037 `role`/`recommended_level` + `fields_json`/`filename_template` + skill/agent/doc-template seeds (Phase 112A–D) on top of the Phase 32/42.5/51/52/57/78 foundation +- SQLite persistence layer with 43 versioned migrations (V001–V043) — V040 `mcp_servers.id` stable surrogate for workspace-scoped gating (Phase 105), V041 `workspace_memory` owner/privacy columns (Phase 123), V042 `session_summaries`/`learned_patterns`/`instincts` owner scoping (Phase 123), V043 email-as-user-id rewrite (replaces `usr_{hex}` PKs), V038 `knowledge_attributions` table (Phase 116F), V039 `keystore` table — master AES key in DB (Phase 96), V030 `is_private` (Phase 98), V031 `agent_name` (Phase 106), V032–V033 `knowledge_pages` + `knowledge_attributions` schema (Phase 108/116F), V034–V037 `role`/`recommended_level` + `fields_json`/`filename_template` + skill/agent/doc-template seeds (Phase 112A–D) on top of the Phase 32/42.5/51/52/57/78 foundation - Single `.env` file configuration — `sovrant.config` removed; all bootstrap knobs are env vars; routing and swarm config fully DB-backed - **Integrations Gallery** on Web and Desktop — catalog-first MCP onramp with Automation (Composio, n8n, Zapier, Make), Platform (GitHub, Slack, Notion, Linear, Stripe, PostgreSQL, Supabase, Filesystem), and Search (Brave, Exa, Tavily) tiers; credentials stored in encrypted keystore (Phase 95 ✅) - **Model switcher with provider discovery** — configured providers selectable inline; unconfigured known providers shown with click-to-configure deep-link to Settings → Providers on both Web and Desktop @@ -150,7 +152,7 @@ The engine is fully functional across five delivery modes with enterprise multi- ### Still pending -> **Last audited:** 2026-05-26. Shipped since prior audit: Phase 79 Agents page ✅, Phase 94 Orchestration Studio ✅, Phase 95 Integrations Gallery ✅ (encrypted credentials, catalog with 14 integrations across 3 tiers, Web + Desktop parity). Phase 50 OpenClaw federation ✅ (SwarmFederationMode, bus client, manager-led routing, V029, 3 new API endpoints). Phase 73 code scaffolding ✅ (21 templates, CodeCreateMultiTool, ScaffoldManifestValidator, 235 tests). Session-level MCP opt-in lifted to context bar (Desktop WorkspacePanelView + Web TopContextBar). Phase 98 User Dashboard ✅ (Shared stat = own public items, pagination + timestamps + 30s poll + page-preserve on both surfaces, guide panels). Phase 99 privacy toggles ✅ (per-record is_private on sessions/agent_runs/missions, Command Center masking, User Dashboard exclusion). Phase 85.5 local/remote mode selection ✅ (Sovrant.Api.Client, sovrant connect/disconnect, two-phase Desktop boot, setup wizard mode picker). Phase 91 Knowledge Authoring deferred. +> **Last audited:** 2026-06-29. Shipped since prior audit: Phase 96 MCP runtime variables ✅ (inline env var editor Web + Desktop, V039 keystore). Phase 105 MCP server permissions ✅ (partial — workspace-level gating via V040 stable MCP IDs, admin toggle UI on Web + Desktop; project-level restrictions remain deferred). Phase 123 Memory System ✅ (workspace memory with public/private scoping, "+Remember" button in Chat on Web + Desktop, dedicated Workspace tab on Memory page, V041/V042 migrations, per-user injection in multi-user deployments). V043 email-as-user-id migration ✅ (replaces opaque `usr_{hex}` PKs across all FK columns). Phase 40C Supabase backend ✅ (PostgreSQL stores, admin UI, SQLite→Postgres migrator, boot-time DI switch; schema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/`). Phase 91 Knowledge Authoring partial ✅ (Guidelines + Documents pages complete with single Edit + silent copy-on-write; Skills page Duplicate button removal + AvaloniaEdit Desktop fixes remain). > > Quality / polish / audit phases (62, 68, 69, 70, 71, 72, 75) and partial-completion phases (56) are tracked in their own sections below; this table is gap-only. @@ -182,7 +184,7 @@ The engine is fully functional across five delivery modes with enterprise multi- | Local / remote mode selection — CLI + Desktop can run embedded (local DB) or connect to a shared `Sovrant.Server`; setup wizard mode picker; `sovrant connect ` | Phase 85.5 | ✅ Done | | Background session continuation across navigation & session switches | Phase 86 | ✅ Done | | Artifacts-by-default for code & documents (with workspace identity unification) | Phase 87 | ✅ Done | -| Knowledge Authoring Revisit — Web + Desktop UX rework: single Edit action on any item, silent copy-on-write for built-ins, no "Duplicate to user" intermediate; fix AvaloniaEdit defects on Desktop | Phase 91 | Deferred | +| Knowledge Authoring Revisit — Guidelines and Documents pages complete (single Edit, silent copy-on-write, no "Duplicate" intermediate); Skills page Duplicate button removal + AvaloniaEdit Desktop fixes remain | Phase 91 | Partial ✅ — Skills + Desktop deferred | | Active Sessions: up to 5 concurrent live tasks with return-anytime results; Settings UI on Web + Desktop, DB-backed; future admin console fallback | Phase 92 | ✅ Done | | Agents page — in-app create/edit, Launch chat, Run one-shot, prompt titles in ledger | Phase 79 | ✅ Done | | Orchestration Studio — compose and run teams from the UI; Run button with task prompt | Phase 94 | ✅ Done | From 1b96ec872755fd9bd7c384392a31e5f9bc57dac2 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 11:24:19 -0400 Subject: [PATCH 12/24] docs(roadmap): add v1.3/v1.4 focus table rows; fix test count to 2,208 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Current Focus: add v1.3 row for Phase 91 partial (admin Knowledge editing, Monaco editor, Avalonia 11→12 migration) - Current Focus: add v1.4 rows for V043 email-as-user-id breaking change and Ollama routing + project FK bug fixes - Current State: 2,222 tests → 2,208 (reflects current suite across 10 projects) Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 4aa3955c..e8b73cda 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -27,6 +27,9 @@ What we are actively working on and shipping next, in priority order. | **v1.2 — done** | Phase 96 | MCP runtime variables — inline env var editor on Web + Desktop; KEY=VALUE textarea in stdio add form; JSON paste auto-populates env vars; master key moved into DB (V039 `keystore` table); AES-256-GCM encrypted at rest ✅ | | **v1.3 — done** | Phase 105 | MCP server permissions — workspace-level gating via V040 stable MCP IDs; `GetEnabledEntriesAsync` workspace filter; admin toggle UI on Web + Desktop; memory-gate guards at selection point ✅ (project-level deferred) | | **v1.3 — done** | Phase 123 | Memory System — workspace memory with public/private scoping; "+Remember" button in Chat (Web + Desktop); Workspace tab on Memory page; V041/V042 migrations; per-user injection in multi-user deployments ✅ | +| **v1.3 — done** | Phase 91 *(partial)* | Knowledge Authoring — admin edit/revert on Skills and Document Templates pages; Monaco editor for prompt + JSON editing; Knowledge sub-nav sorted alphabetically (Web + Desktop); Avalonia 11 → 12 desktop migration ✅ (AvaloniaEdit Desktop fixes deferred) | +| **v1.4 — done** | V043 | Email-as-user-id — V043 migration replaces all `usr_{hex}` PKs with email across every FK and soft-reference column; `username` column dropped; `IUserService` API updated; personal workspace IDs updated in tandem ✅ | +| **v1.4 — done** | Bug fixes | Ollama provider label/routing fixed (FriendlyProviderName checks provider name before URL); FK error 19 on project creation fixed (PersonalWorkspaceId reads `App.SovrantUserId` at call time) ✅ | > Items below v1.0 are planned but not yet scheduled. See [Still pending](#still-pending) for the full gap list. @@ -37,7 +40,7 @@ What we are actively working on and shipping next, in priority order. The engine is fully functional across five delivery modes with enterprise multi-tenant infrastructure: - **58 tools** across 18 categories (core file, extended, todo, tasks, plan mode, worktree, skills, MCP, agent, team, missions, artifacts, documents, quality, swarm, coordination, LSP, code scaffolding) -- **2,222 tests** across 10 projects, 0 failures +- **2,208 tests** across 10 projects, 0 failures - **141 server endpoints** + 1 SignalR hub (chat, sessions, config, status, models, usage, cost, command-center, webhooks, workspaces, projects, users, teams, runs, missions, engine, artifacts, evals, swarm, tools, skills, agents, MCP auth, knowledge, trust-rules, attributions) - **5 delivery modes:** CLI REPL, HTTP server (:5200), desktop app (Avalonia), web app (Blazor :5100), MCP server (stdio) - Agentic loop with up to 20 tool rounds per turn From 549f557d4a8d2a2be408e369ac448cb64dbea761 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 12:28:47 -0400 Subject: [PATCH 13/24] docs(roadmap): add Phase 128; mark Phase 114/74/128 as v1.5 focus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Current Focus table: add v1.5 wave rows for Phase 114 (skill enrichment), Phase 74 (markdown document templates), Phase 128 (code generation quality gates) — ordered by effort/impact - Last Updated header: note Phase 128 planned and v1.5 focus - Still pending table: add Phase 128 row - Phase 128 full section: ICodeValidator per-language, self-correction loop (max 2 rounds), guideline conformance check, V044 production scaffold enrichment (CI, .gitignore, Dockerfile, security scan, README, editorconfig) for all 21 templates; designed to work with any code-capable LLM Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 102 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index e8b73cda..f8046cae 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Sovrant — Roadmap **Branch:** `development` -**Last updated:** 2026-06-29 (Phase 123 ✅ — Memory System: workspace memory with public/private scoping, "+Remember" button in Chat (Web + Desktop), V041/V042 migrations, per-user injection in multi-user deployments. V040 MCP stable IDs (Phase 105 workspace-level gating). V043 email-as-user-id (replaces `usr_{hex}` PKs). PostgresSchema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/` (Phase 40C documentation update). Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) +**Last updated:** 2026-06-29 (Phase 128 planned — code generation quality gates (post-generation lint/self-correction loop, production-grade scaffold enrichment). v1.5 focus: Phase 114 skill enrichment + Phase 74 markdown document templates + Phase 128. Phase 123 ✅ — Memory System: workspace memory with public/private scoping, "+Remember" button in Chat (Web + Desktop), V041/V042 migrations, per-user injection in multi-user deployments. V040 MCP stable IDs (Phase 105 workspace-level gating). V043 email-as-user-id (replaces `usr_{hex}` PKs). PostgresSchema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/` (Phase 40C documentation update). Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) This document tracks planned features, architectural decisions, and the reasoning behind them. @@ -30,6 +30,9 @@ What we are actively working on and shipping next, in priority order. | **v1.3 — done** | Phase 91 *(partial)* | Knowledge Authoring — admin edit/revert on Skills and Document Templates pages; Monaco editor for prompt + JSON editing; Knowledge sub-nav sorted alphabetically (Web + Desktop); Avalonia 11 → 12 desktop migration ✅ (AvaloniaEdit Desktop fixes deferred) | | **v1.4 — done** | V043 | Email-as-user-id — V043 migration replaces all `usr_{hex}` PKs with email across every FK and soft-reference column; `username` column dropped; `IUserService` API updated; personal workspace IDs updated in tandem ✅ | | **v1.4 — done** | Bug fixes | Ollama provider label/routing fixed (FriendlyProviderName checks provider name before URL); FK error 19 on project creation fixed (PersonalWorkspaceId reads `App.SovrantUserId` at call time) ✅ | +| **v1.5 — next** | Phase 114 | Enrich built-in skill definitions — richer descriptions (2–3 sentences), ≥5-step workflow bodies, agents/tools lists; SQL migration only, no code changes; immediate quality lift for every LLM via the IKnowledgeRouter harness | +| **v1.5 — next** | Phase 74 | Markdown-backed document templates — 44 hardcoded C# templates become editable `.md` files (Scriban expressions, YAML frontmatter field schema, code-behind for computed-logic templates); domain experts iterate without rebuilds | +| **v1.5 — next** | Phase 128 | Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); works with any code-capable LLM | > Items below v1.0 are planned but not yet scheduled. See [Still pending](#still-pending) for the full gap list. @@ -219,6 +222,7 @@ The engine is fully functional across five delivery modes with enterprise multi- | Chat conversation UX — collapsed work strips replace per-tool boxes; two-level expand (strip → tool list → full detail); live "doing X" in-progress indicator while agent works; clean visual hierarchy where the agent's answer is prominent and tool work is subordinate; consistent Web + Desktop parity | Phase 126 | Planned | | Web search via integrations — move web search out of the hard-coded `WebSearchBackend` enum and into the Integration Gallery; add `IntegrationKind.HttpApi` for direct REST adapters (no MCP process); define `IWebSearchProvider` interface; ship DuckDuckGo (built-in free default), Brave, FireCrawl, Exa, Tavily as `HttpApi` catalog entries; add Crawl4AI as a scraper/fetcher integration; admin picks active search provider from Integrations page; remove `WebSearchBackend` enum; existing MCP search entries remain as alternatives; unit test coverage for WebFetchTool, search providers, and dispatch | Phase 125 | Planned | | Supabase Row Level Security — enable RLS on all privacy-sensitive tables in the Supabase migration and write policies for the `owner_user_id` model; service-role key retains full unrestricted access (Supabase bypasses RLS for service role by design); anon/authenticated JWT callers are scoped to their own data at the database layer; complements the existing application-layer query filters | Phase 127 | Planned | +| Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); guideline conformance check against workspace language pages; works with any code-capable LLM | Phase 128 | Planned (v1.5) | ### v1.0 release polish ✅ @@ -11577,5 +11581,101 @@ The commented-out policy skeletons already exist in `db/supabase/migrations/2026 - After applying the migration, a direct Supabase client authenticated as user A cannot read user B's private `workspace_memory` rows via the dashboard or an Edge Function - User A can read all `workspace_memory` rows where `is_private = 0` regardless of `owner_user_id` - The Sovrant API server (service-role key) continues to read and write all rows without restriction + +--- + +## Phase 128 — Code Generation Quality Gates + +**Status:** Planned (v1.5) + +### Why + +Phase 73 shipped the scaffolding *mechanism* — 21 templates, `CodeCreateTool`, `CodeCreateMultiTool`, 235 tests. The generated code builds and passes its first test. That met the "works" bar. The "industry standard" bar is higher: generated code should also pass the project's linter, conform to the workspace's language guidelines (Phase 108), and arrive with the CI/security/commit infrastructure that any production project has from day one. + +Two root causes hold quality back today: + +1. **No verification loop.** `CodeCreateTool` writes files and stops. If TypeScript complains about `any` types, if Python has an import cycle, if the C# build has a nullable warning — those errors are invisible until the user runs the build themselves. A deterministic post-generation check (compiler, linter, type-checker) closes this loop regardless of which LLM produced the code. + +2. **Scaffold templates meet the minimum bar, not the production bar.** The 21 templates produce runnable skeletons but omit the infrastructure every real project needs: CI pipeline, conventional-commit setup, security scanning, `.gitignore` tuned to the language, README with build/test/deploy instructions. These are mechanical additions, not model quality — they belong in the template, not the prompt. + +This phase is explicitly designed to work with **any LLM capable of code generation** — the feedback loop is compiler output, not LLM judgment. A weaker model that makes a type error gets the same precise error message a strong model would and can fix it on the second pass. + +### Scope + +#### 1 — Language check runners + +A small `ICodeValidator` abstraction with one implementation per supported language. After `CodeCreateTool` writes files to the artifact directory, it invokes the validator for the detected language and captures structured output (file, line, code, message). + +| Language | Check command | Notes | +|---|---|---| +| TypeScript / JS | `tsc --noEmit` | Uses the scaffold's `tsconfig.json`; requires Node.js in PATH | +| Python | `pylint --output-format=json` or `ruff check --output-format=json` | Prefer `ruff` if present (faster) | +| C# / .NET | `dotnet build -v q` | Exit-code + MSBuild error lines | +| Rust | `cargo check --message-format=json` | JSON per-diagnostic | +| Go | `go vet ./...` | Exit code + stderr lines | +| Java | Compile step via Maven/Gradle (`mvn compile -q` / `gradle compileJava -q`) | Requires JDK in PATH | + +If the language runtime is not in PATH the validator reports `skipped` rather than `failed` — generation still completes, and the UI notes which checks ran. + +#### 2 — Self-correction loop + +When validation finds errors: + +1. Format errors as a compact block: `:: ` +2. Inject as a follow-up user turn: `"The code you generated has {N} issue(s). Fix them and emit the corrected files:\n{errors}"` +3. Re-run the model, apply the new file writes, re-validate +4. Repeat at most **2 rounds** — if errors remain after round 2, deliver the best output with a warning + +Max 2 rounds keeps cost predictable. On weaker models 2 rounds resolves the majority of type/lint errors; pathological cases are surfaced to the user rather than silently looped. + +The correction turn is injected into the existing conversation context so the model retains its intent. No new tool is needed — `CodeCreateTool` orchestrates the loop internally. + +#### 3 — Guideline conformance check + +After the compiler/linter pass, a lightweight structural check reads the active workspace language guideline page (Phase 108) and verifies the scaffold's key files against it. This is not a full LLM re-review — it checks for presence/absence of declared patterns: + +- TypeScript: `strict: true` in tsconfig, correct test runner (`vitest` vs `jest`), no bare `any` in generated files +- Python: uses the declared package manager (`uv`, `poetry`, or `pip`), includes `pyproject.toml` +- C#: nullable annotations enabled, correct test framework (`xUnit` vs `NUnit` vs `MSTest`) + +Failures here are warnings, not blockers — the user's guideline page may intentionally diverge. + +#### 4 — Production-grade scaffold enrichment + +Update the 21 scaffolding templates (seeded via V035/V037 knowledge_pages rows) with the following additions, delivered as a new additive migration (`V044__enrich_scaffold_templates.sql`): + +| Addition | Applies to | +|---|---| +| `.github/workflows/ci.yml` — install, lint, test on push/PR | All templates | +| `.gitignore` tuned to the language/runtime | All templates | +| `CHANGELOG.md` + `commitlint.config.js` (or equivalent) for conventional commits | Node.js, C# | +| `README.md` with Build / Test / Deploy sections | All templates | +| `Dockerfile` (multi-stage, minimal base image) | API/server templates | +| Security scanning step in CI (`npm audit`, `pip-audit`, `dotnet list package --vulnerable`) | All templates | +| `.editorconfig` with standard indentation/encoding | All templates | + +These additions are pure file content — no new scaffolding logic. The migration updates the `body` (template content) for each affected scaffold row in `knowledge_pages`. User-overridden scaffold templates are unaffected (copy-on-write overlay wins). + +### What this is not + +- Not a full linting dashboard — the validation runs at generation time only; it does not continuously lint the user's working tree +- Not a style enforcer — guideline conformance is a warning, not a gate +- Not a replacement for the user's own CI — the generated CI config is a starting point, not a production-hardened pipeline + +### Acceptance Criteria + +- [ ] `ICodeValidator` abstraction + implementations for TypeScript, Python, C#, Rust, Go; Java as stretch +- [ ] `CodeCreateTool` runs the validator after file writes; round-trip self-correction fires on non-empty error list; max 2 rounds enforced +- [ ] Validator `skipped` (runtime not in PATH) does not block delivery; UI notes which checks ran +- [ ] After 2 rounds with remaining errors, best output delivered with a `⚠ N issue(s) remain` notice in the artifact detail +- [ ] Production scaffold additions land in `V044__enrich_scaffold_templates.sql`; all 21 templates produce CI config, .gitignore, README, .editorconfig on generation +- [ ] Guideline conformance check verifies key structural properties (tsconfig strict, correct test runner, nullable enabled) against the workspace's language guideline page +- [ ] All existing Phase 73 golden-path tests pass unchanged — this phase adds a post-processing step, not a replacement +- [ ] Tested with at least one "weak" open-weights model (e.g. Llama 3.1 8B via Ollama) — self-correction round resolves at least 70% of type/lint errors on a representative scaffold + +### Deferred + +- Runtime-not-in-PATH bootstrap (auto-installing Node/Python in a sandbox) — too much platform complexity for v1.5; `skipped` validator is acceptable +- Continuous background linting of the user's working tree — separate Phase 129 concern - `supabase db push` applies the migration cleanly with no errors on a fresh project and on an existing project that already has the initial schema - All existing Sovrant API tests pass unchanged — no application code is modified by this phase From c402e9c6648503403e8f52cbca1639ea0b85df88 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 12:53:10 -0400 Subject: [PATCH 14/24] docs(roadmap): add Phase 126 to v1.5 focus wave Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/roadmap.md b/docs/roadmap.md index f8046cae..14da4d00 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,6 +33,7 @@ What we are actively working on and shipping next, in priority order. | **v1.5 — next** | Phase 114 | Enrich built-in skill definitions — richer descriptions (2–3 sentences), ≥5-step workflow bodies, agents/tools lists; SQL migration only, no code changes; immediate quality lift for every LLM via the IKnowledgeRouter harness | | **v1.5 — next** | Phase 74 | Markdown-backed document templates — 44 hardcoded C# templates become editable `.md` files (Scriban expressions, YAML frontmatter field schema, code-behind for computed-logic templates); domain experts iterate without rebuilds | | **v1.5 — next** | Phase 128 | Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); works with any code-capable LLM | +| **v1.5 — next** | Phase 126 | Chat conversation UX — collapsed work strips replace per-tool boxes; two-level expand (strip → tool list → full detail); live "doing X" in-progress indicator; agent answer prominent, tool work subordinate; Web + Desktop parity | > Items below v1.0 are planned but not yet scheduled. See [Still pending](#still-pending) for the full gap list. From 853d20e7368a933c777a324afd32c97236e980e1 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 13:02:31 -0400 Subject: [PATCH 15/24] =?UTF-8?q?feat(knowledge):=20Phase=20114=20?= =?UTF-8?q?=E2=80=94=20enrich=20all=2032=20built-in=20skill=20descriptions?= =?UTF-8?q?=20(V044)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V044 migration updates every BuiltIn skill row (workspace_id='') with: - 2-3 sentence descriptions covering what/when/output for the IKnowledgeRouter harness and Skills page visibility - Agent list wiring for 9 skills that had natural delegations but NULL agents: billing-ops → data-analyst, content-engine/crosspost → content-writer, doc-update → doc-updater, lead-intelligence → sales-intelligence+researcher, project-flow → project-manager, prompt-optimize → prompt-optimizer, refactor → refactor-cleaner, search-first → researcher - verification-loop tools corrected: removes non-existent `Verify` tool Test assertions updated: schema version 43 → 44, migration count 43 → 44. Roadmap: Phase 114 marked Done, migration count 43 → 44, v1.5 row updated. Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 6 +- .../V044__enrich_builtin_skills.sql | 145 ++++++++++++++++++ .../Storage/OldDbUpgradeTests.cs | 4 +- .../Storage/SqliteStorageProviderTests.cs | 4 +- 4 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 src/Sovrant.Runtime/Storage/Migrations/V044__enrich_builtin_skills.sql diff --git a/docs/roadmap.md b/docs/roadmap.md index 14da4d00..06e56f42 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -30,7 +30,7 @@ What we are actively working on and shipping next, in priority order. | **v1.3 — done** | Phase 91 *(partial)* | Knowledge Authoring — admin edit/revert on Skills and Document Templates pages; Monaco editor for prompt + JSON editing; Knowledge sub-nav sorted alphabetically (Web + Desktop); Avalonia 11 → 12 desktop migration ✅ (AvaloniaEdit Desktop fixes deferred) | | **v1.4 — done** | V043 | Email-as-user-id — V043 migration replaces all `usr_{hex}` PKs with email across every FK and soft-reference column; `username` column dropped; `IUserService` API updated; personal workspace IDs updated in tandem ✅ | | **v1.4 — done** | Bug fixes | Ollama provider label/routing fixed (FriendlyProviderName checks provider name before URL); FK error 19 on project creation fixed (PersonalWorkspaceId reads `App.SovrantUserId` at call time) ✅ | -| **v1.5 — next** | Phase 114 | Enrich built-in skill definitions — richer descriptions (2–3 sentences), ≥5-step workflow bodies, agents/tools lists; SQL migration only, no code changes; immediate quality lift for every LLM via the IKnowledgeRouter harness | +| **v1.5 — done** | Phase 114 | Enrich built-in skill definitions — 2-3 sentence descriptions, agent list wiring, `verification-loop` tools fix; V044 migration; immediate quality lift for every LLM via the IKnowledgeRouter harness ✅ | | **v1.5 — next** | Phase 74 | Markdown-backed document templates — 44 hardcoded C# templates become editable `.md` files (Scriban expressions, YAML frontmatter field schema, code-behind for computed-logic templates); domain experts iterate without rebuilds | | **v1.5 — next** | Phase 128 | Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); works with any code-capable LLM | | **v1.5 — next** | Phase 126 | Chat conversation UX — collapsed work strips replace per-tool boxes; two-level expand (strip → tool list → full detail); live "doing X" in-progress indicator; agent answer prominent, tool work subordinate; Web + Desktop parity | @@ -48,7 +48,7 @@ The engine is fully functional across five delivery modes with enterprise multi- - **141 server endpoints** + 1 SignalR hub (chat, sessions, config, status, models, usage, cost, command-center, webhooks, workspaces, projects, users, teams, runs, missions, engine, artifacts, evals, swarm, tools, skills, agents, MCP auth, knowledge, trust-rules, attributions) - **5 delivery modes:** CLI REPL, HTTP server (:5200), desktop app (Avalonia), web app (Blazor :5100), MCP server (stdio) - Agentic loop with up to 20 tool rounds per turn -- SQLite persistence layer with 43 versioned migrations (V001–V043) — V040 `mcp_servers.id` stable surrogate for workspace-scoped gating (Phase 105), V041 `workspace_memory` owner/privacy columns (Phase 123), V042 `session_summaries`/`learned_patterns`/`instincts` owner scoping (Phase 123), V043 email-as-user-id rewrite (replaces `usr_{hex}` PKs), V038 `knowledge_attributions` table (Phase 116F), V039 `keystore` table — master AES key in DB (Phase 96), V030 `is_private` (Phase 98), V031 `agent_name` (Phase 106), V032–V033 `knowledge_pages` + `knowledge_attributions` schema (Phase 108/116F), V034–V037 `role`/`recommended_level` + `fields_json`/`filename_template` + skill/agent/doc-template seeds (Phase 112A–D) on top of the Phase 32/42.5/51/52/57/78 foundation +- SQLite persistence layer with 44 versioned migrations (V001–V044) — V044 enrich built-in skill descriptions + agent/tool list fixes (Phase 114), V040 `mcp_servers.id` stable surrogate for workspace-scoped gating (Phase 105), V041 `workspace_memory` owner/privacy columns (Phase 123), V042 `session_summaries`/`learned_patterns`/`instincts` owner scoping (Phase 123), V043 email-as-user-id rewrite (replaces `usr_{hex}` PKs), V038 `knowledge_attributions` table (Phase 116F), V039 `keystore` table — master AES key in DB (Phase 96), V030 `is_private` (Phase 98), V031 `agent_name` (Phase 106), V032–V033 `knowledge_pages` + `knowledge_attributions` schema (Phase 108/116F), V034–V037 `role`/`recommended_level` + `fields_json`/`filename_template` + skill/agent/doc-template seeds (Phase 112A–D) on top of the Phase 32/42.5/51/52/57/78 foundation - Single `.env` file configuration — `sovrant.config` removed; all bootstrap knobs are env vars; routing and swarm config fully DB-backed - **Integrations Gallery** on Web and Desktop — catalog-first MCP onramp with Automation (Composio, n8n, Zapier, Make), Platform (GitHub, Slack, Notion, Linear, Stripe, PostgreSQL, Supabase, Filesystem), and Search (Brave, Exa, Tavily) tiers; credentials stored in encrypted keystore (Phase 95 ✅) - **Model switcher with provider discovery** — configured providers selectable inline; unconfigured known providers shown with click-to-configure deep-link to Settings → Providers on both Web and Desktop @@ -211,7 +211,7 @@ The engine is fully functional across five delivery modes with enterprise multi- | Migrate built-in markdown knowledge into the DB — move all on-disk built-in markdown (32 skills, 25 agents) plus the 44 code-defined document templates out of the filesystem/C# and into the `knowledge_pages` table so users manage every template (base + their own) in the DB without code changes; copy-on-write overlay model (immutable base rows, user edits become `global`/project overlays that win, revert = delete overlay); built-ins seeded via SQL migration; registries flipped to read DB; document rendering becomes data-driven via a sandboxed templating engine; `.md` files and disk scans deleted once verified; SQLite-only (knowledge store is local even on Postgres backend) | Phase 112 | Done ✅ | | Caching: DB read cache + Phase 31 invalidation fix — `CachedKnowledgeStore` decorator wraps `IKnowledgeStore` with TTL-based in-process caching for rarely-changing content (skills, agents, document templates, tool/doc guides) and fires a `KnowledgePageChanged` event on every write; repairs Phase 31's `CacheInvalidator` whose file-watcher triggers were deleted by Phase 112, restoring HTTP-cache invalidation for `skills:list`, `templates:list`, and `knowledge:*` keys; opt-out via `SOVRANT_KNOWLEDGE_CACHE_TTL=0` | Phase 113 | ✅ Done | | Intelligent Knowledge Harness — on-demand per-turn knowledge loading via `IKnowledgeRouter` (keyword/trigger/intent scoring, no LLM call); dynamic per-turn addendum preserves stable system-prompt cache; full-round-trip PII sanitization for knowledge bodies and tool results; `knowledge_attributions` table; MCP tool relevance filtering per turn; provenance Sources section in Web + Desktop chat | Phase 116 | ✅ Done | -| Enrich built-in skill definitions — review and improve all 32 built-in skill descriptions (2-3 sentences), workflow bodies (≥5 concrete steps), agents/tools lists, and trigger phrases; ship as a `V038__enrich_builtin_skills.sql` additive migration that UPDATEs only the BuiltIn base rows; user overlays are unaffected | Phase 114 | Planned | +| Enrich built-in skill definitions — 2-3 sentence descriptions, agent list wiring, `verification-loop` tools fix; V044 additive migration updates all 32 BuiltIn base rows; user overlays unaffected | Phase 114 | ✅ Done | | API endpoint integration — connect REST and GraphQL APIs as first-class platform integrations alongside MCP servers; harness auto-discovers endpoints via OpenAPI/Swagger spec import or GraphQL introspection, or admin can manually describe specific endpoints; discovered endpoints exposed as typed tools through the same `MCPTool` proxy layer (trust rules, session picker, `FilteredToolRegistry`); admin configures per workspace; credentials stored in encrypted keystore; Web + Desktop parity | Phase 117 | Planned | | Bootstrap configuration — declarative YAML file (`sovrant.bootstrap.yaml`) that pre-configures a Sovrant installation before or at first run; covers providers, MCP servers, knowledge/skills, agent templates, workspace setup, admin users, permission defaults, and branding; Sovrant installs and starts normally then applies the bootstrap idempotently; enables sales-assisted and partner-delivered custom installs without code changes | Phase 118 | Planned | | Orchestration improvements — enhanced mission run-mode for teams and swarms; missions get a named run-mode (autonomous, supervised, step-through) set at launch rather than inherited from global permission; Claws sourced from configured platform integrations can be added as team members if the integration is connected, giving orchestrations access to external agents alongside local ones | Phase 119 | Planned | diff --git a/src/Sovrant.Runtime/Storage/Migrations/V044__enrich_builtin_skills.sql b/src/Sovrant.Runtime/Storage/Migrations/V044__enrich_builtin_skills.sql new file mode 100644 index 00000000..d21daa20 --- /dev/null +++ b/src/Sovrant.Runtime/Storage/Migrations/V044__enrich_builtin_skills.sql @@ -0,0 +1,145 @@ +-- V044: Enrich built-in skill descriptions and agent/tool lists (Phase 114). +-- Updates `description` to 2-3 sentence form (what, when, output) for all 32 BuiltIn skills. +-- Wires `agents` lists where natural delegations exist and fixes incorrect tool references. +-- Only touches BuiltIn base rows (workspace_id=''); user overlay rows are never clobbered. + +-- ─── SKILLS ──────────────────────────────────────────────────────────────── + +UPDATE knowledge_pages +SET description = 'Creates a structured Architecture Decision Record (ADR) to document an architectural choice, its context, and trade-offs. Use when your team needs a permanent record of a major design decision — framework selection, data model, API boundary. Produces a markdown ADR with options considered, decision rationale, and consequences.' +WHERE slug = 'architecture-decision' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Writes high-quality long-form content — blog posts, essays, newsletters — with voice matching and anti-slop discipline. Use when you need polished published content, not a quick draft. Produces a title, subtitle, structured body with headers, and an optional call to action.' +WHERE slug = 'article-writing' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Designs and executes configurable autonomous loops across six patterns: simple poll, pipeline, watch-and-react, retry-with-backoff, fan-out/fan-in, and DAG. Use when a task needs to repeat, monitor a condition, or process a queue of items with dependency ordering. Always produces a loop with a defined termination condition, per-iteration logging, and rate-limit awareness.' +WHERE slug = 'autonomous-loop' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Triages refund requests, analyses subscription churn patterns, and produces prioritised retention recommendations from billing data. Use when investigating cancellation spikes, processing a batch of refund tickets, or preparing a quarterly retention report. Produces a triage summary (auto-approve / review / escalate counts), a churn dashboard, and a ranked action-item list.', + agents = '["data-analyst"]' +WHERE slug = 'billing-ops' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Analyses 5–20 writing samples to extract a reusable brand voice profile covering tone, sentence structure, vocabulary, and signature rhetorical patterns. Use before generating any content that must match an established person or company voice. Produces a structured voice profile and a sample paragraph demonstrating the extracted style.' +WHERE slug = 'brand-voice' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Generates a comprehensive onboarding guide for a new contributor: architecture overview, tech stack, setup instructions, key patterns, and common gotchas. Use at the start of a new project or when onboarding someone who needs to get productive quickly. Produces a structured markdown guide with a Quick Start section, architecture map, and recommended reading order.' +WHERE slug = 'codebase-onboard' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Performs a systematic code review with findings ranked by severity (CRITICAL / HIGH / MEDIUM / LOW) covering logic errors, security vulnerabilities, performance issues, and style violations. Use on any diff or pull request before merge, or when auditing an unfamiliar codebase. Produces a structured findings list with file:line references, explanations, and suggested fixes.' +WHERE slug = 'code-review' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Audits a professional network, scores contacts by strategic value and relationship strength, identifies gaps, and drafts warm re-engagement messages for dormant high-value contacts. Use when preparing for a fundraise, hiring push, or strategic partnership campaign. Produces a network health report, ranked contact list, recommended additions, and personalised outreach drafts.' +WHERE slug = 'connections-optimizer' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Repurposes a single piece of anchor content (article, talk, or thread) into platform-optimised versions for X/Twitter, LinkedIn, newsletter, and blog. Use after publishing long-form content to maximise reach without duplicating the same text across channels. Produces a self-contained adapted version for each target channel with platform-native formatting and a varied hook.', + agents = '["content-writer"]' +WHERE slug = 'content-engine' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Adapts a single piece of content for multiple platforms by applying platform-specific constraints: character limits, formatting conventions, hashtag norms, and tone. Use when you have a finished piece and need ready-to-post versions for X, LinkedIn, Threads, Mastodon, or a blog. Produces a self-contained adapted version for each requested platform.', + agents = '["content-writer"]' +WHERE slug = 'crosspost' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Builds and executes a structured data collection pipeline: discover sources, fetch pages via WebFetch, clean and normalise, enrich with cross-references, and store as structured output. Use when you need to extract and consolidate data from websites, documents, or APIs into a queryable format. Produces a data summary with record counts, completeness metrics, sample records, and an output file.' +WHERE slug = 'data-scraper' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Conducts thorough multi-source research (15–30 sources), cross-references claims, scores confidence per finding, and produces a structured report with inline citations. Use for strategic decisions, due diligence, or technical investigations where a single source is insufficient. Produces an executive summary, detailed findings with citations, per-finding confidence levels, and identified gaps.' +WHERE slug = 'deep-research' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Locates and extracts specific API, library, or framework documentation — parameters, return types, examples, and version-specific gotchas — from official sources. Use when you need a precise, directly actionable reference without manually reading through an entire doc site. Produces a source URL, API signature, parameter table, usage example, and any known gotchas.' +WHERE slug = 'doc-lookup' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Keeps documentation in sync with recent code changes: locates affected docs, audits accuracy, rewrites outdated sections, verifies links, and adds missing coverage for new APIs. Use after any non-trivial code change or before a release to prevent documentation drift. Produces updated documentation files and a summary of what changed and what was added.', + agents = '["doc-updater"]' +WHERE slug = 'doc-update' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Creates investor-ready materials: a 12-slide pitch deck following the Problem→Solution→Market→Traction→Team→Ask structure, a one-page executive summary, and optionally basic financial projections. Use when preparing for a fundraise, demo day, or investor introduction. Produces markdown-formatted deck slides with speaker notes, a one-pager, and a financially consistent model if requested.' +WHERE slug = 'investor-materials' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Researches and qualifies potential leads with a three-axis score (ICP fit, timing signals, warm-path access), ranks the top prospects, surfaces mutual connections, and drafts personalised first-touch messages. Use when building a targeted outreach list or preparing for a sales push into a new segment. Produces a scored lead table and personalised outreach drafts for the top five.', + agents = '["sales-intelligence","researcher"]' +WHERE slug = 'lead-intelligence' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Produces structured competitive and market analysis: competitor mapping, feature/price matrix, SWOT summaries, TAM/SAM/SOM estimates with methodology, and trend analysis with source attribution. Use when evaluating a market entry, positioning a product, or preparing for a strategic planning session. Produces a sourced report with a competitor matrix, market-size estimates, and strategic recommendations.' +WHERE slug = 'market-research' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Crafts and executes optimised generation prompts for image, video, or audio AI services, iterates on outputs against the brief, and delivers the final file with reproducible prompt metadata. Use when you need AI-generated visuals or audio as part of a project or campaign. Produces a final media file and the generation prompt saved alongside it for reproducibility.' +WHERE slug = 'media-gen' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Decomposes a complex goal into a phased implementation plan with components, dependencies, risk assessment, and acceptance criteria per phase — before writing any code. Use before starting any non-trivial implementation to reach alignment and surface unknowns early. Produces a phased plan document presented for user review and approval before execution begins.' +WHERE slug = 'plan' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Performs structured product analysis: feature audit, user need mapping, competitive comparison, gap analysis, and prioritisation using an impact/effort matrix. Use when evaluating a roadmap, planning a feature investment, or preparing a competitive positioning brief. Produces a feature matrix, user-need map, ranked gap list, and build/buy/partner recommendations.' +WHERE slug = 'product-lens' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Coordinates project work by triaging open issues, verifying consistency with current code state, updating priorities, and producing a clear status report. Use at the start of a sprint, during triage, or when a project feels out of sync with its issue tracker. Produces an active-work summary, triage results, stale-item list, and blocker callouts.', + agents = '["project-manager"]' +WHERE slug = 'project-flow' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Analyses a prompt through a 6-phase evaluation (intent, gaps, structure, constraints, rewrite, comparison) and produces an improved version with a bullet-by-bullet changelog. Use whenever a prompt is producing inconsistent, off-target, or verbose model outputs. Produces the original prompt with annotated issues, an optimised rewrite, and an explanation of every change made.', + agents = '["prompt-optimizer"]' +WHERE slug = 'prompt-optimize' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Identifies and safely removes dead code, consolidates duplicates, simplifies complex conditionals, and improves naming — without changing any observable behaviour. Use when a codebase has grown messy or before adding a new feature to a tangled area. Produces incremental edits with a full test run after each change and a summary checklist of what was cleaned.', + agents = '["refactor-cleaner"]' +WHERE slug = 'refactor' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Forces thorough research before any implementation: locates existing solutions, compares 2–3 approaches with trade-offs, checks for known bugs and deprecations, and presents a ranked recommendation for user approval. Use at the start of any non-trivial task to avoid reinventing the wheel or repeating known mistakes. Produces a research summary with a recommended approach — no code until the user confirms.', + agents = '["researcher"]' +WHERE slug = 'search-first' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Performs a systematic OWASP Top 10 security audit covering injection, auth failures, sensitive data exposure, access control, and vulnerable dependencies, plus a secret scan for hardcoded credentials. Use on any codebase before release, after a significant feature addition, or as part of a compliance audit. Produces a severity-ranked findings table with OWASP category, file:line references, and specific remediation steps.' +WHERE slug = 'security-review' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Captures a successful workflow pattern as a reusable skill definition: identifies the repeatable steps, required tools and agents, an intuitive slash-command trigger, and saves it to the Skills registry. Use after completing a workflow you expect to repeat, or to formalise a team standard. Produces a saved skill definition ready to invoke from the Skills page or via its trigger command.' +WHERE slug = 'skill-create' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Creates a structured presentation deck as markdown or HTML slides: confirms brief, builds an outline, writes each slide with one key idea and speaker notes, and ensures a clean narrative arc from problem to call to action. Use when you need a presentation for a meeting, conference, or internal review. Produces a complete deck with title, context, core-content, summary, and CTA slides.' +WHERE slug = 'slides' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Drives strict Test-Driven Development through the Red-Green-Refactor cycle: write a failing test, confirm it fails, write minimal passing code, confirm it passes, refactor, confirm coverage. Use for any new feature or bug fix where correctness and test coverage are non-negotiable. Produces implementation code with a matching test suite, a confirmed green build, and coverage meeting the configured threshold.' +WHERE slug = 'tdd-workflow' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Assembles a team of specialised agents, writes self-contained briefs for each, dispatches them in parallel, and synthesises their outputs into a coherent result. Use when a task has clearly parallelisable sub-tasks — research + implementation + review — that different specialist agents can handle independently. Produces individual agent deliverables merged into a unified outcome with a monitoring summary.' +WHERE slug = 'team-builder' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Builds an interactive UI prototype as a single self-contained HTML file with inline CSS and JS, realistic sample data, responsive layout, and hover/transition states — no build step required. Use to quickly demonstrate a layout, interaction pattern, or user flow before committing to a full implementation. Produces a browser-ready file the user can open immediately.' +WHERE slug = 'ui-demo' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Runs the structured 6-phase quality gate pipeline in sequence: build, type-check, lint, test with coverage, security scan, and diff review for debug code or secrets. Use before merging any change or after a significant implementation session to confirm the codebase is clean. Produces a pass/fail result per phase with actionable failure details and the precise command that failed.', + -- Remove the non-existent `Verify` tool; keep the real tools only + tools = '["Bash","Read","Grep","Glob"]' +WHERE slug = 'verification-loop' AND tier = 'BuiltIn' AND kind = 'skills'; + +UPDATE knowledge_pages +SET description = 'Creates animated technical explainers as self-contained HTML files using CSS animations or inline SVG: script with timing markers, storyboard with progressive reveal, and a fully playable animation requiring no build step. Use to communicate a complex concept visually — architecture diagrams, algorithm walkthroughs, data flows. Produces a single browser-ready HTML file with embedded script, animation, and labels.' +WHERE slug = 'video-explainer' AND tier = 'BuiltIn' AND kind = 'skills'; diff --git a/tests/Sovrant.Runtime.Tests/Storage/OldDbUpgradeTests.cs b/tests/Sovrant.Runtime.Tests/Storage/OldDbUpgradeTests.cs index ff3d8e2b..ec45fda2 100644 --- a/tests/Sovrant.Runtime.Tests/Storage/OldDbUpgradeTests.cs +++ b/tests/Sovrant.Runtime.Tests/Storage/OldDbUpgradeTests.cs @@ -207,9 +207,9 @@ public async Task UpgradeFromV005_SchemaVersionRowsStamped() while (r.Read()) rows.Add((r.GetInt32(0), r.IsDBNull(1) ? null : r.GetString(1))); - // V001..V005 applied manually (no checksum), V006..V043 applied + // V001..V005 applied manually (no checksum), V006..V044 applied // by the real runner (with checksum). - Assert.Equal(43, rows.Count); + Assert.Equal(44, rows.Count); Assert.All(rows.Where(x => x.v <= 5), x => Assert.Null(x.c)); Assert.All(rows.Where(x => x.v >= 6), x => Assert.False(string.IsNullOrEmpty(x.c))); diff --git a/tests/Sovrant.Runtime.Tests/Storage/SqliteStorageProviderTests.cs b/tests/Sovrant.Runtime.Tests/Storage/SqliteStorageProviderTests.cs index d3f88bd3..0f204cbb 100644 --- a/tests/Sovrant.Runtime.Tests/Storage/SqliteStorageProviderTests.cs +++ b/tests/Sovrant.Runtime.Tests/Storage/SqliteStorageProviderTests.cs @@ -35,8 +35,8 @@ public async Task InitializeAsync_SetsSchemaVersion() { await _provider.InitializeAsync(); - // Schema head bumped to 43 by V043 (email as user_id). - Assert.Equal(43, _provider.SchemaVersion); + // Schema head bumped to 44 by V044 (enrich built-in skill descriptions). + Assert.Equal(44, _provider.SchemaVersion); } [Fact] From 60f83d4f5f027fcfe7910812cd6f732a9deb8274 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Mon, 29 Jun 2026 13:28:26 -0400 Subject: [PATCH 16/24] =?UTF-8?q?docs(roadmap):=20add=20Phase=20129=20?= =?UTF-8?q?=E2=80=94=20Missions=20=E2=86=92=20Workflows=20rename=20+=20UX?= =?UTF-8?q?=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 129 full section: surface-label rename only (no DB/runtime changes), dedicated Workflows page (goal-first launch form, active/recent cards, journal + artifacts detail view), positioning callout distinguishing AI-driven workflows from trigger-automation (n8n/Zapier/Make via MCP), /v1/workflows API alias proxying to /v1/missions, Phase 119 run-modes in the launch form; explicit non-goals table (no node editor, no cron, no connector library) - Still pending table: Phase 129 row added - v1.5 focus table: Phase 129 row added after Phase 126 - Last Updated header: note Phase 129 and v1.5 focus update Co-Authored-By: Claude Sonnet 4.6 --- docs/roadmap.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 06e56f42..adbef329 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,7 +1,7 @@ # Sovrant — Roadmap **Branch:** `development` -**Last updated:** 2026-06-29 (Phase 128 planned — code generation quality gates (post-generation lint/self-correction loop, production-grade scaffold enrichment). v1.5 focus: Phase 114 skill enrichment + Phase 74 markdown document templates + Phase 128. Phase 123 ✅ — Memory System: workspace memory with public/private scoping, "+Remember" button in Chat (Web + Desktop), V041/V042 migrations, per-user injection in multi-user deployments. V040 MCP stable IDs (Phase 105 workspace-level gating). V043 email-as-user-id (replaces `usr_{hex}` PKs). PostgresSchema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/` (Phase 40C documentation update). Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) +**Last updated:** 2026-06-29 (Phase 129 planned — Missions → Workflows rename + UX (surface labels only, dedicated Workflows page, positioning callout, `/v1/workflows` alias). Phase 128 planned — code generation quality gates (post-generation lint/self-correction loop, production-grade scaffold enrichment). v1.5 focus: Phase 114 ✅ skill enrichment + Phase 74 markdown document templates + Phase 128 + Phase 126 + Phase 129. Phase 123 ✅ — Memory System: workspace memory with public/private scoping, "+Remember" button in Chat (Web + Desktop), V041/V042 migrations, per-user injection in multi-user deployments. V040 MCP stable IDs (Phase 105 workspace-level gating). V043 email-as-user-id (replaces `usr_{hex}` PKs). PostgresSchema split to `db/postgres/PostgresSchema.sql` + `db/supabase/migrations/` (Phase 40C documentation update). Phase 127 planned — Supabase RLS. Phase 126 planned — chat conversation UX: collapsed work strips. Phase 125 planned — web search via integrations. Phase 124 planned — file system access controls. Phase 96 ✅ — MCP runtime variables: inline env var editor Web + Desktop, keystore in DB (V039). Phase 116 ✅ — Intelligent Knowledge Harness complete: A–H shipped; knowledge_attributions table, IKnowledgeRouter, per-turn PII sanitization, MCP tool relevance filtering, provenance Sources UI. Phase 113 ✅ — CachedKnowledgeStore + Phase 31 CacheInvalidator repair. Phase 112 ✅ — all built-in markdown (skills, agents, 42 doc templates) in DB; dual-write removed. Phase 108 ✅ — knowledge_pages universal store. Phase 103 ✅ — MCP trust gates + trust rules editor UI. Phase 101 ✅ — OAuth 2.1 + PKCE for MCP.) This document tracks planned features, architectural decisions, and the reasoning behind them. @@ -34,6 +34,7 @@ What we are actively working on and shipping next, in priority order. | **v1.5 — next** | Phase 74 | Markdown-backed document templates — 44 hardcoded C# templates become editable `.md` files (Scriban expressions, YAML frontmatter field schema, code-behind for computed-logic templates); domain experts iterate without rebuilds | | **v1.5 — next** | Phase 128 | Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); works with any code-capable LLM | | **v1.5 — next** | Phase 126 | Chat conversation UX — collapsed work strips replace per-tool boxes; two-level expand (strip → tool list → full detail); live "doing X" in-progress indicator; agent answer prominent, tool work subordinate; Web + Desktop parity | +| **v1.5 — next** | Phase 129 | Missions → Workflows — surface-label rename (no DB/engine changes); dedicated Workflows page with goal-first launch, active/recent cards, detail view; positioning callout (AI workflows vs n8n/Zapier automation); `/v1/workflows` API alias | > Items below v1.0 are planned but not yet scheduled. See [Still pending](#still-pending) for the full gap list. @@ -224,6 +225,7 @@ The engine is fully functional across five delivery modes with enterprise multi- | Web search via integrations — move web search out of the hard-coded `WebSearchBackend` enum and into the Integration Gallery; add `IntegrationKind.HttpApi` for direct REST adapters (no MCP process); define `IWebSearchProvider` interface; ship DuckDuckGo (built-in free default), Brave, FireCrawl, Exa, Tavily as `HttpApi` catalog entries; add Crawl4AI as a scraper/fetcher integration; admin picks active search provider from Integrations page; remove `WebSearchBackend` enum; existing MCP search entries remain as alternatives; unit test coverage for WebFetchTool, search providers, and dispatch | Phase 125 | Planned | | Supabase Row Level Security — enable RLS on all privacy-sensitive tables in the Supabase migration and write policies for the `owner_user_id` model; service-role key retains full unrestricted access (Supabase bypasses RLS for service role by design); anon/authenticated JWT callers are scoped to their own data at the database layer; complements the existing application-layer query filters | Phase 127 | Planned | | Code generation quality gates — post-generation lint/type-check inside `CodeCreateTool`; errors fed back to the LLM for up to 2 self-correction rounds; production-grade scaffold enrichment (CI config, conventional commits, .gitignore, security scanning); guideline conformance check against workspace language pages; works with any code-capable LLM | Phase 128 | Planned (v1.5) | +| Missions → Workflows rename and UX review — surface-label rename only (no DB/runtime changes); dedicated Workflows page (goal-first launch, active/recent cards, detail view with journal + artifacts); positioning callout distinguishing AI-driven workflows from trigger-automation (n8n/Zapier/Make); `/v1/workflows` alias for `/v1/missions`; Phase 119 run-modes surfaced in the launch form | Phase 129 | Planned (v1.5) | ### v1.0 release polish ✅ @@ -11678,5 +11680,117 @@ These additions are pure file content — no new scaffolding logic. The migratio - Runtime-not-in-PATH bootstrap (auto-installing Node/Python in a sandbox) — too much platform complexity for v1.5; `skipped` validator is acceptable - Continuous background linting of the user's working tree — separate Phase 129 concern -- `supabase db push` applies the migration cleanly with no errors on a fresh project and on an existing project that already has the initial schema -- All existing Sovrant API tests pass unchanged — no application code is modified by this phase + +--- + +## Phase 129 — Missions → Workflows: Rename, Positioning, and UX Review + +**Status:** Planned (v1.5) + +### Why + +The term "missions" is accurate for the engine internals — a bounded goal delegated to an AI team with a planner, executor, and journal — but it is opaque to users. Almost everyone arriving at Sovrant calls this concept a "workflow." Renaming closes the vocabulary gap without changing what the engine does. + +More importantly, the rename is the right moment to resolve a positioning question the codebase sidesteps today: **what kind of workflows does Sovrant own, and what does it deliberately leave to external tools?** + +The answer is clear from what's already shipped: +- **Sovrant owns**: AI-orchestrated, goal-driven workflows where the "steps" are LLM agents making decisions — not predetermined trigger→action chains. The planner decomposes the goal; the executor routes it; the agents adapt. This is the lane n8n and Zapier cannot fill. +- **n8n, Zapier, Make, Composio own**: trigger-based automation, scheduled jobs, branching deterministic pipelines, and hundreds of SaaS connectors. These are already available in Sovrant via the Integrations Gallery as MCP connections — Sovrant acts as the AI layer on top of them, not a replacement for them. + +Without this distinction surfaced in the UI, users either expect Sovrant to be a Zapier clone (and are disappointed) or miss entirely that they can run complex AI-driven goals with a single prompt. + +The UX also needs work independent of naming. Today "missions" is discoverable only through the Orchestration Studio → Run button and the Command Center grid. There is no dedicated Workflows page where a user can launch, monitor, and review their AI-driven workflows without knowing what an "orchestration" or "mission" is first. + +### What ships + +#### 1 — Surface label rename (Web + Desktop) + +Replace every user-visible "Mission" / "Missions" label with "Workflow" / "Workflows" across both surfaces. The underlying DB table (`missions`), API path (`/v1/missions`), and C# types (`IMissionStore`, `LlmMissionPlanner`, etc.) are **not renamed** — this is a presentation-layer change only. + +| Where | Old label | New label | +|---|---|---| +| Nav / rail | Missions | Workflows | +| Orchestration Studio | Run Mission | Run Workflow | +| Command Center | Missions column | Workflows column | +| User Dashboard | missions stat | workflows stat | +| Page titles | Missions | Workflows | +| Privacy toggle | Make mission private | Make workflow private | +| Completion messages | "Mission complete" | "Workflow complete" | + +**API compatibility:** Add `/v1/workflows` as an alias that proxies to `/v1/missions` on all CRUD and status endpoints. The `/v1/missions` path is retained and marked `@deprecated` in comments (not removed — no external consumers confirmed yet, but a grace period is the right call). + +#### 2 — Dedicated Workflows page + +Replace or supplement the current Orchestration Studio with a standalone `/workflows` page (Web) and `WorkflowsView` (Desktop) that is the primary entry point for AI-driven workflows. The Orchestration Studio can remain for team composition; the Workflows page is for goal-level users who don't care about the agent structure underneath. + +**Page layout:** +- **Header**: "Workflows" + "New Workflow" button (prominent) +- **Active workflows**: card row — name, goal snippet, team name, run-mode badge (Autonomous / Supervised / Step-through), live status indicator (dot + "Running step 3 of ~5"), elapsed time +- **Recent workflows**: collapsible list of completed/failed — name, team, finish time, outcome badge (✅ Complete / ❌ Failed), link to detail +- **Empty state**: "No workflows yet. A workflow gives an AI team a goal and lets it figure out the steps." + "New Workflow" CTA + +**What it is not**: a node editor, a visual flow diagram, a cron scheduler. + +#### 3 — New Workflow launch form + +A simple form replacing (or complementing) the current Orchestration Studio run path: + +``` +Goal: [textarea — describe what you want accomplished] +Team: [dropdown of configured teams, or "Let Sovrant pick"] +Run mode: [Autonomous | Supervised | Step-through] +Name: [optional, auto-generated from goal if blank] +[Run Workflow] +``` + +The "Let Sovrant pick" team option uses `AgentOrchestrator` in decomposition mode — the engine selects agents based on the goal. This is the one-click path for users who don't want to configure a team first. + +#### 4 — Workflow detail view + +Each workflow gets a detail page/view showing: +- **Goal** — the original prompt +- **Team** — agents involved, with roles +- **Journal** — chronological list of events (agent step started, tool called, step complete, replanning triggered) — collapsible by default, expandable for debugging +- **Artifacts** — files produced, with open/download links +- **Output summary** — the final LLM-produced summary of what was accomplished +- **Status timeline** — duration per step if available + +This replaces the current "click-through from Command Center" flow, which lands on a sparse detail page. + +#### 5 — Positioning callout in the UI + +A short inline note on the Workflows page (dismissible, not a modal) that surfaces the positioning: + +> **Workflows vs automation tools.** Sovrant workflows are AI-driven — you describe a goal and the AI team plans and executes the steps. For trigger-based automation, scheduled jobs, and SaaS connectors, connect n8n, Zapier, or Make from the [Integrations](link) page and use them as tools inside a workflow. + +This single sentence prevents the most common expectation mismatch. + +### What we explicitly do not build + +| Not building | Why | Alternative | +|---|---|---| +| Visual node/flow editor | That is n8n's lane | Connect n8n via MCP Integrations | +| Cron / scheduled triggers | That is n8n/Zapier's lane | n8n workflows can call Sovrant's API | +| Webhook-triggered workflows | Same | n8n trigger → Sovrant REST call | +| 500-connector library | Already covered | Composio / n8n in the Integrations Gallery | +| Deterministic branching pipelines | LLM planner handles branching better | No alternative needed | + +The constraint is intentional and permanent. Sovrant's value is the AI layer. Adding a visual editor or a scheduler would compete with tools that have years of head start and would dilute what makes Sovrant different. + +### Relationship to other phases + +- **Phase 51** ✅ built the engine (`IMissionStore`, `LlmMissionPlanner`, `ParallelMissionExecutor`) — not touched +- **Phase 119** (planned) adds per-mission run-modes and integration-sourced Claws — Phase 129 uses those run-modes in the new launch form; Phase 119 should ship first or in parallel +- **Phase 94** ✅ (Orchestration Studio) remains as the team-composition surface; Phase 129 adds a goal-first surface on top + +### Acceptance criteria + +- [ ] Every user-visible "Mission" / "Missions" label replaced with "Workflow" / "Workflows" on Web and Desktop; no DB or runtime type names changed +- [ ] `/v1/workflows` alias proxies to `/v1/missions`; all CRUD and status operations work identically through both paths +- [ ] Dedicated Workflows page reachable from nav; shows active + recent workflows; "New Workflow" button launches the new goal form +- [ ] New Workflow form: goal textarea, team picker with "Let Sovrant pick" option, run-mode selector, optional name field +- [ ] "Let Sovrant pick" path invokes `AgentOrchestrator` in decomposition mode and runs the workflow without requiring a pre-built team +- [ ] Workflow detail view shows goal, journal (collapsible), artifacts, and output summary +- [ ] Positioning callout visible on Workflows page; dismissible per user; links to Integrations +- [ ] All existing mission/orchestration tests pass unchanged (no engine code modified) +- [ ] Web + Desktop parity on all new UI surfaces From d8c772aa7bbbe1c7341360954c6cef8cfc4a3942 Mon Sep 17 00:00:00 2001 From: Eric Ramseur Date: Wed, 1 Jul 2026 09:52:21 -0400 Subject: [PATCH 17/24] =?UTF-8?q?feat(phase-128):=20Code=20generation=20qu?= =?UTF-8?q?ality=20gates=20=E2=80=94=20Parts=20A=E2=80=93D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A — Artifact security hardening - ArtifactRoutes.cs: zip download endpoint with correct content-disposition - ArtifactRoutes.cs: #pragma disable CA1849 around ZipArchiveEntry.Open() (no async overload) - Program.cs: X-Content-Type-Options / Cache-Control headers on artifact serve; force-download for unsafe-inline file types (html, js, svg, etc.) - Artifacts.razor: remove LocalArtifactStore cast; use IArtifactStore.ListAsync + IWorkspaceService.ListAllAsync; ReadAsync for preview; ArtifactItem gains WorkspaceId/WorkspaceName, drops FullDiskPath; .sln/.editorconfig added to TextExtensions - RemoteArtifactStore: field-name fix (stub SetCodeMetadataAsync added) B — Code manifest in ArtifactManifest - ArtifactManifest: new CodeManifest nested type (template_id, language, kind, build/run/test commands, entry_point); ArtifactManifest.Code property - IArtifactStore: SetCodeMetadataAsync(handle, metadata, ct) - LocalArtifactStore: async SetCodeMetadataAsync reads/merges _manifest.json - ScaffoldCommands.cs: new static helper deriving build/run/test/entry-point per language+kind for all 21 scaffold types (#pragma CA1308 — ASCII IDs) - CodeCreateTool/CodeCreateMultiTool: call SetCodeMetadataAsync after scaffold C — Scaffold enrichment (all 21 templates) - All 5 .NET scaffolds: .sln (SDK-style GUIDs), Directory.Build.props, .editorconfig, .github/workflows/ci.yml (dotnet build + test) - All 16 non-.NET scaffolds: .github/workflows/ci.yml per language (node/go/python/rust/java/kotlin/ruby/swift/lua/zig/cpp) D — LLM instruction enrichment - IProjectTemplate: optional default interface members BuildCommand, RunCommand, TestCommand, EntryPoint (null = ScaffoldCommands default; no existing impl changes) - CodeCreateTool: build_command/run_command/test_command/next_steps in response; BuildCodeManifest prefers template overrides over ScaffoldCommands defaults; ToolDefinition.Description updated - CodeCreateMultiTool: per-component commands + next_steps in response; ToolDefinition.Description updated - V045: seed knowledge_pages kind=''tools'' BuiltIn rows for CodeCreate and CodeCreateMulti with usage guide bodies - MigrationRunnerTests/OldDbUpgradeTests: bump schema version assertions to 45 Co-Authored-By: Claude Sonnet 4.6 --- docs/document-templates.md | 310 ++++++++++++++ docs/roadmap.md | 405 +++++++++++------- src/Sovrant.Api.Client/RemoteArtifactStore.cs | 15 +- src/Sovrant.Cli/DocumentCommand.cs | 112 +++++ .../Artifacts/ArtifactManifest.cs | 47 ++ .../Artifacts/IArtifactStore.cs | 7 + .../Artifacts/LocalArtifactStore.cs | 56 ++- .../Projects/Templates/IProjectTemplate.cs | 24 ++ .../V045__seed_builtin_tool_guides.sql | 97 +++++ src/Sovrant.Server/Routes/ArtifactRoutes.cs | 83 ++++ .../Projects/CodeCreateMultiTool.cs | 47 +- src/Sovrant.Tools/Projects/CodeCreateTool.cs | 51 ++- .../Scaffolds/DotNet/DotNetBlazorScaffold.cs | 86 ++++ .../Scaffolds/DotNet/DotNetConsoleScaffold.cs | 86 ++++ .../Scaffolds/DotNet/DotNetLibraryScaffold.cs | 86 ++++ .../Scaffolds/DotNet/DotNetWebApiScaffold.cs | 86 ++++ .../Scaffolds/DotNet/DotNetWorkerScaffold.cs | 79 ++++ .../Projects/Scaffolds/Go/GoApiScaffold.cs | 22 + .../Scaffolds/Java/JavaMavenAppScaffold.cs | 21 + .../Scaffolds/Minimal/CppCmakeScaffold.cs | 19 + .../Minimal/KotlinConsoleScaffold.cs | 21 + .../Scaffolds/Minimal/LuaScriptScaffold.cs | 19 + .../Scaffolds/Minimal/RubyScriptScaffold.cs | 20 + .../Scaffolds/Minimal/SwiftCliScaffold.cs | 18 + .../Scaffolds/Minimal/ZigCliScaffold.cs | 21 + .../Scaffolds/Node/NodeCliScaffold.cs | 23 + .../Scaffolds/Node/NodeExpressApiScaffold.cs | 23 + .../Scaffolds/Node/NodeLibraryScaffold.cs | 23 + .../Scaffolds/Node/NodeMonorepoScaffold.cs | 26 ++ .../Scaffolds/Node/NodeNextJsScaffold.cs | 23 + .../Scaffolds/Python/PythonFastApiScaffold.cs | 22 + .../Scaffolds/Python/PythonScriptScaffold.cs | 22 + .../Scaffolds/Rust/RustCliScaffold.cs | 20 + .../Projects/Scaffolds/ScaffoldCommands.cs | 85 ++++ .../Components/Pages/Artifacts.razor | 139 +++--- src/Sovrant.Web/Program.cs | 29 +- .../Storage/MigrationRunnerTests.cs | 10 +- .../Storage/OldDbUpgradeTests.cs | 10 +- 38 files changed, 2036 insertions(+), 257 deletions(-) create mode 100644 docs/document-templates.md create mode 100644 src/Sovrant.Runtime/Storage/Migrations/V045__seed_builtin_tool_guides.sql create mode 100644 src/Sovrant.Tools/Projects/Scaffolds/ScaffoldCommands.cs diff --git a/docs/document-templates.md b/docs/document-templates.md new file mode 100644 index 00000000..6a8ac1fc --- /dev/null +++ b/docs/document-templates.md @@ -0,0 +1,310 @@ +# Document Template Authoring Guide + +Document templates in Sovrant are stored as rows in the `knowledge_pages` table +(`kind = 'document-templates'`). The body is a Scriban template that renders to +Markdown (or structured JSON for Excel), and the field schema is stored as a +JSON array in the `fields_json` column. + +This guide covers everything a domain expert needs to author, edit, test, and +deploy a new document template without touching C#. + +--- + +## Quick Start + +1. **Open the Knowledge UI** → Documents → find the template you want to edit. + Click **Edit** to modify the body or fields inline. Changes land in a + `Global`-tier overlay row that shadows the built-in automatically. + +2. **Or write a SQL migration** (see [Adding a new template via migration](#adding-a-new-template-via-migration)). + +3. **Validate** with the CLI: + ``` + sovrant document lint --id legal/nda + ``` + +--- + +## Template Body — Scriban Syntax + +The body is a [Scriban](https://github.com/scriban/scriban) template. Scriban +uses `{{ }}` for expressions and `{% %}` for statements. + +### Variable interpolation + +```scriban +**Client:** {{ client_name }} +**Date:** {{ format_date effective_date }} +``` + +### Conditionals + +```scriban +{{ if governing_law && governing_law != "" }} +## Governing Law +This agreement is governed by the laws of {{ governing_law }}. +{{ end }} +``` + +### Loops over string arrays + +```scriban +## Parties +{{ for party in parties }}- {{ party }} +{{ end }} +``` + +### Loops over object arrays + +```scriban +## Action Items +{{ for item in action_items }}- {{ item.task }}{{ if item.owner && item.owner != "" }} — **{{ item.owner }}**{{ end }} +{{ end }} +``` + +### Markdown tables from object arrays + +```scriban +| Date | Category | Amount | +|------|----------|--------| +{{ for row in line_items }}| {{ format_date row.date }} | {{ escape_pipes row.category }} | {{ format_money row.amount currency }} | +{{ end }} +``` + +Always call `escape_pipes` on string values inside table cells to prevent +Markdown table corruption from `|` characters in user data. + +--- + +## Format Helpers + +These functions are available in every template body. + +| Helper | Signature | Example | +|--------|-----------|---------| +| `format_date` | `(value) → string` | `{{ format_date start_date }}` → `2024-03-15` | +| `format_money` | `(amount, currency) → string` | `{{ format_money total "USD" }}` → `USD 1,500.00` | +| `format_money_whole` | `(amount, currency) → string` | `{{ format_money_whole fee "USD" }}` → `USD 1,500` | +| `format_number` | `(value) → string` | `{{ format_number count }}` → `1,234` | +| `format_percent` | `(rate) → string` | `{{ format_percent tax_rate }}` → `8.5%` | +| `escape_pipes` | `(value) → string` | `{{ escape_pipes description }}` — escapes `\|` in Markdown tables | +| `slug` | `(value, fallback?) → string` | `{{ slug client_name "client" }}` → `acme-corp` | +| `normalize_currency` | `(value) → string` | `{{ normalize_currency currency }}` → `USD` | + +`format_date` accepts ISO date strings (`2024-03-15`) and passes them through +unchanged if they don't parse. `format_money` / `format_money_whole` accept +numeric amounts (Decimal or Double from JSON). + +--- + +## Field Schema (`fields_json`) + +Every template declares its inputs as a JSON array of field objects stored in +the `fields_json` column. The agent uses this to collect missing data before +calling the template. + +### Field object shape + +```json +{ + "name": "client_name", + "type": "string", + "required": true, + "description": "Full legal name of the client." +} +``` + +### Field types + +| `type` value | JSON shape expected | Notes | +|---|---|---| +| `string` | `"value"` | Single-line text | +| `text` | `"value"` | Multi-line / paragraph | +| `integer` | `42` | Whole number | +| `decimal` | `3.14` or `"3.14"` | Fractional number | +| `currency` | `1500.00` or `"1500.00"` | Monetary amount (use `format_money` in body) | +| `date` | `"2024-03-15"` | ISO date string | +| `boolean` | `true` / `false` | | +| `stringArray` | `["a", "b"]` | List of strings | +| `objectArray` | `[{...}, {...}]` | List of objects; requires `itemFields` | + +### `objectArray` with nested fields + +```json +{ + "name": "line_items", + "type": "objectArray", + "required": true, + "description": "Expense line items.", + "itemFields": [ + { "name": "date", "type": "date", "required": true }, + { "name": "description", "type": "string", "required": true }, + { "name": "amount", "type": "currency", "required": true }, + { "name": "receipt", "type": "string", "required": false } + ] +} +``` + +--- + +## `filename_template` and `default_format` + +### `default_format` + +One of: `Word`, `StructuredPdf`, `Excel`, `Markdown`, `Pdf`, `PowerPoint`. + +Most Markdown-body templates use `Word` (rendered to DOCX via MigraDoc). + +### `filename_template` + +A Scriban expression producing the output filename. The same format helpers +and field variables are available. + +``` +nda-{{ slug client_name "client" }}-{{ slug counterparty_name "counterparty" }}-{{ format_date effective_date }}.docx +``` + +If omitted, Sovrant falls back to `.`. + +--- + +## Adding a New Template via Migration + +Create `src/Sovrant.Runtime/Storage/Migrations/V0NN__.sql`: + +```sql +-- V0NN: Add