Spaces:
Sleeping
Sleeping
| -- 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; | |