-- Fixed-scope account roles, audit attribution, and reversible task archive. -- Existing accounts remain administrators; no organization/tenant UI is added. ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check; ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'user')); ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password boolean NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS password_changed_at timestamptz NOT NULL DEFAULT now(); -- User-supplied idempotency keys are account-scoped. System/AgentBus keys -- remain deployment-scoped through the NULL actor partial index. ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_organization_id_idempotency_key_key; ALTER TABLE idempotency_keys DROP CONSTRAINT IF EXISTS idempotency_keys_organization_id_scope_idempotency_key_key; ALTER TABLE idempotency_keys ADD COLUMN IF NOT EXISTS actor_user_id uuid REFERENCES users(id); UPDATE idempotency_keys i SET actor_user_id = t.created_by FROM tasks t WHERE i.task_id = t.id AND i.actor_user_id IS NULL AND t.source = 'manual'; CREATE UNIQUE INDEX IF NOT EXISTS idempotency_keys_actor_unique_idx ON idempotency_keys (organization_id, scope, idempotency_key, actor_user_id) WHERE actor_user_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idempotency_keys_system_unique_idx ON idempotency_keys (organization_id, scope, idempotency_key) WHERE actor_user_id IS NULL; ALTER TABLE agent_session_messages ADD COLUMN IF NOT EXISTS actor_user_id uuid REFERENCES users(id), ADD COLUMN IF NOT EXISTS input_source text; ALTER TABLE agent_session_messages DROP CONSTRAINT IF EXISTS agent_session_messages_input_source_check; ALTER TABLE agent_session_messages ADD CONSTRAINT agent_session_messages_input_source_check CHECK (input_source IS NULL OR input_source IN ('manual', 'agentbus', 'reparse', 'system')); -- Only the initial input has a provable historical platform actor. Do not -- invent actors for later turns that predate explicit attribution. UPDATE agent_session_messages m SET actor_user_id = t.created_by, input_source = CASE WHEN t.source = 'agentbus' THEN 'agentbus' ELSE 'manual' END FROM tasks t WHERE m.task_id = t.id AND m.role = 'user' AND m.turn_no = 1 AND (m.actor_user_id IS NULL OR m.input_source IS NULL); ALTER TABLE task_input_attachments ADD COLUMN IF NOT EXISTS created_by uuid REFERENCES users(id); UPDATE task_input_attachments a SET created_by = t.created_by FROM tasks t WHERE a.task_id = t.id AND a.created_by IS NULL AND a.source = 'manual'; ALTER TABLE tasks ADD COLUMN IF NOT EXISTS archived_at timestamptz, ADD COLUMN IF NOT EXISTS archived_by uuid REFERENCES users(id), ADD COLUMN IF NOT EXISTS archive_reason text; ALTER TABLE tasks DROP CONSTRAINT IF EXISTS tasks_archive_reason_length_check; ALTER TABLE tasks ADD CONSTRAINT tasks_archive_reason_length_check CHECK (archive_reason IS NULL OR char_length(archive_reason) <= 500); CREATE INDEX IF NOT EXISTS users_org_role_active_idx ON users (organization_id, role, is_active, username); CREATE INDEX IF NOT EXISTS tasks_owner_visible_created_idx ON tasks (organization_id, created_by, created_at DESC, id DESC) WHERE archived_at IS NULL; CREATE INDEX IF NOT EXISTS tasks_archive_created_idx ON tasks (organization_id, archived_at DESC, created_at DESC, id DESC) WHERE archived_at IS NOT NULL; CREATE INDEX IF NOT EXISTS agent_session_messages_actor_idx ON agent_session_messages (organization_id, actor_user_id, created_at DESC) WHERE actor_user_id IS NOT NULL; CREATE INDEX IF NOT EXISTS audit_events_actor_created_idx ON audit_events (organization_id, actor_user_id, created_at DESC);