File size: 5,549 Bytes
3493993
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
-- 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;