@consiliency/agent-board-schema 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +55 -0
- package/bin/agent-board-schema.js +97 -0
- package/fixtures/already-applied.json +181 -0
- package/fixtures/divergent-history.json +173 -0
- package/fixtures/production-history.json +173 -0
- package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
- package/migrations/20260502000000_agent_board_schema.sql +180 -0
- package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
- package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
- package/migrations/20260504000000_agent_board_read_models.sql +206 -0
- package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
- package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
- package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
- package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
- package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
- package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
- package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
- package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
- package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
- package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
- package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
- package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
- package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
- package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
- package/migrations/20260513000001_agent_board_retention.sql +160 -0
- package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
- package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
- package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
- package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
- package/migrations/20260517000000_agent_board_addressing.sql +542 -0
- package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
- package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
- package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
- package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
- package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
- package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
- package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
- package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
- package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
- package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
- package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
- package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
- package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
- package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
- package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
- package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
- package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
- package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
- package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
- package/package.json +42 -0
- package/provenance.json +580 -0
- package/schema-manifest.json +458 -0
- package/src/check.js +23 -0
- package/src/index.js +50 -0
- package/src/materialize.js +73 -0
- package/src/plan.js +44 -0
- package/src/tooling.test.mjs +97 -0
- package/tests/agent_board_addressing.test.sql +166 -0
- package/tests/agent_board_auth_rls.test.sql +243 -0
- package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
- package/tests/agent_board_auth_rpcs.test.sql +311 -0
- package/tests/agent_board_auth_schema.test.sql +411 -0
- package/tests/agent_board_authbound_boundary.test.sql +148 -0
- package/tests/agent_board_boardopt.test.sql +170 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
- package/tests/agent_board_callback_runtime.test.sql +425 -0
- package/tests/agent_board_callbackcap.test.sql +222 -0
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
- package/tests/agent_board_dblint_rpcs.test.sql +247 -0
- package/tests/agent_board_fleet_policy.test.sql +370 -0
- package/tests/agent_board_fleet_read_models.test.sql +158 -0
- package/tests/agent_board_fleet_registry.test.sql +149 -0
- package/tests/agent_board_inbox_reads.test.sql +471 -0
- package/tests/agent_board_inbox_rpcs.test.sql +452 -0
- package/tests/agent_board_inbox_schema.test.sql +355 -0
- package/tests/agent_board_nonfleet_identity.test.sql +26 -0
- package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
- package/tests/agent_board_schema_version.test.sql +24 -0
- package/tests/agent_board_schema_version_rpc.test.sql +40 -0
- package/tests/agent_board_signature_rls_reads.test.sql +133 -0
- package/tests/agent_board_signed_write_gateway.test.sql +219 -0
- package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
- package/tests/agent_board_signing_store.test.sql +183 -0
- package/tests/agent_board_smoke.test.sql +12 -0
- package/tests/agent_board_v1_invariants.test.sql +298 -0
- package/tests/agent_board_v2_invariants.test.sql +753 -0
- package/tests/agent_board_workload_providers.test.sql +15 -0
- package/tests/fixtures/adopter-base.sql +19 -0
- package/tests/fixtures/operator-hardening.sql +27 -0
- package/tests/fixtures/test-prereqs.sql +84 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
create or replace function agent_board._next_task_event_sequence(p_task_id uuid)
|
|
2
|
+
returns bigint
|
|
3
|
+
language plpgsql
|
|
4
|
+
security definer
|
|
5
|
+
set search_path = agent_board, public
|
|
6
|
+
as $$
|
|
7
|
+
declare
|
|
8
|
+
v_sequence bigint;
|
|
9
|
+
begin
|
|
10
|
+
perform 1
|
|
11
|
+
from agent_board.tasks t
|
|
12
|
+
where t.id = p_task_id
|
|
13
|
+
for update;
|
|
14
|
+
|
|
15
|
+
select coalesce(max(te.event_sequence), 0) + 1
|
|
16
|
+
into v_sequence
|
|
17
|
+
from agent_board.task_events te
|
|
18
|
+
where te.task_id = p_task_id;
|
|
19
|
+
|
|
20
|
+
return v_sequence;
|
|
21
|
+
end;
|
|
22
|
+
$$;
|
|
23
|
+
|
|
24
|
+
create or replace function agent_board.heartbeat(
|
|
25
|
+
p_agent_id uuid,
|
|
26
|
+
p_thread_id uuid default null,
|
|
27
|
+
p_availability text default 'ready',
|
|
28
|
+
p_activity_summary text default null,
|
|
29
|
+
p_expires_in interval default interval '5 minutes',
|
|
30
|
+
p_metadata jsonb default '{}'::jsonb
|
|
31
|
+
)
|
|
32
|
+
returns table(agent_id uuid, availability text, thread_id uuid, last_heartbeat_at timestamptz, expires_at timestamptz, updated_at timestamptz)
|
|
33
|
+
language plpgsql
|
|
34
|
+
security definer
|
|
35
|
+
set search_path = agent_board, public
|
|
36
|
+
as $$
|
|
37
|
+
begin
|
|
38
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
39
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
40
|
+
end if;
|
|
41
|
+
if p_thread_id is not null and not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
42
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
43
|
+
end if;
|
|
44
|
+
|
|
45
|
+
return query
|
|
46
|
+
insert into agent_board.presence as p(agent_id, thread_id, availability, activity_summary, last_heartbeat_at, expires_at, metadata, updated_at)
|
|
47
|
+
values (p_agent_id, p_thread_id, coalesce(p_availability, 'ready'), p_activity_summary, now(), now() + coalesce(p_expires_in, interval '5 minutes'), coalesce(p_metadata, '{}'::jsonb), now())
|
|
48
|
+
on conflict on constraint presence_pkey do update
|
|
49
|
+
set thread_id = excluded.thread_id,
|
|
50
|
+
availability = excluded.availability,
|
|
51
|
+
activity_summary = excluded.activity_summary,
|
|
52
|
+
last_heartbeat_at = excluded.last_heartbeat_at,
|
|
53
|
+
expires_at = excluded.expires_at,
|
|
54
|
+
metadata = excluded.metadata,
|
|
55
|
+
updated_at = excluded.updated_at
|
|
56
|
+
returning p.agent_id, p.availability, p.thread_id, p.last_heartbeat_at, p.expires_at, p.updated_at;
|
|
57
|
+
end;
|
|
58
|
+
$$;
|
|
59
|
+
|
|
60
|
+
create or replace function agent_board.mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)
|
|
61
|
+
returns table(thread_id uuid, agent_id uuid, last_read_message_sequence bigint, last_read_at timestamptz)
|
|
62
|
+
language plpgsql
|
|
63
|
+
security definer
|
|
64
|
+
set search_path = agent_board, public
|
|
65
|
+
as $$
|
|
66
|
+
declare
|
|
67
|
+
v_sequence bigint;
|
|
68
|
+
begin
|
|
69
|
+
if p_last_read_message_sequence < 0 then
|
|
70
|
+
raise exception 'negative read sequence' using errcode = '22023';
|
|
71
|
+
end if;
|
|
72
|
+
|
|
73
|
+
select least(p_last_read_message_sequence, t.last_message_sequence)
|
|
74
|
+
into v_sequence
|
|
75
|
+
from agent_board.threads t
|
|
76
|
+
where t.id = p_thread_id;
|
|
77
|
+
|
|
78
|
+
if v_sequence is null then
|
|
79
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
80
|
+
end if;
|
|
81
|
+
|
|
82
|
+
return query
|
|
83
|
+
insert into agent_board.read_receipts as rr(thread_id, agent_id, last_read_message_sequence, last_read_at)
|
|
84
|
+
values (p_thread_id, p_agent_id, v_sequence, now())
|
|
85
|
+
on conflict on constraint read_receipts_pkey do update
|
|
86
|
+
set last_read_message_sequence = greatest(rr.last_read_message_sequence, excluded.last_read_message_sequence),
|
|
87
|
+
last_read_at = now()
|
|
88
|
+
returning rr.thread_id, rr.agent_id, rr.last_read_message_sequence, rr.last_read_at;
|
|
89
|
+
end;
|
|
90
|
+
$$;
|
|
91
|
+
|
|
92
|
+
create or replace function agent_board.resolve_conflict(
|
|
93
|
+
p_conflict_id uuid,
|
|
94
|
+
p_resolved_by_agent_id uuid,
|
|
95
|
+
p_details jsonb default '{}'::jsonb
|
|
96
|
+
)
|
|
97
|
+
returns table(conflict_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
|
|
98
|
+
language plpgsql
|
|
99
|
+
security definer
|
|
100
|
+
set search_path = agent_board, public
|
|
101
|
+
as $$
|
|
102
|
+
declare
|
|
103
|
+
v_conflict agent_board.conflicts%rowtype;
|
|
104
|
+
v_event record;
|
|
105
|
+
begin
|
|
106
|
+
select *
|
|
107
|
+
into v_conflict
|
|
108
|
+
from agent_board.conflicts c
|
|
109
|
+
where c.id = p_conflict_id
|
|
110
|
+
and c.status <> 'resolved';
|
|
111
|
+
if v_conflict.id is null then
|
|
112
|
+
raise exception 'conflict not found' using errcode = 'P0002';
|
|
113
|
+
end if;
|
|
114
|
+
|
|
115
|
+
update agent_board.conflicts c
|
|
116
|
+
set status = 'resolved',
|
|
117
|
+
resolved_by_agent_id = p_resolved_by_agent_id,
|
|
118
|
+
resolved_at = now(),
|
|
119
|
+
details = c.details || coalesce(p_details, '{}'::jsonb),
|
|
120
|
+
updated_at = now()
|
|
121
|
+
where c.id = p_conflict_id
|
|
122
|
+
returning * into v_conflict;
|
|
123
|
+
|
|
124
|
+
if v_conflict.task_id is not null then
|
|
125
|
+
select * into v_event
|
|
126
|
+
from agent_board.append_task_event(v_conflict.task_id, p_resolved_by_agent_id, 'status_changed', jsonb_build_object('kind', 'conflict_resolved', 'conflict_id', p_conflict_id));
|
|
127
|
+
end if;
|
|
128
|
+
|
|
129
|
+
return query
|
|
130
|
+
select v_conflict.id, v_conflict.status, v_conflict.resolved_at, v_conflict.updated_at, v_event.event_sequence;
|
|
131
|
+
end;
|
|
132
|
+
$$;
|
|
133
|
+
|
|
134
|
+
create or replace function agent_board.resolve_escalation(
|
|
135
|
+
p_escalation_id uuid,
|
|
136
|
+
p_resolved_by_agent_id uuid,
|
|
137
|
+
p_details jsonb default '{}'::jsonb
|
|
138
|
+
)
|
|
139
|
+
returns table(escalation_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
|
|
140
|
+
language plpgsql
|
|
141
|
+
security definer
|
|
142
|
+
set search_path = agent_board, public
|
|
143
|
+
as $$
|
|
144
|
+
declare
|
|
145
|
+
v_escalation agent_board.escalations%rowtype;
|
|
146
|
+
v_event record;
|
|
147
|
+
begin
|
|
148
|
+
select *
|
|
149
|
+
into v_escalation
|
|
150
|
+
from agent_board.escalations e
|
|
151
|
+
where e.id = p_escalation_id
|
|
152
|
+
and e.status <> 'resolved';
|
|
153
|
+
if v_escalation.id is null then
|
|
154
|
+
raise exception 'escalation not found' using errcode = 'P0002';
|
|
155
|
+
end if;
|
|
156
|
+
|
|
157
|
+
update agent_board.escalations e
|
|
158
|
+
set status = 'resolved',
|
|
159
|
+
resolved_by_agent_id = p_resolved_by_agent_id,
|
|
160
|
+
resolved_at = now(),
|
|
161
|
+
details = e.details || coalesce(p_details, '{}'::jsonb),
|
|
162
|
+
updated_at = now()
|
|
163
|
+
where e.id = p_escalation_id
|
|
164
|
+
returning * into v_escalation;
|
|
165
|
+
|
|
166
|
+
if v_escalation.task_id is not null then
|
|
167
|
+
select * into v_event
|
|
168
|
+
from agent_board.append_task_event(v_escalation.task_id, p_resolved_by_agent_id, 'status_changed', jsonb_build_object('kind', 'escalation_resolved', 'escalation_id', p_escalation_id));
|
|
169
|
+
end if;
|
|
170
|
+
|
|
171
|
+
return query
|
|
172
|
+
select v_escalation.id, v_escalation.status, v_escalation.resolved_at, v_escalation.updated_at, v_event.event_sequence;
|
|
173
|
+
end;
|
|
174
|
+
$$;
|
|
175
|
+
|
|
176
|
+
create or replace function agent_board.create_tasking_delivery(
|
|
177
|
+
p_thread_id uuid,
|
|
178
|
+
p_created_by_agent_id uuid,
|
|
179
|
+
p_title text,
|
|
180
|
+
p_details text default null,
|
|
181
|
+
p_recipient_agent_ids uuid[] default null,
|
|
182
|
+
p_board_id uuid default null,
|
|
183
|
+
p_priority text default 'medium',
|
|
184
|
+
p_orchestration_mode text default 'distributed',
|
|
185
|
+
p_idempotency_key text default null,
|
|
186
|
+
p_metadata jsonb default '{}'::jsonb
|
|
187
|
+
)
|
|
188
|
+
returns table(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz, updated_at timestamptz)
|
|
189
|
+
language plpgsql
|
|
190
|
+
security definer
|
|
191
|
+
set search_path = agent_board, public
|
|
192
|
+
as $$
|
|
193
|
+
declare
|
|
194
|
+
v_task agent_board.tasks%rowtype;
|
|
195
|
+
v_created_event_id uuid;
|
|
196
|
+
v_created_task record;
|
|
197
|
+
v_recipient_ids uuid[];
|
|
198
|
+
v_recipient_id uuid;
|
|
199
|
+
v_membership record;
|
|
200
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
201
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
202
|
+
v_delivered_count integer := 0;
|
|
203
|
+
v_inbox_count integer := 0;
|
|
204
|
+
v_callback_count integer := 0;
|
|
205
|
+
v_metadata jsonb;
|
|
206
|
+
v_preview text;
|
|
207
|
+
v_fetch_descriptor jsonb;
|
|
208
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
209
|
+
begin
|
|
210
|
+
if nullif(trim(p_title), '') is null then
|
|
211
|
+
raise exception 'task title is required' using errcode = '22023';
|
|
212
|
+
end if;
|
|
213
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
214
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
215
|
+
end if;
|
|
216
|
+
if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
|
|
217
|
+
raise exception 'invalid orchestration mode' using errcode = '22023';
|
|
218
|
+
end if;
|
|
219
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
220
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
221
|
+
end if;
|
|
222
|
+
if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
|
|
223
|
+
raise exception 'creator agent not found' using errcode = 'P0002';
|
|
224
|
+
end if;
|
|
225
|
+
if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
|
|
226
|
+
raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
|
|
227
|
+
end if;
|
|
228
|
+
if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
|
|
229
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
230
|
+
end if;
|
|
231
|
+
if p_board_id is not null and not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
232
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
233
|
+
end if;
|
|
234
|
+
|
|
235
|
+
select array_agg(distinct recipient_id)
|
|
236
|
+
into v_recipient_ids
|
|
237
|
+
from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
|
|
238
|
+
|
|
239
|
+
if v_recipient_ids is not null and (
|
|
240
|
+
select count(*)
|
|
241
|
+
from agent_board.agents
|
|
242
|
+
where id = any(v_recipient_ids)
|
|
243
|
+
) <> cardinality(v_recipient_ids) then
|
|
244
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
245
|
+
end if;
|
|
246
|
+
|
|
247
|
+
perform agent_board._lock_idempotency_key(
|
|
248
|
+
'create_tasking_delivery:' || p_thread_id::text || ':' || p_created_by_agent_id::text,
|
|
249
|
+
v_idempotency_key
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if v_idempotency_key is not null then
|
|
253
|
+
select *
|
|
254
|
+
into v_task
|
|
255
|
+
from agent_board.tasks
|
|
256
|
+
where thread_id = p_thread_id
|
|
257
|
+
and created_by_agent_id = p_created_by_agent_id
|
|
258
|
+
and metadata ->> 'rpcName' = 'create_tasking_delivery'
|
|
259
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
260
|
+
order by created_at desc
|
|
261
|
+
limit 1;
|
|
262
|
+
end if;
|
|
263
|
+
|
|
264
|
+
if v_task.id is null then
|
|
265
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
266
|
+
|| jsonb_build_object(
|
|
267
|
+
'rpcName', 'create_tasking_delivery',
|
|
268
|
+
'priority', p_priority
|
|
269
|
+
);
|
|
270
|
+
if p_board_id is not null then
|
|
271
|
+
v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
|
|
272
|
+
end if;
|
|
273
|
+
if v_idempotency_key is not null then
|
|
274
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
275
|
+
end if;
|
|
276
|
+
|
|
277
|
+
select *
|
|
278
|
+
into v_created_task
|
|
279
|
+
from agent_board.create_task(
|
|
280
|
+
p_thread_id,
|
|
281
|
+
p_created_by_agent_id,
|
|
282
|
+
trim(p_title),
|
|
283
|
+
p_details,
|
|
284
|
+
null,
|
|
285
|
+
null,
|
|
286
|
+
p_priority,
|
|
287
|
+
p_orchestration_mode,
|
|
288
|
+
v_metadata
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
v_created_event_id := v_created_task.created_event_id;
|
|
292
|
+
|
|
293
|
+
select *
|
|
294
|
+
into v_task
|
|
295
|
+
from agent_board.tasks
|
|
296
|
+
where id = v_created_task.task_id;
|
|
297
|
+
else
|
|
298
|
+
select te.id
|
|
299
|
+
into v_created_event_id
|
|
300
|
+
from agent_board.task_events te
|
|
301
|
+
where te.task_id = v_task.id
|
|
302
|
+
and te.event_sequence = 1;
|
|
303
|
+
end if;
|
|
304
|
+
|
|
305
|
+
v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
|
|
306
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
307
|
+
'task',
|
|
308
|
+
v_task.id,
|
|
309
|
+
'agent_board.v_task_tree',
|
|
310
|
+
'tasking delivery',
|
|
311
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if v_recipient_ids is not null then
|
|
315
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
316
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
317
|
+
'tasking',
|
|
318
|
+
v_recipient_id,
|
|
319
|
+
'task',
|
|
320
|
+
null,
|
|
321
|
+
v_task.id,
|
|
322
|
+
null,
|
|
323
|
+
null,
|
|
324
|
+
null,
|
|
325
|
+
jsonb_build_object('priority', p_priority)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
329
|
+
v_recipient.id,
|
|
330
|
+
v_recipient_id,
|
|
331
|
+
null,
|
|
332
|
+
v_recipient.delivery_mode,
|
|
333
|
+
'task',
|
|
334
|
+
null,
|
|
335
|
+
v_task.id,
|
|
336
|
+
null,
|
|
337
|
+
trim(p_title),
|
|
338
|
+
v_preview,
|
|
339
|
+
p_priority,
|
|
340
|
+
true,
|
|
341
|
+
false,
|
|
342
|
+
v_fetch_descriptor,
|
|
343
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
v_delivered_count := v_delivered_count + 1;
|
|
347
|
+
v_inbox_count := v_inbox_count + 1;
|
|
348
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
349
|
+
end loop;
|
|
350
|
+
end if;
|
|
351
|
+
|
|
352
|
+
if p_board_id is not null then
|
|
353
|
+
for v_membership in
|
|
354
|
+
select bm.id, bm.agent_id
|
|
355
|
+
from agent_board.board_memberships bm
|
|
356
|
+
where bm.board_id = p_board_id
|
|
357
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
358
|
+
order by bm.created_at
|
|
359
|
+
loop
|
|
360
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
361
|
+
'tasking',
|
|
362
|
+
v_membership.agent_id,
|
|
363
|
+
'task',
|
|
364
|
+
null,
|
|
365
|
+
v_task.id,
|
|
366
|
+
null,
|
|
367
|
+
p_board_id,
|
|
368
|
+
v_membership.id,
|
|
369
|
+
jsonb_build_object('priority', p_priority)
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
373
|
+
v_recipient.id,
|
|
374
|
+
v_membership.agent_id,
|
|
375
|
+
v_recipient.board_id,
|
|
376
|
+
v_recipient.delivery_mode,
|
|
377
|
+
'task',
|
|
378
|
+
null,
|
|
379
|
+
v_task.id,
|
|
380
|
+
null,
|
|
381
|
+
trim(p_title),
|
|
382
|
+
v_preview,
|
|
383
|
+
p_priority,
|
|
384
|
+
true,
|
|
385
|
+
v_recipient.board_id is not null,
|
|
386
|
+
v_fetch_descriptor,
|
|
387
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
v_delivered_count := v_delivered_count + 1;
|
|
391
|
+
v_inbox_count := v_inbox_count + 1;
|
|
392
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
393
|
+
end loop;
|
|
394
|
+
end if;
|
|
395
|
+
|
|
396
|
+
return query
|
|
397
|
+
select v_task.id, v_created_event_id, v_delivered_count, v_inbox_count, v_callback_count, v_task.created_at, v_task.updated_at;
|
|
398
|
+
end;
|
|
399
|
+
$$;
|
|
400
|
+
|
|
401
|
+
revoke all on function agent_board._next_task_event_sequence(uuid) from public, anon, authenticated;
|
|
402
|
+
revoke all on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) from public, anon, authenticated;
|
|
403
|
+
revoke all on function agent_board.mark_read(uuid, uuid, bigint) from public, anon, authenticated;
|
|
404
|
+
revoke all on function agent_board.resolve_conflict(uuid, uuid, jsonb) from public, anon, authenticated;
|
|
405
|
+
revoke all on function agent_board.resolve_escalation(uuid, uuid, jsonb) from public, anon, authenticated;
|
|
406
|
+
revoke all on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from public, anon, authenticated;
|
|
407
|
+
|
|
408
|
+
grant execute on function agent_board._next_task_event_sequence(uuid) to service_role;
|
|
409
|
+
grant execute on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) to service_role;
|
|
410
|
+
grant execute on function agent_board.mark_read(uuid, uuid, bigint) to service_role;
|
|
411
|
+
grant execute on function agent_board.resolve_conflict(uuid, uuid, jsonb) to service_role;
|
|
412
|
+
grant execute on function agent_board.resolve_escalation(uuid, uuid, jsonb) to service_role;
|
|
413
|
+
grant execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) to service_role;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
do $$
|
|
2
|
+
begin
|
|
3
|
+
if not exists (
|
|
4
|
+
select 1
|
|
5
|
+
from pg_type t
|
|
6
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
7
|
+
where n.nspname = 'agent_board'
|
|
8
|
+
and t.typname = 'agent_kind'
|
|
9
|
+
) then
|
|
10
|
+
create type agent_board.agent_kind as enum ('human', 'agent', 'system');
|
|
11
|
+
end if;
|
|
12
|
+
end
|
|
13
|
+
$$;
|
|
14
|
+
|
|
15
|
+
alter table agent_board.agents
|
|
16
|
+
add column if not exists auth_user_id uuid references auth.users(id) on delete set null,
|
|
17
|
+
add column if not exists owner_user_id uuid references auth.users(id) on delete set null,
|
|
18
|
+
add column if not exists kind agent_board.agent_kind;
|
|
19
|
+
|
|
20
|
+
update agent_board.agents
|
|
21
|
+
set kind = agent_type::agent_board.agent_kind
|
|
22
|
+
where kind is null;
|
|
23
|
+
|
|
24
|
+
create or replace function agent_board.sync_agent_principal_columns()
|
|
25
|
+
returns trigger
|
|
26
|
+
language plpgsql
|
|
27
|
+
set search_path = ''
|
|
28
|
+
as $$
|
|
29
|
+
begin
|
|
30
|
+
if new.kind is null
|
|
31
|
+
or (
|
|
32
|
+
tg_op = 'UPDATE'
|
|
33
|
+
and new.kind is not distinct from old.kind
|
|
34
|
+
and new.agent_type is distinct from old.agent_type
|
|
35
|
+
and new.agent_type is not null
|
|
36
|
+
) then
|
|
37
|
+
new.kind := new.agent_type::agent_board.agent_kind;
|
|
38
|
+
end if;
|
|
39
|
+
|
|
40
|
+
if new.agent_type is null
|
|
41
|
+
or (
|
|
42
|
+
tg_op = 'UPDATE'
|
|
43
|
+
and new.agent_type is not distinct from old.agent_type
|
|
44
|
+
and new.kind is distinct from old.kind
|
|
45
|
+
and new.kind is not null
|
|
46
|
+
) then
|
|
47
|
+
new.agent_type := new.kind::text;
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
if new.kind is null or new.agent_type is null then
|
|
51
|
+
raise exception 'kind and agent_type are both required' using errcode = '23502';
|
|
52
|
+
end if;
|
|
53
|
+
|
|
54
|
+
if new.kind::text <> new.agent_type then
|
|
55
|
+
raise exception 'kind and agent_type must match' using errcode = '22000';
|
|
56
|
+
end if;
|
|
57
|
+
|
|
58
|
+
if new.kind = 'system'::agent_board.agent_kind then
|
|
59
|
+
new.owner_user_id := null;
|
|
60
|
+
new.auth_user_id := null;
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
return new;
|
|
64
|
+
end;
|
|
65
|
+
$$;
|
|
66
|
+
|
|
67
|
+
drop trigger if exists trg_sync_agent_principal_columns on agent_board.agents;
|
|
68
|
+
|
|
69
|
+
create trigger trg_sync_agent_principal_columns
|
|
70
|
+
before insert or update on agent_board.agents
|
|
71
|
+
for each row execute function agent_board.sync_agent_principal_columns();
|
|
72
|
+
|
|
73
|
+
do $$
|
|
74
|
+
declare
|
|
75
|
+
v_provisioning_user_id uuid;
|
|
76
|
+
begin
|
|
77
|
+
select uor.user_id
|
|
78
|
+
into v_provisioning_user_id
|
|
79
|
+
from public.user_org_roles uor
|
|
80
|
+
where uor.role = 'platform_admin'
|
|
81
|
+
order by uor.granted_at, uor.user_id
|
|
82
|
+
limit 1;
|
|
83
|
+
|
|
84
|
+
if v_provisioning_user_id is not null then
|
|
85
|
+
update agent_board.agents
|
|
86
|
+
set owner_user_id = coalesce(owner_user_id, case when agent_type = 'system' then null else v_provisioning_user_id end),
|
|
87
|
+
auth_user_id = coalesce(auth_user_id, case when agent_type = 'human' then v_provisioning_user_id else null end),
|
|
88
|
+
kind = coalesce(kind, agent_type::agent_board.agent_kind),
|
|
89
|
+
updated_at = now()
|
|
90
|
+
where agent_type in ('human', 'agent', 'system');
|
|
91
|
+
end if;
|
|
92
|
+
|
|
93
|
+
update agent_board.agents
|
|
94
|
+
set owner_user_id = null,
|
|
95
|
+
auth_user_id = null,
|
|
96
|
+
kind = 'system'::agent_board.agent_kind,
|
|
97
|
+
updated_at = now()
|
|
98
|
+
where agent_type = 'system'
|
|
99
|
+
and (
|
|
100
|
+
owner_user_id is not null
|
|
101
|
+
or auth_user_id is not null
|
|
102
|
+
or kind is distinct from 'system'::agent_board.agent_kind
|
|
103
|
+
);
|
|
104
|
+
end
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
alter table agent_board.agents
|
|
108
|
+
drop constraint if exists agents_kind_agent_type_match,
|
|
109
|
+
add constraint agents_kind_agent_type_match check (kind::text = agent_type),
|
|
110
|
+
drop constraint if exists agents_system_owner_shape,
|
|
111
|
+
add constraint agents_system_owner_shape check (
|
|
112
|
+
(kind = 'system'::agent_board.agent_kind and owner_user_id is null and auth_user_id is null)
|
|
113
|
+
or kind <> 'system'::agent_board.agent_kind
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
create index if not exists idx_agent_board_agents_owner_user_id
|
|
117
|
+
on agent_board.agents(owner_user_id)
|
|
118
|
+
where owner_user_id is not null;
|
|
119
|
+
|
|
120
|
+
create index if not exists idx_agent_board_agents_auth_user_id
|
|
121
|
+
on agent_board.agents(auth_user_id)
|
|
122
|
+
where auth_user_id is not null;
|
|
123
|
+
|
|
124
|
+
create unique index if not exists idx_agent_board_agents_auth_user_id_active_unique
|
|
125
|
+
on agent_board.agents(auth_user_id)
|
|
126
|
+
where auth_user_id is not null and status = 'active';
|
|
127
|
+
|
|
128
|
+
create or replace function agent_board.current_agent_id()
|
|
129
|
+
returns uuid
|
|
130
|
+
language sql
|
|
131
|
+
stable
|
|
132
|
+
security definer
|
|
133
|
+
set search_path = ''
|
|
134
|
+
as $$
|
|
135
|
+
select a.id
|
|
136
|
+
from agent_board.agents a
|
|
137
|
+
where a.auth_user_id = auth.uid()
|
|
138
|
+
and a.status = 'active'
|
|
139
|
+
limit 1;
|
|
140
|
+
$$;
|
|
141
|
+
|
|
142
|
+
revoke all on function agent_board.current_agent_id() from public, anon, authenticated;
|
|
143
|
+
grant execute on function agent_board.current_agent_id() to authenticated;
|
|
144
|
+
grant execute on function agent_board.current_agent_id() to service_role;
|
|
145
|
+
|
|
146
|
+
comment on function agent_board.current_agent_id() is 'Resolves auth.uid() to the active agent_board.agents.id or returns null.';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
create table if not exists agent_board.agent_credentials (
|
|
2
|
+
id uuid primary key default gen_random_uuid(),
|
|
3
|
+
agent_id uuid not null references agent_board.agents(id) on delete cascade,
|
|
4
|
+
refresh_token_hash bytea not null,
|
|
5
|
+
hash_version integer not null check (hash_version > 0),
|
|
6
|
+
credential_label text not null,
|
|
7
|
+
expires_at timestamptz not null,
|
|
8
|
+
last_used_at timestamptz,
|
|
9
|
+
revoked_at timestamptz,
|
|
10
|
+
rotated_at timestamptz,
|
|
11
|
+
replaced_by_credential_id uuid references agent_board.agent_credentials(id) on delete set null,
|
|
12
|
+
replaces_credential_id uuid references agent_board.agent_credentials(id) on delete set null,
|
|
13
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
14
|
+
created_at timestamptz not null default now(),
|
|
15
|
+
updated_at timestamptz not null default now(),
|
|
16
|
+
constraint agent_credentials_not_self_replaced_by check (replaced_by_credential_id is null or replaced_by_credential_id <> id),
|
|
17
|
+
constraint agent_credentials_not_self_replaces check (replaces_credential_id is null or replaces_credential_id <> id),
|
|
18
|
+
constraint agent_credentials_revocation_rotation_shape check (
|
|
19
|
+
revoked_at is null
|
|
20
|
+
or rotated_at is null
|
|
21
|
+
or revoked_at <= rotated_at
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
comment on table agent_board.agent_credentials is 'Durable hashed refresh-token references plus lifecycle metadata for one agent principal.';
|
|
26
|
+
|
|
27
|
+
create index if not exists idx_agent_board_agent_credentials_agent
|
|
28
|
+
on agent_board.agent_credentials(agent_id, created_at desc);
|
|
29
|
+
|
|
30
|
+
create unique index if not exists idx_agent_board_agent_credentials_refresh_token_hash
|
|
31
|
+
on agent_board.agent_credentials(refresh_token_hash);
|
|
32
|
+
|
|
33
|
+
create index if not exists idx_agent_board_agent_credentials_active
|
|
34
|
+
on agent_board.agent_credentials(agent_id, expires_at, created_at desc)
|
|
35
|
+
where revoked_at is null;
|
|
36
|
+
|
|
37
|
+
insert into agent_board.schema_version (version)
|
|
38
|
+
values (2)
|
|
39
|
+
on conflict ((true)) do update set version = excluded.version;
|