@consiliency/agent-board-schema 1.3.0 → 1.4.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/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +104 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/package.json +9 -9
- package/schema-manifest.json +238 -4
- package/src/tooling.test.mjs +42 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +119 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +6 -6
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +159 -0
- package/tests/agent_board_send_role_gate.test.sql +177 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-1 (Lane A): send-path board-role gate + service_role bypass telemetry.
|
|
2
|
+
--
|
|
3
|
+
-- Forward migration. Re-declares the four core send RPCs (bodies forward-copied
|
|
4
|
+
-- verbatim from 20260512000002 except the enumerated additions) to:
|
|
5
|
+
-- (i) add the board-role observer-denied gate to the board-target sends via
|
|
6
|
+
-- agent_board._current_agent_can_send_to_board (post_board_message always;
|
|
7
|
+
-- create_tasking_delivery only when a board target is present), and
|
|
8
|
+
-- (ii) record a service_role_bypass telemetry row (with the frozen ef|host|unknown
|
|
9
|
+
-- origin discriminator, resolved from p_metadata -> 'signed_write' ->> 'origin')
|
|
10
|
+
-- on every _is_service_role() send.
|
|
11
|
+
--
|
|
12
|
+
-- The restricted-thread ACL consult already fires via MBAUTHZ's
|
|
13
|
+
-- _current_agent_can_access_thread and is NOT re-implemented here. Dormancy is
|
|
14
|
+
-- preserved: all four cores stay revoked from authenticated (re-asserted below);
|
|
15
|
+
-- the role gate bites only once MBSESSION re-grants sends to authenticated.
|
|
16
|
+
|
|
17
|
+
-- 1. _current_agent_can_send_to_board (IF-0-MBSENDAUTHZ-1) -------------------
|
|
18
|
+
-- Mirrors _current_agent_has_board_access but excludes the observer role: the only
|
|
19
|
+
-- net-new send-path authz predicate.
|
|
20
|
+
create or replace function agent_board._current_agent_can_send_to_board(
|
|
21
|
+
p_board_id uuid
|
|
22
|
+
)
|
|
23
|
+
returns boolean
|
|
24
|
+
language sql
|
|
25
|
+
stable
|
|
26
|
+
security definer
|
|
27
|
+
set search_path = ''
|
|
28
|
+
as $$
|
|
29
|
+
select
|
|
30
|
+
agent_board._is_service_role()
|
|
31
|
+
or exists (
|
|
32
|
+
select 1
|
|
33
|
+
from agent_board.board_memberships bm
|
|
34
|
+
where bm.board_id = p_board_id
|
|
35
|
+
and bm.agent_id = agent_board.current_agent_id()
|
|
36
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
37
|
+
and bm.role <> 'observer'
|
|
38
|
+
)
|
|
39
|
+
$$;
|
|
40
|
+
|
|
41
|
+
comment on function agent_board._current_agent_can_send_to_board(uuid) is
|
|
42
|
+
'True iff the current agent may send to the board: service_role, or an active board member whose role is not observer. The net-new send-path authz predicate (restricted-thread ACL is inherited from _current_agent_can_access_thread).';
|
|
43
|
+
|
|
44
|
+
revoke all on function agent_board._current_agent_can_send_to_board(uuid) from public, anon, authenticated;
|
|
45
|
+
grant execute on function agent_board._current_agent_can_send_to_board(uuid) to service_role;
|
|
46
|
+
|
|
47
|
+
-- 2. Re-declared core send RPCs (verbatim bodies + gate/telemetry additions) --
|
|
48
|
+
create or replace function agent_board.send_direct_message(
|
|
49
|
+
p_thread_id uuid,
|
|
50
|
+
p_author_agent_id uuid,
|
|
51
|
+
p_recipient_agent_id uuid,
|
|
52
|
+
p_subject text,
|
|
53
|
+
p_body text,
|
|
54
|
+
p_message_type text default 'comment',
|
|
55
|
+
p_priority text default 'medium',
|
|
56
|
+
p_idempotency_key text default null,
|
|
57
|
+
p_metadata jsonb default '{}'::jsonb
|
|
58
|
+
)
|
|
59
|
+
returns table(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamptz)
|
|
60
|
+
language plpgsql
|
|
61
|
+
security definer
|
|
62
|
+
set search_path = agent_board, public
|
|
63
|
+
as $$
|
|
64
|
+
declare
|
|
65
|
+
v_message agent_board.messages%rowtype;
|
|
66
|
+
v_created_message record;
|
|
67
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
68
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
69
|
+
v_callback_count integer := 0;
|
|
70
|
+
v_metadata jsonb;
|
|
71
|
+
v_preview text;
|
|
72
|
+
v_fetch_descriptor jsonb;
|
|
73
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
74
|
+
v_actor_agent_id uuid;
|
|
75
|
+
begin
|
|
76
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
77
|
+
|
|
78
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
79
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
80
|
+
end if;
|
|
81
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
82
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
83
|
+
end if;
|
|
84
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
85
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
86
|
+
end if;
|
|
87
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
88
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
89
|
+
end if;
|
|
90
|
+
if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
|
|
91
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
92
|
+
end if;
|
|
93
|
+
|
|
94
|
+
if agent_board._is_service_role() then
|
|
95
|
+
perform agent_board._record_send_authz_telemetry(
|
|
96
|
+
'service_role_bypass',
|
|
97
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
98
|
+
v_actor_agent_id,
|
|
99
|
+
null,
|
|
100
|
+
'send_direct_message',
|
|
101
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
102
|
+
null,
|
|
103
|
+
jsonb_build_object('redacted', true)
|
|
104
|
+
);
|
|
105
|
+
end if;
|
|
106
|
+
|
|
107
|
+
perform agent_board._lock_idempotency_key(
|
|
108
|
+
'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
109
|
+
v_idempotency_key
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if v_idempotency_key is not null then
|
|
113
|
+
select *
|
|
114
|
+
into v_message
|
|
115
|
+
from agent_board.messages
|
|
116
|
+
where thread_id = p_thread_id
|
|
117
|
+
and author_agent_id = v_actor_agent_id
|
|
118
|
+
and metadata ->> 'rpcName' = 'send_direct_message'
|
|
119
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
120
|
+
order by created_at desc
|
|
121
|
+
limit 1;
|
|
122
|
+
end if;
|
|
123
|
+
|
|
124
|
+
if v_message.id is null then
|
|
125
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
126
|
+
|| jsonb_build_object(
|
|
127
|
+
'rpcName', 'send_direct_message',
|
|
128
|
+
'subject', trim(p_subject),
|
|
129
|
+
'priority', p_priority
|
|
130
|
+
);
|
|
131
|
+
if v_idempotency_key is not null then
|
|
132
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
133
|
+
end if;
|
|
134
|
+
|
|
135
|
+
select *
|
|
136
|
+
into v_created_message
|
|
137
|
+
from agent_board.post_message(
|
|
138
|
+
p_thread_id,
|
|
139
|
+
v_actor_agent_id,
|
|
140
|
+
p_body,
|
|
141
|
+
p_message_type,
|
|
142
|
+
null,
|
|
143
|
+
v_metadata
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
select *
|
|
147
|
+
into v_message
|
|
148
|
+
from agent_board.messages
|
|
149
|
+
where id = v_created_message.message_id;
|
|
150
|
+
end if;
|
|
151
|
+
|
|
152
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
153
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
154
|
+
'thread',
|
|
155
|
+
p_thread_id,
|
|
156
|
+
'agent_board.v_thread_feed',
|
|
157
|
+
'direct delivery',
|
|
158
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
162
|
+
'direct',
|
|
163
|
+
p_recipient_agent_id,
|
|
164
|
+
'message',
|
|
165
|
+
v_message.id,
|
|
166
|
+
null,
|
|
167
|
+
null,
|
|
168
|
+
null,
|
|
169
|
+
null,
|
|
170
|
+
jsonb_build_object('priority', p_priority)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
174
|
+
v_recipient.id,
|
|
175
|
+
p_recipient_agent_id,
|
|
176
|
+
null,
|
|
177
|
+
v_recipient.delivery_mode,
|
|
178
|
+
'message',
|
|
179
|
+
v_message.id,
|
|
180
|
+
null,
|
|
181
|
+
null,
|
|
182
|
+
trim(p_subject),
|
|
183
|
+
v_preview,
|
|
184
|
+
p_priority,
|
|
185
|
+
true,
|
|
186
|
+
false,
|
|
187
|
+
v_fetch_descriptor,
|
|
188
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
192
|
+
|
|
193
|
+
return query
|
|
194
|
+
select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
|
|
195
|
+
end;
|
|
196
|
+
$$;
|
|
197
|
+
|
|
198
|
+
create or replace function agent_board.send_multicast_message(
|
|
199
|
+
p_thread_id uuid,
|
|
200
|
+
p_author_agent_id uuid,
|
|
201
|
+
p_recipient_agent_ids uuid[],
|
|
202
|
+
p_subject text,
|
|
203
|
+
p_body text,
|
|
204
|
+
p_message_type text default 'comment',
|
|
205
|
+
p_priority text default 'medium',
|
|
206
|
+
p_idempotency_key text default null,
|
|
207
|
+
p_metadata jsonb default '{}'::jsonb
|
|
208
|
+
)
|
|
209
|
+
returns table(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz)
|
|
210
|
+
language plpgsql
|
|
211
|
+
security definer
|
|
212
|
+
set search_path = agent_board, public
|
|
213
|
+
as $$
|
|
214
|
+
declare
|
|
215
|
+
v_message agent_board.messages%rowtype;
|
|
216
|
+
v_created_message record;
|
|
217
|
+
v_recipient_ids uuid[];
|
|
218
|
+
v_recipient_id uuid;
|
|
219
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
220
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
221
|
+
v_delivered_count integer := 0;
|
|
222
|
+
v_inbox_count integer := 0;
|
|
223
|
+
v_callback_count integer := 0;
|
|
224
|
+
v_metadata jsonb;
|
|
225
|
+
v_preview text;
|
|
226
|
+
v_fetch_descriptor jsonb;
|
|
227
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
228
|
+
v_actor_agent_id uuid;
|
|
229
|
+
begin
|
|
230
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
231
|
+
|
|
232
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
233
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
234
|
+
end if;
|
|
235
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
236
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
237
|
+
end if;
|
|
238
|
+
if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
|
|
239
|
+
raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
|
|
240
|
+
end if;
|
|
241
|
+
if array_position(p_recipient_agent_ids, null) is not null then
|
|
242
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
243
|
+
end if;
|
|
244
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
245
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
246
|
+
end if;
|
|
247
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
248
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
249
|
+
end if;
|
|
250
|
+
|
|
251
|
+
select array_agg(distinct recipient_id)
|
|
252
|
+
into v_recipient_ids
|
|
253
|
+
from unnest(p_recipient_agent_ids) as recipient_id;
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
select count(*)
|
|
257
|
+
from agent_board.agents
|
|
258
|
+
where id = any(v_recipient_ids)
|
|
259
|
+
) <> cardinality(v_recipient_ids) then
|
|
260
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
261
|
+
end if;
|
|
262
|
+
|
|
263
|
+
if agent_board._is_service_role() then
|
|
264
|
+
perform agent_board._record_send_authz_telemetry(
|
|
265
|
+
'service_role_bypass',
|
|
266
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
267
|
+
v_actor_agent_id,
|
|
268
|
+
null,
|
|
269
|
+
'send_multicast_message',
|
|
270
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
271
|
+
null,
|
|
272
|
+
jsonb_build_object('redacted', true)
|
|
273
|
+
);
|
|
274
|
+
end if;
|
|
275
|
+
|
|
276
|
+
perform agent_board._lock_idempotency_key(
|
|
277
|
+
'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
278
|
+
v_idempotency_key
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if v_idempotency_key is not null then
|
|
282
|
+
select *
|
|
283
|
+
into v_message
|
|
284
|
+
from agent_board.messages
|
|
285
|
+
where thread_id = p_thread_id
|
|
286
|
+
and author_agent_id = v_actor_agent_id
|
|
287
|
+
and metadata ->> 'rpcName' = 'send_multicast_message'
|
|
288
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
289
|
+
order by created_at desc
|
|
290
|
+
limit 1;
|
|
291
|
+
end if;
|
|
292
|
+
|
|
293
|
+
if v_message.id is null then
|
|
294
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
295
|
+
|| jsonb_build_object(
|
|
296
|
+
'rpcName', 'send_multicast_message',
|
|
297
|
+
'subject', trim(p_subject),
|
|
298
|
+
'priority', p_priority
|
|
299
|
+
);
|
|
300
|
+
if v_idempotency_key is not null then
|
|
301
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
302
|
+
end if;
|
|
303
|
+
|
|
304
|
+
select *
|
|
305
|
+
into v_created_message
|
|
306
|
+
from agent_board.post_message(
|
|
307
|
+
p_thread_id,
|
|
308
|
+
v_actor_agent_id,
|
|
309
|
+
p_body,
|
|
310
|
+
p_message_type,
|
|
311
|
+
null,
|
|
312
|
+
v_metadata
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
select *
|
|
316
|
+
into v_message
|
|
317
|
+
from agent_board.messages
|
|
318
|
+
where id = v_created_message.message_id;
|
|
319
|
+
end if;
|
|
320
|
+
|
|
321
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
322
|
+
|
|
323
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
324
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
325
|
+
'thread',
|
|
326
|
+
p_thread_id,
|
|
327
|
+
'agent_board.v_thread_feed',
|
|
328
|
+
'multicast delivery',
|
|
329
|
+
jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
333
|
+
'multicast',
|
|
334
|
+
v_recipient_id,
|
|
335
|
+
'message',
|
|
336
|
+
v_message.id,
|
|
337
|
+
null,
|
|
338
|
+
null,
|
|
339
|
+
null,
|
|
340
|
+
null,
|
|
341
|
+
jsonb_build_object('priority', p_priority)
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
345
|
+
v_recipient.id,
|
|
346
|
+
v_recipient_id,
|
|
347
|
+
null,
|
|
348
|
+
v_recipient.delivery_mode,
|
|
349
|
+
'message',
|
|
350
|
+
v_message.id,
|
|
351
|
+
null,
|
|
352
|
+
null,
|
|
353
|
+
trim(p_subject),
|
|
354
|
+
v_preview,
|
|
355
|
+
p_priority,
|
|
356
|
+
true,
|
|
357
|
+
false,
|
|
358
|
+
v_fetch_descriptor,
|
|
359
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
v_delivered_count := v_delivered_count + 1;
|
|
363
|
+
v_inbox_count := v_inbox_count + 1;
|
|
364
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
365
|
+
end loop;
|
|
366
|
+
|
|
367
|
+
return query
|
|
368
|
+
select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
|
|
369
|
+
end;
|
|
370
|
+
$$;
|
|
371
|
+
|
|
372
|
+
create or replace function agent_board.post_board_message(
|
|
373
|
+
p_thread_id uuid,
|
|
374
|
+
p_author_agent_id uuid,
|
|
375
|
+
p_board_id uuid,
|
|
376
|
+
p_subject text,
|
|
377
|
+
p_body text,
|
|
378
|
+
p_message_type text default 'comment',
|
|
379
|
+
p_priority text default 'medium',
|
|
380
|
+
p_idempotency_key text default null,
|
|
381
|
+
p_metadata jsonb default '{}'::jsonb
|
|
382
|
+
)
|
|
383
|
+
returns table(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamptz)
|
|
384
|
+
language plpgsql
|
|
385
|
+
security definer
|
|
386
|
+
set search_path = agent_board, public
|
|
387
|
+
as $$
|
|
388
|
+
declare
|
|
389
|
+
v_message agent_board.messages%rowtype;
|
|
390
|
+
v_created_message record;
|
|
391
|
+
v_membership record;
|
|
392
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
393
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
394
|
+
v_delivered_count integer := 0;
|
|
395
|
+
v_callback_count integer := 0;
|
|
396
|
+
v_metadata jsonb;
|
|
397
|
+
v_preview text;
|
|
398
|
+
v_fetch_descriptor jsonb;
|
|
399
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
400
|
+
v_actor_agent_id uuid;
|
|
401
|
+
begin
|
|
402
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
|
|
403
|
+
|
|
404
|
+
if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
|
|
405
|
+
raise exception 'subject and body are required' using errcode = '22023';
|
|
406
|
+
end if;
|
|
407
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
408
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
409
|
+
end if;
|
|
410
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
411
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
412
|
+
end if;
|
|
413
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
414
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
415
|
+
end if;
|
|
416
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
417
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
418
|
+
end if;
|
|
419
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
420
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
421
|
+
end if;
|
|
422
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
423
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
424
|
+
end if;
|
|
425
|
+
|
|
426
|
+
if agent_board._is_service_role() then
|
|
427
|
+
perform agent_board._record_send_authz_telemetry(
|
|
428
|
+
'service_role_bypass',
|
|
429
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
430
|
+
v_actor_agent_id,
|
|
431
|
+
p_board_id,
|
|
432
|
+
'post_board_message',
|
|
433
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
434
|
+
null,
|
|
435
|
+
jsonb_build_object('redacted', true)
|
|
436
|
+
);
|
|
437
|
+
end if;
|
|
438
|
+
|
|
439
|
+
perform agent_board._lock_idempotency_key(
|
|
440
|
+
'post_board_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_board_id::text,
|
|
441
|
+
v_idempotency_key
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if v_idempotency_key is not null then
|
|
445
|
+
select *
|
|
446
|
+
into v_message
|
|
447
|
+
from agent_board.messages
|
|
448
|
+
where thread_id = p_thread_id
|
|
449
|
+
and author_agent_id = v_actor_agent_id
|
|
450
|
+
and metadata ->> 'rpcName' = 'post_board_message'
|
|
451
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
452
|
+
order by created_at desc
|
|
453
|
+
limit 1;
|
|
454
|
+
end if;
|
|
455
|
+
|
|
456
|
+
if v_message.id is null then
|
|
457
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
458
|
+
|| jsonb_build_object(
|
|
459
|
+
'rpcName', 'post_board_message',
|
|
460
|
+
'subject', trim(p_subject),
|
|
461
|
+
'priority', p_priority,
|
|
462
|
+
'boardId', p_board_id::text
|
|
463
|
+
);
|
|
464
|
+
if v_idempotency_key is not null then
|
|
465
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
466
|
+
end if;
|
|
467
|
+
|
|
468
|
+
select *
|
|
469
|
+
into v_created_message
|
|
470
|
+
from agent_board.post_message(
|
|
471
|
+
p_thread_id,
|
|
472
|
+
v_actor_agent_id,
|
|
473
|
+
p_body,
|
|
474
|
+
p_message_type,
|
|
475
|
+
null,
|
|
476
|
+
v_metadata
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
select *
|
|
480
|
+
into v_message
|
|
481
|
+
from agent_board.messages
|
|
482
|
+
where id = v_created_message.message_id;
|
|
483
|
+
end if;
|
|
484
|
+
|
|
485
|
+
v_preview := agent_board._trim_inbox_text(p_body, 280);
|
|
486
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
487
|
+
'board',
|
|
488
|
+
p_board_id,
|
|
489
|
+
'agent_board.v_board_feed',
|
|
490
|
+
'board delivery',
|
|
491
|
+
jsonb_build_object('messageId', v_message.id::text)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
for v_membership in
|
|
495
|
+
select bm.id, bm.agent_id
|
|
496
|
+
from agent_board.board_memberships bm
|
|
497
|
+
where bm.board_id = p_board_id
|
|
498
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
499
|
+
order by bm.created_at
|
|
500
|
+
loop
|
|
501
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
502
|
+
'board',
|
|
503
|
+
v_membership.agent_id,
|
|
504
|
+
'message',
|
|
505
|
+
v_message.id,
|
|
506
|
+
null,
|
|
507
|
+
null,
|
|
508
|
+
p_board_id,
|
|
509
|
+
v_membership.id,
|
|
510
|
+
jsonb_build_object('priority', p_priority)
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
514
|
+
v_recipient.id,
|
|
515
|
+
v_membership.agent_id,
|
|
516
|
+
p_board_id,
|
|
517
|
+
v_recipient.delivery_mode,
|
|
518
|
+
'message',
|
|
519
|
+
v_message.id,
|
|
520
|
+
null,
|
|
521
|
+
null,
|
|
522
|
+
trim(p_subject),
|
|
523
|
+
v_preview,
|
|
524
|
+
p_priority,
|
|
525
|
+
true,
|
|
526
|
+
true,
|
|
527
|
+
v_fetch_descriptor,
|
|
528
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
v_delivered_count := v_delivered_count + 1;
|
|
532
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
533
|
+
end loop;
|
|
534
|
+
|
|
535
|
+
v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
|
|
536
|
+
p_board_id,
|
|
537
|
+
v_message.id,
|
|
538
|
+
trim(p_subject),
|
|
539
|
+
v_preview,
|
|
540
|
+
p_priority,
|
|
541
|
+
v_fetch_descriptor
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
return query
|
|
545
|
+
select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
|
|
546
|
+
end;
|
|
547
|
+
$$;
|
|
548
|
+
|
|
549
|
+
create or replace function agent_board.create_tasking_delivery(
|
|
550
|
+
p_thread_id uuid,
|
|
551
|
+
p_created_by_agent_id uuid,
|
|
552
|
+
p_title text,
|
|
553
|
+
p_details text default null,
|
|
554
|
+
p_recipient_agent_ids uuid[] default null,
|
|
555
|
+
p_board_id uuid default null,
|
|
556
|
+
p_priority text default 'medium',
|
|
557
|
+
p_orchestration_mode text default 'distributed',
|
|
558
|
+
p_idempotency_key text default null,
|
|
559
|
+
p_metadata jsonb default '{}'::jsonb
|
|
560
|
+
)
|
|
561
|
+
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)
|
|
562
|
+
language plpgsql
|
|
563
|
+
security definer
|
|
564
|
+
set search_path = agent_board, public
|
|
565
|
+
as $$
|
|
566
|
+
declare
|
|
567
|
+
v_task agent_board.tasks%rowtype;
|
|
568
|
+
v_created_event_id uuid;
|
|
569
|
+
v_created_task record;
|
|
570
|
+
v_recipient_ids uuid[];
|
|
571
|
+
v_recipient_id uuid;
|
|
572
|
+
v_membership record;
|
|
573
|
+
v_recipient agent_board.delivery_recipients%rowtype;
|
|
574
|
+
v_inbox agent_board.inbox_items%rowtype;
|
|
575
|
+
v_delivered_count integer := 0;
|
|
576
|
+
v_inbox_count integer := 0;
|
|
577
|
+
v_callback_count integer := 0;
|
|
578
|
+
v_metadata jsonb;
|
|
579
|
+
v_preview text;
|
|
580
|
+
v_fetch_descriptor jsonb;
|
|
581
|
+
v_idempotency_key text := nullif(trim(p_idempotency_key), '');
|
|
582
|
+
v_actor_agent_id uuid;
|
|
583
|
+
begin
|
|
584
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
|
|
585
|
+
|
|
586
|
+
if nullif(trim(p_title), '') is null then
|
|
587
|
+
raise exception 'task title is required' using errcode = '22023';
|
|
588
|
+
end if;
|
|
589
|
+
if p_priority not in ('low', 'medium', 'high', 'urgent') then
|
|
590
|
+
raise exception 'invalid priority' using errcode = '22023';
|
|
591
|
+
end if;
|
|
592
|
+
if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
|
|
593
|
+
raise exception 'invalid orchestration mode' using errcode = '22023';
|
|
594
|
+
end if;
|
|
595
|
+
if not exists (select 1 from agent_board.threads where id = p_thread_id) then
|
|
596
|
+
raise exception 'thread not found' using errcode = 'P0002';
|
|
597
|
+
end if;
|
|
598
|
+
if not agent_board._current_agent_can_access_thread(p_thread_id) then
|
|
599
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
600
|
+
end if;
|
|
601
|
+
if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
|
|
602
|
+
raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
|
|
603
|
+
end if;
|
|
604
|
+
if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
|
|
605
|
+
raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
|
|
606
|
+
end if;
|
|
607
|
+
if p_board_id is not null then
|
|
608
|
+
if not exists (select 1 from agent_board.boards where id = p_board_id) then
|
|
609
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
610
|
+
end if;
|
|
611
|
+
if not agent_board._current_agent_has_board_access(p_board_id) then
|
|
612
|
+
raise exception 'board not accessible to current agent' using errcode = '42501';
|
|
613
|
+
end if;
|
|
614
|
+
if not agent_board._current_agent_can_send_to_board(p_board_id) then
|
|
615
|
+
raise exception 'board send requires a non-observer board role' using errcode = '42501';
|
|
616
|
+
end if;
|
|
617
|
+
end if;
|
|
618
|
+
|
|
619
|
+
select array_agg(distinct recipient_id)
|
|
620
|
+
into v_recipient_ids
|
|
621
|
+
from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
|
|
622
|
+
|
|
623
|
+
if v_recipient_ids is not null and (
|
|
624
|
+
select count(*)
|
|
625
|
+
from agent_board.agents
|
|
626
|
+
where id = any(v_recipient_ids)
|
|
627
|
+
) <> cardinality(v_recipient_ids) then
|
|
628
|
+
raise exception 'recipient agent not found' using errcode = 'P0002';
|
|
629
|
+
end if;
|
|
630
|
+
|
|
631
|
+
if agent_board._is_service_role() then
|
|
632
|
+
perform agent_board._record_send_authz_telemetry(
|
|
633
|
+
'service_role_bypass',
|
|
634
|
+
coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
|
|
635
|
+
v_actor_agent_id,
|
|
636
|
+
p_board_id,
|
|
637
|
+
'create_tasking_delivery',
|
|
638
|
+
agent_board._resolve_send_fleet_scope(v_actor_agent_id),
|
|
639
|
+
null,
|
|
640
|
+
jsonb_build_object('redacted', true)
|
|
641
|
+
);
|
|
642
|
+
end if;
|
|
643
|
+
|
|
644
|
+
perform agent_board._lock_idempotency_key(
|
|
645
|
+
'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
|
|
646
|
+
v_idempotency_key
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
if v_idempotency_key is not null then
|
|
650
|
+
select *
|
|
651
|
+
into v_task
|
|
652
|
+
from agent_board.tasks
|
|
653
|
+
where thread_id = p_thread_id
|
|
654
|
+
and created_by_agent_id = v_actor_agent_id
|
|
655
|
+
and metadata ->> 'rpcName' = 'create_tasking_delivery'
|
|
656
|
+
and metadata ->> 'idempotencyKey' = v_idempotency_key
|
|
657
|
+
order by created_at desc
|
|
658
|
+
limit 1;
|
|
659
|
+
end if;
|
|
660
|
+
|
|
661
|
+
if v_task.id is null then
|
|
662
|
+
v_metadata := coalesce(p_metadata, '{}'::jsonb)
|
|
663
|
+
|| jsonb_build_object(
|
|
664
|
+
'rpcName', 'create_tasking_delivery',
|
|
665
|
+
'priority', p_priority
|
|
666
|
+
);
|
|
667
|
+
if p_board_id is not null then
|
|
668
|
+
v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
|
|
669
|
+
end if;
|
|
670
|
+
if v_idempotency_key is not null then
|
|
671
|
+
v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
|
|
672
|
+
end if;
|
|
673
|
+
|
|
674
|
+
select *
|
|
675
|
+
into v_created_task
|
|
676
|
+
from agent_board.create_task(
|
|
677
|
+
p_thread_id,
|
|
678
|
+
v_actor_agent_id,
|
|
679
|
+
trim(p_title),
|
|
680
|
+
p_details,
|
|
681
|
+
null,
|
|
682
|
+
null,
|
|
683
|
+
p_priority,
|
|
684
|
+
p_orchestration_mode,
|
|
685
|
+
v_metadata
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
v_created_event_id := v_created_task.created_event_id;
|
|
689
|
+
|
|
690
|
+
select *
|
|
691
|
+
into v_task
|
|
692
|
+
from agent_board.tasks
|
|
693
|
+
where id = v_created_task.task_id;
|
|
694
|
+
else
|
|
695
|
+
select te.id
|
|
696
|
+
into v_created_event_id
|
|
697
|
+
from agent_board.task_events te
|
|
698
|
+
where te.task_id = v_task.id
|
|
699
|
+
and te.event_sequence = 1;
|
|
700
|
+
end if;
|
|
701
|
+
|
|
702
|
+
v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
|
|
703
|
+
v_fetch_descriptor := agent_board._build_fetch_descriptor(
|
|
704
|
+
'task',
|
|
705
|
+
v_task.id,
|
|
706
|
+
'agent_board.v_task_tree',
|
|
707
|
+
'tasking delivery',
|
|
708
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
if v_recipient_ids is not null then
|
|
712
|
+
foreach v_recipient_id in array v_recipient_ids loop
|
|
713
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
714
|
+
'tasking',
|
|
715
|
+
v_recipient_id,
|
|
716
|
+
'task',
|
|
717
|
+
null,
|
|
718
|
+
v_task.id,
|
|
719
|
+
null,
|
|
720
|
+
null,
|
|
721
|
+
null,
|
|
722
|
+
jsonb_build_object('priority', p_priority)
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
726
|
+
v_recipient.id,
|
|
727
|
+
v_recipient_id,
|
|
728
|
+
null,
|
|
729
|
+
v_recipient.delivery_mode,
|
|
730
|
+
'task',
|
|
731
|
+
null,
|
|
732
|
+
v_task.id,
|
|
733
|
+
null,
|
|
734
|
+
trim(p_title),
|
|
735
|
+
v_preview,
|
|
736
|
+
p_priority,
|
|
737
|
+
true,
|
|
738
|
+
false,
|
|
739
|
+
v_fetch_descriptor,
|
|
740
|
+
jsonb_build_object('threadId', p_thread_id::text)
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
v_delivered_count := v_delivered_count + 1;
|
|
744
|
+
v_inbox_count := v_inbox_count + 1;
|
|
745
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
746
|
+
end loop;
|
|
747
|
+
end if;
|
|
748
|
+
|
|
749
|
+
if p_board_id is not null then
|
|
750
|
+
for v_membership in
|
|
751
|
+
select bm.id, bm.agent_id
|
|
752
|
+
from agent_board.board_memberships bm
|
|
753
|
+
where bm.board_id = p_board_id
|
|
754
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
755
|
+
order by bm.created_at
|
|
756
|
+
loop
|
|
757
|
+
v_recipient := agent_board._upsert_delivery_recipient(
|
|
758
|
+
'tasking',
|
|
759
|
+
v_membership.agent_id,
|
|
760
|
+
'task',
|
|
761
|
+
null,
|
|
762
|
+
v_task.id,
|
|
763
|
+
null,
|
|
764
|
+
p_board_id,
|
|
765
|
+
v_membership.id,
|
|
766
|
+
jsonb_build_object('priority', p_priority)
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
v_inbox := agent_board._upsert_inbox_item(
|
|
770
|
+
v_recipient.id,
|
|
771
|
+
v_membership.agent_id,
|
|
772
|
+
v_recipient.board_id,
|
|
773
|
+
v_recipient.delivery_mode,
|
|
774
|
+
'task',
|
|
775
|
+
null,
|
|
776
|
+
v_task.id,
|
|
777
|
+
null,
|
|
778
|
+
trim(p_title),
|
|
779
|
+
v_preview,
|
|
780
|
+
p_priority,
|
|
781
|
+
true,
|
|
782
|
+
v_recipient.board_id is not null,
|
|
783
|
+
v_fetch_descriptor,
|
|
784
|
+
jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
v_delivered_count := v_delivered_count + 1;
|
|
788
|
+
v_inbox_count := v_inbox_count + 1;
|
|
789
|
+
v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
|
|
790
|
+
end loop;
|
|
791
|
+
end if;
|
|
792
|
+
|
|
793
|
+
return query
|
|
794
|
+
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;
|
|
795
|
+
end;
|
|
796
|
+
$$;
|
|
797
|
+
|
|
798
|
+
-- 3. Re-assert dormancy grants: all four core send RPCs stay revoked from
|
|
799
|
+
-- authenticated (mirrors 20260711000100:224-231). create or replace preserves
|
|
800
|
+
-- grants, but re-assert so a fresh replay is order-independent.
|
|
801
|
+
revoke execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
|
|
802
|
+
revoke execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) from authenticated;
|
|
803
|
+
revoke execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
|
|
804
|
+
revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from authenticated;
|