@consiliency/agent-board-schema 1.58.0 → 1.60.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.
@@ -0,0 +1,490 @@
1
+ -- OWN POST NOT DELIVERED pgTAP (Consiliency/message-board#608, migration 20261010000100).
2
+ --
3
+ -- The owner's rule (2026-10-03): an agent does not receive its own room post as an inbox item,
4
+ -- and its own writes do not wake it. The wake side reads v_agent_wake_recovery, so "no inbox item
5
+ -- for the author" is asserted on that view, as the author, through a real authenticated session.
6
+ --
7
+ -- THE CLASS COMES FROM THE CATALOG (AGENTS.md rule 1). A recipient set can reach its own sender in
8
+ -- two ways, and each is derived from pg_proc so a new site fails here BY NAME:
9
+ -- * a BOARD expansion -- OPN-01 finds every inbox_items inserter; OPN-02 every caller of it that
10
+ -- reads board_memberships; OPN-03 the callers that do not (asserted positively, OPN-18..22, 29, 33);
11
+ -- * a DESCRIPTOR expansion -- OPN-34 finds every caller of the descriptor resolver; the only one that
12
+ -- delivers is _addressing_agent_ids_or_raise, asserted through both its overload callers (OPN-25..32).
13
+ -- Each member is asserted behaviourally, as an authenticated session; each exemption POSITIVELY.
14
+ --
15
+ -- Mutation controls (measured 2026-10-03, each run counting all 40 assertions):
16
+ -- * post_board_message writes the author's row visible_in_inbox = true -> OPN-06, 07, 09, 10, 14
17
+ -- * post_board_message skips the author's row entirely -> OPN-09, 13
18
+ -- * post_board_message counts the author in delivered_recipient_count -> OPN-05
19
+ -- * _queue_inbox_callbacks loses its visible_in_inbox filter -> OPN-10
20
+ -- * create_tasking_delivery writes the creator's row visible_in_inbox -> OPN-16
21
+ -- * create_tasking_delivery skips the creator (the round-1 shape) -> OPN-16, 24
22
+ -- * the board loop demotes an explicit self-listing (the round-2 shape) -> OPN-35, 39 (abort), 37, 40
23
+ -- * the board loop skips an explicitly listed creator (round-3 T1 shape) -> OPN-40
24
+ -- * _addressing_agent_ids_or_raise keeps the sender for a set descriptor -> OPN-28, 30, 32
25
+ -- * ... and drops it even when named explicitly -> OPN-31, 38
26
+ -- * a new function that calls _upsert_inbox_item over board_memberships -> OPN-02
27
+ -- * a new caller of _resolve_addressing_recipient_rows -> OPN-34
28
+ -- (create_tasking_delivery counting its creator is pinned by inbox_rpcs and v2_invariants.)
29
+ -- TAP numbers follow EMISSION order, not labels (OPN-34 is emitted last): cite by label.
30
+ --
31
+ -- HAND-COUNTED plan: OPN-01..OPN-40 = 40 assertions. The runner checks the emitted count.
32
+ create extension if not exists pgtap with schema extensions;
33
+ set search_path = extensions, public, agent_board;
34
+
35
+ begin;
36
+
37
+ select plan(40);
38
+
39
+ -- ---------------------------------------------------------------------------
40
+ -- The class, derived from pg_proc.
41
+ -- ---------------------------------------------------------------------------
42
+ select is(
43
+ (select array_agg(p.proname::text order by p.proname)
44
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
45
+ where n.nspname = 'agent_board'
46
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into|merge into', 'inbox_items')),
47
+ array['_upsert_inbox_item'],
48
+ 'OPN-01: _upsert_inbox_item is the only function that inserts into inbox_items');
49
+
50
+ select is(
51
+ (select array_agg(p.proname::text order by p.proname)
52
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
53
+ where n.nspname = 'agent_board'
54
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
55
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.qname_re('board_memberships')
56
+ and p.proname <> '_upsert_inbox_item'),
57
+ array['create_tasking_delivery', 'post_board_message'],
58
+ 'OPN-02: the board-expanding delivery writers are exactly the two this file asserts behaviourally');
59
+
60
+ select is(
61
+ (select array_agg(p.proname::text order by p.proname)
62
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
63
+ where n.nspname = 'agent_board'
64
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
65
+ and agent_board_test.stripped_body(p.oid) !~ agent_board_test.qname_re('board_memberships')
66
+ and p.proname <> '_upsert_inbox_item'),
67
+ array['send_direct_message', 'send_multicast_message'],
68
+ 'OPN-03: the non-expanding delivery writers are exactly the two exemptions asserted below');
69
+
70
+ -- ---------------------------------------------------------------------------
71
+ -- Fixture: author A, member B; board AB (A owner, B joined) and board S (A alone).
72
+ -- ---------------------------------------------------------------------------
73
+ create temp table _opn (
74
+ a_user uuid, b_user uuid,
75
+ a_agent uuid, b_agent uuid,
76
+ board_ab uuid, board_s uuid, thread uuid, thread_unlinked uuid, thread_unlinked2 uuid
77
+ ) on commit drop;
78
+ create temp table _opn_post (message_id uuid, board_id uuid, delivered_recipient_count integer,
79
+ callback_outbox_count integer, created_at timestamptz) on commit drop;
80
+ create temp table _opn_sole (like _opn_post) on commit drop;
81
+ create temp table _opn_task (task_id uuid, task_event_id uuid, delivered_recipient_count integer,
82
+ inbox_item_count integer, callback_outbox_count integer, created_at timestamptz,
83
+ updated_at timestamptz) on commit drop;
84
+ create temp table _opn_self_task (like _opn_task) on commit drop;
85
+ create temp table _opn_sole_task (like _opn_task) on commit drop;
86
+ create temp table _opn_desc_task (like _opn_task) on commit drop;
87
+ create temp table _opn_both_task (like _opn_task) on commit drop;
88
+ create temp table _opn_both_sole (like _opn_task) on commit drop;
89
+ create temp table _opn_multi (message_id uuid, delivered_recipient_count integer, inbox_item_count integer,
90
+ callback_outbox_count integer, created_at timestamptz) on commit drop;
91
+ create temp table _opn_group (like _opn_multi) on commit drop;
92
+ create temp table _opn_group_self (like _opn_multi) on commit drop;
93
+ create temp table _opn_nested (like _opn_multi) on commit drop;
94
+ create temp table _opn_direct (message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid,
95
+ callback_outbox_count integer, created_at timestamptz) on commit drop;
96
+ grant all on table _opn, _opn_post, _opn_sole, _opn_task, _opn_self_task, _opn_multi,
97
+ _opn_sole_task, _opn_desc_task, _opn_group, _opn_group_self, _opn_direct, _opn_both_task, _opn_nested, _opn_both_sole
98
+ to authenticated, service_role;
99
+
100
+ do $$
101
+ declare
102
+ v_a_user uuid := public.ensure_github_user('opn-author', 'opn-author@example.test', 'opn-author', 'OPN Author');
103
+ v_b_user uuid := public.ensure_github_user('opn-member', 'opn-member@example.test', 'opn-member', 'OPN Member');
104
+ v_a uuid := '76080000-0000-4000-8000-000000000001';
105
+ v_b uuid := '76080000-0000-4000-8000-000000000002';
106
+ v_thread uuid := '76080000-0000-4000-8000-000000000010';
107
+ v_thread_unlinked uuid := '76080000-0000-4000-8000-000000000011';
108
+ v_thread_unlinked2 uuid := '76080000-0000-4000-8000-000000000012';
109
+ v_board_ab uuid;
110
+ v_board_s uuid;
111
+ v_endpoint uuid;
112
+ begin
113
+ -- Both agents are in the group alias `opn-team` and carry the role `opn-role`, so a set-naming
114
+ -- descriptor resolves to BOTH of them -- the author included -- before the #608 rule applies.
115
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id, metadata)
116
+ values
117
+ (v_a, 'agent', 'opn-author', 'OPN Author', 'active', v_a_user, v_a_user, '{"groupAliases":["opn-team","opn-solo"],"roles":["opn-role"]}'::jsonb),
118
+ (v_b, 'agent', 'opn-member', 'OPN Member', 'active', v_b_user, v_b_user, '{"groupAliases":["opn-team"],"roles":["opn-role"]}'::jsonb);
119
+
120
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
121
+ values (v_thread, 'coordination', v_a, 'OPN thread'),
122
+ (v_thread_unlinked, 'coordination', v_a, 'OPN unlinked thread'),
123
+ (v_thread_unlinked2, 'coordination', v_a, 'OPN second unlinked thread');
124
+
125
+ select board_id into v_board_ab
126
+ from agent_board.create_board(v_a, 'opn-board-ab', 'OPN AB', 'two members', '{}'::jsonb, 'private');
127
+ perform agent_board.join_board(v_board_ab, v_b, 'joined', v_a, '{}'::jsonb);
128
+ select board_id into v_board_s
129
+ from agent_board.create_board(v_a, 'opn-board-s', 'OPN S', 'author alone', '{}'::jsonb, 'private');
130
+
131
+ -- Both agents subscribe their own inbox to delivered callbacks: B's is the positive control
132
+ -- for OPN-10, so a zero for A cannot come from a fixture that queues nothing for anyone.
133
+ select endpoint_id into v_endpoint from agent_board.register_callback_endpoint(v_a, 'https://example.test/opn-a', '{}'::jsonb);
134
+ perform agent_board.verify_callback_endpoint(v_endpoint, v_a, '{"controlProofRef":"opn/a"}'::jsonb);
135
+ perform agent_board.subscribe_callback_endpoint(v_endpoint, v_a, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
136
+ select endpoint_id into v_endpoint from agent_board.register_callback_endpoint(v_b, 'https://example.test/opn-b', '{}'::jsonb);
137
+ perform agent_board.verify_callback_endpoint(v_endpoint, v_b, '{"controlProofRef":"opn/b"}'::jsonb);
138
+ perform agent_board.subscribe_callback_endpoint(v_endpoint, v_b, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
139
+
140
+ insert into _opn values (v_a_user, v_b_user, v_a, v_b, v_board_ab, v_board_s, v_thread, v_thread_unlinked, v_thread_unlinked2);
141
+ end;
142
+ $$;
143
+
144
+ -- ---------------------------------------------------------------------------
145
+ -- A posts to AB, through its own authenticated session.
146
+ -- ---------------------------------------------------------------------------
147
+ select set_config('request.jwt.claim.role', 'authenticated', true);
148
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
149
+ set local role authenticated;
150
+
151
+ select lives_ok(
152
+ $$ insert into _opn_post
153
+ select * from agent_board.post_board_message(
154
+ (select thread from _opn), null, (select board_ab from _opn),
155
+ 'opn post', 'the author posts to a two-member board', 'comment', 'medium', null, '{}'::jsonb) $$,
156
+ 'OPN-04: the author can post to a board it owns');
157
+
158
+ select is((select delivered_recipient_count from _opn_post), 1,
159
+ 'OPN-05: delivered_recipient_count counts the other member only, not the author');
160
+
161
+ select is(
162
+ (select count(*)::int from agent_board.v_agent_wake_recovery
163
+ where source_message_id = (select message_id from _opn_post)),
164
+ 0,
165
+ 'OPN-06: the author''s own post is not in the author''s v_agent_wake_recovery');
166
+
167
+ select is(
168
+ (select count(*)::int from agent_board.v_agent_inbox
169
+ where source_message_id = (select message_id from _opn_post)
170
+ and recipient_agent_id = (select a_agent from _opn)),
171
+ 0,
172
+ 'OPN-07: the author''s own post is not in the author''s v_agent_inbox');
173
+
174
+ reset role;
175
+ select set_config('request.jwt.claim.sub', (select b_user::text from _opn), true);
176
+ set local role authenticated;
177
+
178
+ select is(
179
+ (select count(*)::int from agent_board.v_agent_wake_recovery
180
+ where source_message_id = (select message_id from _opn_post)),
181
+ 1,
182
+ 'OPN-08: the other member does receive the post as an unhandled item');
183
+
184
+ reset role;
185
+ select set_config('request.jwt.claim.role', '', true);
186
+ select set_config('request.jwt.claim.sub', '', true);
187
+
188
+ select is(
189
+ (select array_agg(array[ii.visible_in_inbox, ii.visible_in_board_feed])
190
+ from agent_board.inbox_items ii
191
+ where ii.source_message_id = (select message_id from _opn_post)
192
+ and ii.recipient_agent_id = (select a_agent from _opn)),
193
+ array[array[false, true]],
194
+ 'OPN-09: the author keeps exactly one row for its post, and it is feed-only');
195
+
196
+ select is(
197
+ (select count(*)::int from agent_board.callback_outbox_events
198
+ where source_message_id = (select message_id from _opn_post)
199
+ and event_category = 'inbox.item.delivered'
200
+ and recipient_agent_id = (select a_agent from _opn)),
201
+ 0,
202
+ 'OPN-10: no own_inbox delivered callback is queued to the author for its own post');
203
+
204
+ select is(
205
+ (select count(*)::int from agent_board.callback_outbox_events
206
+ where source_message_id = (select message_id from _opn_post)
207
+ and event_category = 'inbox.item.delivered'
208
+ and recipient_agent_id = (select b_agent from _opn)),
209
+ 1,
210
+ 'OPN-11: the other member''s own_inbox delivered callback is queued (control for OPN-10)');
211
+
212
+ -- ---------------------------------------------------------------------------
213
+ -- A posts to S, where it is the only member: the post must stay in the feed.
214
+ -- ---------------------------------------------------------------------------
215
+ select set_config('request.jwt.claim.role', 'authenticated', true);
216
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
217
+ set local role authenticated;
218
+
219
+ select lives_ok(
220
+ $$ insert into _opn_sole
221
+ select * from agent_board.post_board_message(
222
+ (select thread from _opn), null, (select board_s from _opn),
223
+ 'opn sole post', 'the author posts to a board it is alone on', 'comment', 'medium', null, '{}'::jsonb) $$,
224
+ 'OPN-12: the author can post to a board it is the only member of');
225
+
226
+ select is(
227
+ (select count(*)::int from agent_board.v_board_feed
228
+ where message_id = (select message_id from _opn_sole)),
229
+ 1,
230
+ 'OPN-13: a post on a board whose only member is its author is still in that board''s feed');
231
+
232
+ select is(
233
+ (select count(*)::int from agent_board.v_agent_wake_recovery
234
+ where source_message_id = (select message_id from _opn_sole)),
235
+ 0,
236
+ 'OPN-14: the sole-member post is not in the author''s v_agent_wake_recovery either');
237
+
238
+ -- ---------------------------------------------------------------------------
239
+ -- A creates a board-wide task on AB.
240
+ -- ---------------------------------------------------------------------------
241
+ select lives_ok(
242
+ $$ insert into _opn_task
243
+ select * from agent_board.create_tasking_delivery(
244
+ (select thread from _opn), null, 'opn task', 'a board-wide task', null,
245
+ (select board_ab from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
246
+ 'OPN-15: the creator can deliver a task to its board');
247
+
248
+ reset role;
249
+ select set_config('request.jwt.claim.role', '', true);
250
+ select set_config('request.jwt.claim.sub', '', true);
251
+
252
+ select is(
253
+ (select array_agg(array[ii.visible_in_inbox, ii.visible_in_board_feed])
254
+ from agent_board.inbox_items ii
255
+ where ii.source_task_id = (select task_id from _opn_task)
256
+ and ii.recipient_agent_id = (select a_agent from _opn)),
257
+ array[array[false, true]],
258
+ 'OPN-16: the board fan-out of a task gives its creator one feed-only row, not an inbox item');
259
+
260
+ select is(
261
+ (select count(*)::int from agent_board.inbox_items
262
+ where source_task_id = (select task_id from _opn_task)
263
+ and recipient_agent_id = (select b_agent from _opn)
264
+ and visible_in_inbox),
265
+ 1,
266
+ 'OPN-17: the board fan-out of a task delivers it to the other member');
267
+
268
+ -- ---------------------------------------------------------------------------
269
+ -- Exemptions, asserted positively: the explicit-recipient paths deliver what the caller lists.
270
+ -- ---------------------------------------------------------------------------
271
+ select set_config('request.jwt.claim.role', 'authenticated', true);
272
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
273
+ set local role authenticated;
274
+
275
+ select lives_ok(
276
+ $$ insert into _opn_multi
277
+ select * from agent_board.send_multicast_message(
278
+ (select thread from _opn), null, array[(select b_agent from _opn)],
279
+ 'opn multicast', 'listed recipients only', 'comment', 'medium', null, '{}'::jsonb) $$,
280
+ 'OPN-18: the author can multicast to a listed recipient');
281
+
282
+ select lives_ok(
283
+ $$ insert into _opn_self_task
284
+ select * from agent_board.create_tasking_delivery(
285
+ (select thread from _opn), null, 'opn self task', 'the creator lists itself',
286
+ array[(select a_agent from _opn)], null, 'medium', 'distributed', null, '{}'::jsonb) $$,
287
+ 'OPN-19: the creator can list itself as an explicit task recipient');
288
+
289
+ reset role;
290
+ select set_config('request.jwt.claim.role', '', true);
291
+ select set_config('request.jwt.claim.sub', '', true);
292
+
293
+ select is(
294
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
295
+ from agent_board.inbox_items
296
+ where source_message_id = (select message_id from _opn_multi)),
297
+ array[(select b_agent from _opn)],
298
+ 'OPN-20: a multicast delivers to exactly the listed recipients, so not to an unlisted author');
299
+
300
+ select is(
301
+ (select count(*)::int from agent_board.inbox_items
302
+ where source_task_id = (select task_id from _opn_self_task)
303
+ and recipient_agent_id = (select a_agent from _opn)
304
+ and visible_in_inbox),
305
+ 1,
306
+ 'OPN-21: an explicit recipient list naming the creator still delivers to it');
307
+
308
+ select is(
309
+ (select delivered_recipient_count from _opn_self_task),
310
+ 1,
311
+ 'OPN-22: the explicit self-addressed task counts its one listed recipient');
312
+
313
+ -- ---------------------------------------------------------------------------
314
+ -- Round 1 of the PR #615 board: the creator-only task, the descriptor expansions, a direct send.
315
+ -- ---------------------------------------------------------------------------
316
+ select set_config('request.jwt.claim.role', 'authenticated', true);
317
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
318
+ set local role authenticated;
319
+
320
+ -- A board-wide task on a board whose only member is its creator. Skipping the creator's delivery
321
+ -- would skip the only write that stamps threads.board_id (OBSGATE #78), leaving the thread on the
322
+ -- access-only branch of post_message's observer gate. Mutation: exclude the creator from the loop.
323
+ select lives_ok(
324
+ $$ insert into _opn_sole_task
325
+ select * from agent_board.create_tasking_delivery(
326
+ (select thread_unlinked from _opn), null, 'opn sole task', 'creator alone', null,
327
+ (select board_s from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
328
+ 'OPN-23: the creator can deliver a task to a board it is alone on');
329
+
330
+ reset role;
331
+ select set_config('request.jwt.claim.role', '', true);
332
+ select set_config('request.jwt.claim.sub', '', true);
333
+
334
+ select is(
335
+ (select board_id from agent_board.threads where id = (select thread_unlinked from _opn)),
336
+ (select board_s from _opn),
337
+ 'OPN-24: a creator-only board task still links its thread to the board');
338
+
339
+ select set_config('request.jwt.claim.role', 'authenticated', true);
340
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
341
+ set local role authenticated;
342
+
343
+ -- A multicast to a group alias the author belongs to.
344
+ select lives_ok(
345
+ $$ insert into _opn_group
346
+ select * from agent_board.send_multicast_message(
347
+ (select thread from _opn), null, '[{"kind":"group_alias","groupAlias":"opn-team"}]'::jsonb,
348
+ 'addressing_v1', 'opn group', 'to a group the author is in', 'comment', 'medium', null, '{}'::jsonb) $$,
349
+ 'OPN-25: the author can multicast to a group alias it belongs to');
350
+
351
+ -- The same group, plus an explicit agent_id naming the author AFTER it: the explicit mention wins.
352
+ select lives_ok(
353
+ $$ insert into _opn_group_self
354
+ select * from agent_board.send_multicast_message(
355
+ (select thread from _opn), null,
356
+ jsonb_build_array(
357
+ jsonb_build_object('kind', 'group_alias', 'groupAlias', 'opn-team'),
358
+ jsonb_build_object('kind', 'agent_id', 'agentId', (select a_agent from _opn)::text)),
359
+ 'addressing_v1', 'opn group self', 'the group, and the author by id', 'comment', 'medium', null, '{}'::jsonb) $$,
360
+ 'OPN-26: the author can multicast to a group and name itself explicitly');
361
+
362
+ -- A task to a role selector the creator carries.
363
+ select lives_ok(
364
+ $$ insert into _opn_desc_task
365
+ select * from agent_board.create_tasking_delivery(
366
+ (select thread from _opn), null, 'opn role task', 'to a role the creator has',
367
+ '[{"kind":"role_selector","role":"opn-role"}]'::jsonb, null, 'addressing_v1',
368
+ 'medium', 'distributed', null, '{}'::jsonb) $$,
369
+ 'OPN-27: the creator can deliver a task to a role selector it matches');
370
+
371
+ -- A group whose only member is the sender resolves to nobody else: refused, not self-delivered.
372
+ select throws_ok(
373
+ $$ select * from agent_board.send_multicast_message(
374
+ (select thread from _opn), null, '[{"kind":"group_alias","groupAlias":"opn-solo"}]'::jsonb,
375
+ 'addressing_v1', 'opn solo', 'a group of one', 'comment', 'medium', null, '{}'::jsonb) $$,
376
+ '22023',
377
+ 'recipient descriptors resolved only the sender',
378
+ 'OPN-28: a set descriptor that resolves only to the sender is refused');
379
+
380
+ select lives_ok(
381
+ $$ insert into _opn_direct
382
+ select * from agent_board.send_direct_message(
383
+ (select thread from _opn), null, (select b_agent from _opn),
384
+ 'opn direct', 'to the member', 'comment', 'medium', null, '{}'::jsonb) $$,
385
+ 'OPN-29: the author can send a direct message');
386
+
387
+ reset role;
388
+ select set_config('request.jwt.claim.role', '', true);
389
+ select set_config('request.jwt.claim.sub', '', true);
390
+
391
+ select is(
392
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
393
+ from agent_board.inbox_items
394
+ where source_message_id = (select message_id from _opn_group)),
395
+ array[(select b_agent from _opn)],
396
+ 'OPN-30: a group alias the author belongs to delivers to the other member only');
397
+
398
+ select is(
399
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
400
+ from agent_board.inbox_items
401
+ where source_message_id = (select message_id from _opn_group_self)
402
+ and visible_in_inbox),
403
+ array[(select a_agent from _opn), (select b_agent from _opn)],
404
+ 'OPN-31: an explicit agent_id naming the author delivers to it even beside a group that contains it');
405
+
406
+ select is(
407
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
408
+ from agent_board.inbox_items
409
+ where source_task_id = (select task_id from _opn_desc_task)),
410
+ array[(select b_agent from _opn)],
411
+ 'OPN-32: a role selector the creator matches delivers the task to the other member only');
412
+
413
+ select is(
414
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
415
+ from agent_board.inbox_items
416
+ where source_message_id = (select message_id from _opn_direct)),
417
+ array[(select b_agent from _opn)],
418
+ 'OPN-33: a direct message delivers to exactly its recipient');
419
+
420
+ -- Round 2 of the PR #615 board: the creator lists itself AND targets a board it is on. Both loops
421
+ -- reach the same delivery row; the board loop must not demote the explicit one (which aborted the
422
+ -- whole task on inbox_items_visibility_shape). And an explicit self-mention nested in an
423
+ -- explicit_set, by handle, beside a group that contains the author.
424
+ select set_config('request.jwt.claim.role', 'authenticated', true);
425
+ select set_config('request.jwt.claim.sub', (select a_user::text from _opn), true);
426
+ set local role authenticated;
427
+
428
+ select lives_ok(
429
+ $$ insert into _opn_both_task
430
+ select * from agent_board.create_tasking_delivery(
431
+ (select thread from _opn), null, 'opn both task', 'the creator lists itself and its board',
432
+ array[(select a_agent from _opn)], (select board_ab from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
433
+ 'OPN-35: the creator can list itself and target a board it is on, in one task');
434
+
435
+ select lives_ok(
436
+ $$ insert into _opn_nested
437
+ select * from agent_board.send_multicast_message(
438
+ (select thread from _opn), null,
439
+ '[{"kind":"group_alias","groupAlias":"opn-team"},{"kind":"explicit_set","recipients":[{"kind":"agent_handle","handle":"opn-author"}]}]'::jsonb,
440
+ 'addressing_v1', 'opn nested', 'a group, and the author by handle inside an explicit set', 'comment', 'medium', null, '{}'::jsonb) $$,
441
+ 'OPN-36: the author can multicast to a group and name itself by handle inside an explicit_set');
442
+
443
+ -- Round 3 (correctness T1): the creator lists itself AND targets a board it is ALONE on, from an
444
+ -- unlinked thread. A fix that skipped the creator in the board loop would pass every assertion above
445
+ -- and leave this thread unlinked.
446
+ select lives_ok(
447
+ $$ insert into _opn_both_sole
448
+ select * from agent_board.create_tasking_delivery(
449
+ (select thread_unlinked2 from _opn), null, 'opn both sole', 'the creator lists itself; board of one',
450
+ array[(select a_agent from _opn)], (select board_s from _opn), 'medium', 'distributed', null, '{}'::jsonb) $$,
451
+ 'OPN-39: the creator can list itself and target a board it is alone on');
452
+
453
+ reset role;
454
+ select set_config('request.jwt.claim.role', '', true);
455
+ select set_config('request.jwt.claim.sub', '', true);
456
+
457
+ select is(
458
+ (select board_id from agent_board.threads where id = (select thread_unlinked2 from _opn)),
459
+ (select board_s from _opn),
460
+ 'OPN-40: that task still links its thread to the board');
461
+
462
+ select is(
463
+ (select array_agg(array[ii.visible_in_inbox::text, (ii.recipient_agent_id = (select a_agent from _opn))::text] order by ii.recipient_agent_id)
464
+ from agent_board.inbox_items ii
465
+ where ii.source_task_id = (select task_id from _opn_both_task)),
466
+ array[array['true', 'true'], array['true', 'false']],
467
+ 'OPN-37: an explicit self-listing survives a board fan-out the creator is in, and the other member is reached');
468
+
469
+ select is(
470
+ (select array_agg(recipient_agent_id order by recipient_agent_id)
471
+ from agent_board.inbox_items
472
+ where source_message_id = (select message_id from _opn_nested)
473
+ and visible_in_inbox),
474
+ array[(select a_agent from _opn), (select b_agent from _opn)],
475
+ 'OPN-38: an agent_handle naming the author inside an explicit_set is an explicit self-mention');
476
+
477
+ -- The descriptor class, from the catalog: every function that resolves recipient descriptors does so
478
+ -- through _resolve_addressing_recipient_rows, and the only callers of it are the resolver itself
479
+ -- (explicit_set recursion), the read-only resolve_recipients, and _addressing_agent_ids_or_raise --
480
+ -- the one delivery path, where the #608 rule lives. A new caller that delivers fails this by name.
481
+ select is(
482
+ (select array_agg(p.proname::text order by p.proname)
483
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
484
+ where n.nspname = 'agent_board'
485
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_resolve_addressing_recipient_rows')),
486
+ array['_addressing_agent_ids_or_raise', '_resolve_addressing_recipient_rows', 'resolve_recipients'],
487
+ 'OPN-34: descriptor expansion reaches a delivery only through _addressing_agent_ids_or_raise');
488
+
489
+ select * from finish();
490
+ rollback;
@@ -323,8 +323,8 @@ select *
323
323
 
324
324
  select is(
325
325
  (select delivered_recipient_count from v2_board_delivery),
326
- 3,
327
- 'post_board_message only reaches active joined or subscribed board members (incl. the seeded owner-creator)'
326
+ 2,
327
+ 'post_board_message only reaches active joined or subscribed board members, not the author (#608: the author is the seeded owner-creator)'
328
328
  );
329
329
 
330
330
  select is(
@@ -369,8 +369,8 @@ select *
369
369
 
370
370
  select is(
371
371
  (select delivered_recipient_count from v2_tasking_delivery),
372
- 4,
373
- 'tasking counts explicit and board fan-out attempts before duplicate collapse (incl. the seeded owner-creator)'
372
+ 3,
373
+ 'tasking counts explicit and board fan-out attempts before duplicate collapse, not the creator (#608: the creator is the seeded owner-creator)'
374
374
  );
375
375
 
376
376
  select is(
@@ -343,6 +343,15 @@
343
343
  "authenticated": false
344
344
  }
345
345
  },
346
+ {
347
+ "name": "_delegate_instances_owner_identity_immutable",
348
+ "signature": "_delegate_instances_owner_identity_immutable()",
349
+ "executable_by": {
350
+ "anon": false,
351
+ "service_role": false,
352
+ "authenticated": false
353
+ }
354
+ },
346
355
  {
347
356
  "name": "_delegate_principal_agent_id",
348
357
  "signature": "_delegate_principal_agent_id(p_agent_id uuid)",
@@ -153,6 +153,10 @@
153
153
  "signature": "_delegate_instance_slug(p_issuer text, p_instance_id text)",
154
154
  "declares_p_metadata": false
155
155
  },
156
+ {
157
+ "signature": "_delegate_instances_owner_identity_immutable()",
158
+ "declares_p_metadata": false
159
+ },
156
160
  {
157
161
  "signature": "_delegate_principal_agent_id(p_agent_id uuid)",
158
162
  "declares_p_metadata": false
@@ -0,0 +1,105 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: o_begin o_add t_begin t_tomb o_commit t_commit check
4
+ step o_begin:
5
+ BEGIN;
6
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
7
+ (select auth_user_id from agent_board.agents
8
+ where handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner')))::text, true) is not null as owner_session;
9
+
10
+ owner_session
11
+ -------------
12
+ t
13
+ (1 row)
14
+
15
+ step o_add:
16
+ select membership_state from agent_board.add_delegate_to_board(
17
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_at_instance_00001'),
18
+ '16591300-0000-4000-8000-000000000001');
19
+
20
+ membership_state
21
+ ----------------
22
+ joined
23
+ (1 row)
24
+
25
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
26
+ step t_tomb:
27
+ select first from agent_board.tombstone_delegate_instance(
28
+ 'iso_at_instance_00001', 'https://iso-at.test', 'iso-at-owner', 'owner_revoked');
29
+ <waiting ...>
30
+ step o_commit: COMMIT;
31
+ step t_tomb: <... completed>
32
+ first
33
+ -----
34
+ t
35
+ (1 row)
36
+
37
+ step t_commit: COMMIT;
38
+ step check:
39
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
40
+ where di.instance_id = 'iso_at_instance_00001') as tombstoned,
41
+ (select a.status from agent_board.agents a
42
+ join agent_board.delegate_instances di on di.agent_id = a.id
43
+ where di.instance_id = 'iso_at_instance_00001') as delegate_status,
44
+ (select string_agg(bm.membership_state, ',' order by bm.created_at, bm.id) from agent_board.board_memberships bm
45
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
46
+ where di.instance_id = 'iso_at_instance_00001') as delegate_memberships,
47
+ (select count(*) from agent_board.board_memberships bm
48
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
49
+ where di.tombstoned_at is not null and bm.membership_state in ('joined', 'subscribed')) as tombstoned_and_active;
50
+
51
+ tombstoned|delegate_status|delegate_memberships|tombstoned_and_active
52
+ ----------+---------------+--------------------+---------------------
53
+ true |revoked |left | 0
54
+ (1 row)
55
+
56
+
57
+ starting permutation: t_begin t_tomb o_begin o_add t_commit o_commit check
58
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
59
+ step t_tomb:
60
+ select first from agent_board.tombstone_delegate_instance(
61
+ 'iso_at_instance_00001', 'https://iso-at.test', 'iso-at-owner', 'owner_revoked');
62
+
63
+ first
64
+ -----
65
+ t
66
+ (1 row)
67
+
68
+ step o_begin:
69
+ BEGIN;
70
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
71
+ (select auth_user_id from agent_board.agents
72
+ where handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner')))::text, true) is not null as owner_session;
73
+
74
+ owner_session
75
+ -------------
76
+ t
77
+ (1 row)
78
+
79
+ step o_add:
80
+ select membership_state from agent_board.add_delegate_to_board(
81
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_at_instance_00001'),
82
+ '16591300-0000-4000-8000-000000000001');
83
+ <waiting ...>
84
+ step t_commit: COMMIT;
85
+ step o_add: <... completed>
86
+ ERROR: the hosted delegate instance is not live
87
+ step o_commit: COMMIT;
88
+ step check:
89
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
90
+ where di.instance_id = 'iso_at_instance_00001') as tombstoned,
91
+ (select a.status from agent_board.agents a
92
+ join agent_board.delegate_instances di on di.agent_id = a.id
93
+ where di.instance_id = 'iso_at_instance_00001') as delegate_status,
94
+ (select string_agg(bm.membership_state, ',' order by bm.created_at, bm.id) from agent_board.board_memberships bm
95
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
96
+ where di.instance_id = 'iso_at_instance_00001') as delegate_memberships,
97
+ (select count(*) from agent_board.board_memberships bm
98
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
99
+ where di.tombstoned_at is not null and bm.membership_state in ('joined', 'subscribed')) as tombstoned_and_active;
100
+
101
+ tombstoned|delegate_status|delegate_memberships|tombstoned_and_active
102
+ ----------+---------------+--------------------+---------------------
103
+ true |revoked | | 0
104
+ (1 row)
105
+