-- AI Copilot: bounded durable plans and action results. -- Apply after projects/0004_ai_studio.sql. Production migration remains explicit. begin; create table if not exists copilot_runs ( id text primary key default gen_random_uuid()::text, workspace_id text not null references workspaces(id) on delete restrict, user_id text not null references users(id) on delete restrict, project_id text references projects(id) on delete restrict, idempotency_key text not null check (char_length(idempotency_key) between 1 and 255), request_fingerprint text not null check (char_length(request_fingerprint) = 64), request_text text not null check (char_length(request_text) between 1 and 4000), context jsonb not null check (jsonb_typeof(context) = 'object'), plan jsonb not null check (jsonb_typeof(plan) = 'object'), status text not null check ( status in ('plan_ready','blocked','executing','completed','partial','failed','cancelled') ), current_action_id text, results jsonb not null default '[]'::jsonb check (jsonb_typeof(results) = 'array'), summary text, error_code text, error_message text, confirmed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), completed_at timestamptz, constraint uq_copilot_run_workspace_idempotency unique (workspace_id, idempotency_key) ); create index if not exists ix_copilot_runs_workspace_created on copilot_runs(workspace_id, created_at desc); create index if not exists ix_copilot_runs_workspace_status on copilot_runs(workspace_id, status); create index if not exists ix_copilot_runs_project_created on copilot_runs(project_id, created_at desc); drop trigger if exists mediarouter_copilot_touch_updated_at on copilot_runs; create trigger mediarouter_copilot_touch_updated_at before update on copilot_runs for each row execute function mediarouter_tenant_touch_updated_at(); create or replace function mediarouter_assert_copilot_run_ownership() returns trigger language plpgsql as $$ declare project_workspace text; begin if tg_op = 'UPDATE' and ( new.workspace_id is distinct from old.workspace_id or new.user_id is distinct from old.user_id or new.project_id is distinct from old.project_id or new.idempotency_key is distinct from old.idempotency_key or new.request_fingerprint is distinct from old.request_fingerprint or new.request_text is distinct from old.request_text or new.context is distinct from old.context or new.plan is distinct from old.plan or new.created_at is distinct from old.created_at ) then raise exception 'copilot run identity fields are immutable' using errcode = '23514'; end if; if new.context->>'workspace_id' is distinct from new.workspace_id then raise exception 'copilot context workspace does not match its run' using errcode = '23514'; end if; if new.project_id is not null then select workspace_id into project_workspace from projects where id = new.project_id; if project_workspace is null or project_workspace is distinct from new.workspace_id then raise exception 'copilot project must belong to its workspace' using errcode = '23503'; end if; end if; if not exists ( select 1 from workspace_memberships m where m.workspace_id = new.workspace_id and m.user_id = new.user_id and m.status = 'active' ) then raise exception 'copilot run requires active workspace membership' using errcode = '23503'; end if; if new.status in ('completed','partial','failed','cancelled') and new.completed_at is null then raise exception 'terminal copilot runs require completed_at' using errcode = '23514'; end if; return new; end; $$; drop trigger if exists mediarouter_copilot_run_ownership on copilot_runs; create trigger mediarouter_copilot_run_ownership before insert or update on copilot_runs for each row execute function mediarouter_assert_copilot_run_ownership(); alter table copilot_runs enable row level security; alter table copilot_runs force row level security; drop policy if exists copilot_runs_select on copilot_runs; create policy copilot_runs_select on copilot_runs for select using ( workspace_id = current_setting('app.workspace_id', true) and exists ( select 1 from workspace_memberships m where m.workspace_id = copilot_runs.workspace_id and m.user_id = current_setting('app.user_id', true) and m.status = 'active' ) ); drop policy if exists copilot_runs_insert on copilot_runs; create policy copilot_runs_insert on copilot_runs for insert with check ( workspace_id = current_setting('app.workspace_id', true) and user_id = current_setting('app.user_id', true) and exists ( select 1 from workspace_memberships m where m.workspace_id = copilot_runs.workspace_id and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer' ) ); drop policy if exists copilot_runs_update on copilot_runs; create policy copilot_runs_update on copilot_runs for update using ( workspace_id = current_setting('app.workspace_id', true) and user_id = current_setting('app.user_id', true) and exists ( select 1 from workspace_memberships m where m.workspace_id = copilot_runs.workspace_id and m.user_id = current_setting('app.user_id', true) and m.status = 'active' and m.role <> 'viewer' ) ) with check ( workspace_id = current_setting('app.workspace_id', true) and user_id = current_setting('app.user_id', true) ); commit;