@consiliency/agent-board-schema 1.10.0 → 1.22.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/fixtures/already-applied.json +136 -0
- package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
- package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
- package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +309 -3
- package/src/tooling.test.mjs +123 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_gatenull_membstate.test.sql +352 -0
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
begin;
|
|
2
2
|
|
|
3
|
-
select plan(
|
|
3
|
+
select plan(51);
|
|
4
|
+
|
|
5
|
+
-- The 32 behavioral assertions below run inside a `do` block. pgTAP assertions
|
|
6
|
+
-- must NOT be called via `perform` there: `perform is(...)` RUNS the assertion
|
|
7
|
+
-- (incrementing the plan counter) but DISCARDS the returned TAP line, so a
|
|
8
|
+
-- failure prints no `not ok` and ships green (message-board#87). Buffer each line
|
|
9
|
+
-- into this temp table and emit them after the block.
|
|
10
|
+
create temp table _tap (seq serial primary key, line text) on commit drop;
|
|
4
11
|
|
|
5
12
|
select has_function(
|
|
6
13
|
'agent_board',
|
|
@@ -231,38 +238,38 @@ begin
|
|
|
231
238
|
'private'
|
|
232
239
|
);
|
|
233
240
|
v_board := v_result.board_id;
|
|
234
|
-
|
|
241
|
+
insert into _tap(line) select ok(v_board is not null, 'create_board returns a board id');
|
|
235
242
|
|
|
236
243
|
select * into v_result
|
|
237
244
|
from agent_board.join_board(v_board, v_board_member, 'subscribed', v_sender, '{}'::jsonb);
|
|
238
|
-
|
|
245
|
+
insert into _tap(line) select is(v_result.membership_state, 'subscribed', 'join_board supports subscribed state');
|
|
239
246
|
|
|
240
247
|
select * into v_result
|
|
241
248
|
from agent_board.join_board(v_board, v_direct_recipient, 'joined', v_sender, '{}'::jsonb);
|
|
242
|
-
|
|
249
|
+
insert into _tap(line) select is(v_result.membership_state, 'joined', 'join_board supports joined state');
|
|
243
250
|
|
|
244
251
|
select * into v_result
|
|
245
252
|
from agent_board.join_board(v_board, v_left_member, 'joined', v_sender, '{}'::jsonb);
|
|
246
253
|
select * into v_result
|
|
247
254
|
from agent_board.leave_board(v_board, v_left_member, '{}'::jsonb);
|
|
248
|
-
|
|
255
|
+
insert into _tap(line) select is(v_result.membership_state, 'left', 'leave_board marks memberships as left');
|
|
249
256
|
|
|
250
257
|
select * into v_result
|
|
251
258
|
from agent_board.register_callback_endpoint(v_direct_recipient, 'https://example.test/direct', '{}'::jsonb);
|
|
252
259
|
v_direct_endpoint := v_result.endpoint_id;
|
|
253
|
-
|
|
260
|
+
insert into _tap(line) select is(v_result.endpoint_status, 'pending_verification', 'register_callback_endpoint starts pending verification');
|
|
254
261
|
|
|
255
262
|
select * into v_result
|
|
256
263
|
from agent_board.verify_callback_endpoint(v_direct_endpoint, v_direct_recipient, '{"controlProofRef":"inbox-rpcs/direct"}'::jsonb);
|
|
257
|
-
|
|
264
|
+
insert into _tap(line) select is(v_result.endpoint_status, 'verified', 'verify_callback_endpoint marks endpoint verified');
|
|
258
265
|
|
|
259
266
|
select * into v_result
|
|
260
267
|
from agent_board.subscribe_callback_endpoint(v_direct_endpoint, v_direct_recipient, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
|
|
261
|
-
|
|
268
|
+
insert into _tap(line) select is(v_result.subscription_status, 'active', 'own-inbox callback subscription activates');
|
|
262
269
|
|
|
263
270
|
select * into v_result
|
|
264
271
|
from agent_board.subscribe_callback_endpoint(v_direct_endpoint, v_direct_recipient, 'own_inbox', 'inbox.item.updated', null, '{}'::jsonb);
|
|
265
|
-
|
|
272
|
+
insert into _tap(line) select is(v_result.subscription_status, 'active', 'own-inbox update subscription activates');
|
|
266
273
|
|
|
267
274
|
select * into v_result
|
|
268
275
|
from agent_board.register_callback_endpoint(v_board_member, 'https://example.test/board', '{}'::jsonb);
|
|
@@ -271,7 +278,7 @@ begin
|
|
|
271
278
|
from agent_board.verify_callback_endpoint(v_board_endpoint, v_board_member, '{"controlProofRef":"inbox-rpcs/board"}'::jsonb);
|
|
272
279
|
select * into v_result
|
|
273
280
|
from agent_board.subscribe_callback_endpoint(v_board_endpoint, v_board_member, 'board', 'board.post.delivered', v_board, '{}'::jsonb);
|
|
274
|
-
|
|
281
|
+
insert into _tap(line) select is(v_result.subscription_status, 'active', 'board callback subscription activates for active member');
|
|
275
282
|
|
|
276
283
|
select * into v_result
|
|
277
284
|
from agent_board.register_callback_endpoint(v_outsider, 'https://example.test/outsider', '{}'::jsonb);
|
|
@@ -279,10 +286,10 @@ begin
|
|
|
279
286
|
|
|
280
287
|
begin
|
|
281
288
|
perform agent_board.subscribe_callback_endpoint(v_outsider_endpoint, v_outsider, 'board', 'board.post.delivered', v_board, '{}'::jsonb);
|
|
282
|
-
|
|
289
|
+
insert into _tap(line) select ok(false, 'board subscription without membership should fail');
|
|
283
290
|
exception
|
|
284
291
|
when sqlstate 'P0002' then
|
|
285
|
-
|
|
292
|
+
insert into _tap(line) select ok(true, 'board subscription requires active board membership');
|
|
286
293
|
end;
|
|
287
294
|
|
|
288
295
|
select * into v_result
|
|
@@ -299,13 +306,13 @@ begin
|
|
|
299
306
|
);
|
|
300
307
|
v_direct_message := v_result.message_id;
|
|
301
308
|
v_direct_inbox := v_result.inbox_item_id;
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
insert into _tap(line) select is(v_result.callback_outbox_count, 1, 'direct delivery enqueues one own-inbox callback');
|
|
310
|
+
insert into _tap(line) select is(
|
|
304
311
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_direct_message),
|
|
305
312
|
1,
|
|
306
313
|
'direct delivery writes one recipient row'
|
|
307
314
|
);
|
|
308
|
-
|
|
315
|
+
insert into _tap(line) select is(
|
|
309
316
|
(select count(*)::integer from agent_board.inbox_items where source_message_id = v_direct_message),
|
|
310
317
|
1,
|
|
311
318
|
'direct delivery writes one inbox row'
|
|
@@ -323,8 +330,8 @@ begin
|
|
|
323
330
|
'direct-key',
|
|
324
331
|
'{}'::jsonb
|
|
325
332
|
);
|
|
326
|
-
|
|
327
|
-
|
|
333
|
+
insert into _tap(line) select is(v_result.message_id, v_direct_message, 'direct idempotency reuses canonical message');
|
|
334
|
+
insert into _tap(line) select is(
|
|
328
335
|
(select count(*)::integer from agent_board.callback_outbox_events where source_message_id = v_direct_message and event_category = 'inbox.item.delivered'),
|
|
329
336
|
1,
|
|
330
337
|
'direct idempotency does not duplicate delivered callback events'
|
|
@@ -332,8 +339,8 @@ begin
|
|
|
332
339
|
|
|
333
340
|
select * into v_result
|
|
334
341
|
from agent_board.update_inbox_item_state(v_direct_inbox, v_direct_recipient, 'read', '{}'::jsonb);
|
|
335
|
-
|
|
336
|
-
|
|
342
|
+
insert into _tap(line) select is(v_result.action_state, 'read', 'update_inbox_item_state changes action state');
|
|
343
|
+
insert into _tap(line) select is(
|
|
337
344
|
(select count(*)::integer from agent_board.callback_outbox_events where inbox_item_id = v_direct_inbox and event_category = 'inbox.item.updated'),
|
|
338
345
|
1,
|
|
339
346
|
'updating inbox state enqueues one inbox.item.updated callback'
|
|
@@ -351,8 +358,8 @@ begin
|
|
|
351
358
|
'multicast-key',
|
|
352
359
|
'{}'::jsonb
|
|
353
360
|
);
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
insert into _tap(line) select is(v_result.delivered_recipient_count, 2, 'multicast collapses duplicate recipients');
|
|
362
|
+
insert into _tap(line) select is(
|
|
356
363
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_result.message_id),
|
|
357
364
|
2,
|
|
358
365
|
'multicast writes one recipient row per unique recipient'
|
|
@@ -371,13 +378,13 @@ begin
|
|
|
371
378
|
'{}'::jsonb
|
|
372
379
|
);
|
|
373
380
|
v_board_message := v_result.message_id;
|
|
374
|
-
|
|
375
|
-
|
|
381
|
+
insert into _tap(line) select is(v_result.delivered_recipient_count, 3, 'board delivery reaches joined and subscribed members only (incl. the seeded owner-creator)');
|
|
382
|
+
insert into _tap(line) select is(
|
|
376
383
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_board_message and recipient_agent_id = v_left_member),
|
|
377
384
|
0,
|
|
378
385
|
'left board members do not receive board delivery'
|
|
379
386
|
);
|
|
380
|
-
|
|
387
|
+
insert into _tap(line) select ok(
|
|
381
388
|
exists(
|
|
382
389
|
select 1
|
|
383
390
|
from agent_board.callback_outbox_events
|
|
@@ -402,13 +409,13 @@ begin
|
|
|
402
409
|
'{}'::jsonb
|
|
403
410
|
);
|
|
404
411
|
v_task := v_result.task_id;
|
|
405
|
-
|
|
406
|
-
|
|
412
|
+
insert into _tap(line) select is(v_result.delivered_recipient_count, 4, 'tasking counts explicit and board fan-out attempts (incl. the seeded owner-creator)');
|
|
413
|
+
insert into _tap(line) select is(
|
|
407
414
|
(select count(*)::integer from agent_board.delivery_recipients where source_task_id = v_task),
|
|
408
415
|
3,
|
|
409
416
|
'tasking suppresses duplicate recipient rows across explicit and board fan-out'
|
|
410
417
|
);
|
|
411
|
-
|
|
418
|
+
insert into _tap(line) select is(
|
|
412
419
|
(select count(*)::integer from agent_board.inbox_items where source_task_id = v_task),
|
|
413
420
|
3,
|
|
414
421
|
'tasking writes one inbox row per unique recipient'
|
|
@@ -416,13 +423,13 @@ begin
|
|
|
416
423
|
|
|
417
424
|
select * into v_result
|
|
418
425
|
from agent_board.rotate_callback_signing_key(v_direct_endpoint, v_direct_recipient, '{}'::jsonb);
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
insert into _tap(line) select is(v_result.signing_key_version, 2, 'rotate_callback_signing_key bumps signing key version');
|
|
427
|
+
insert into _tap(line) select is(v_result.endpoint_status, 'pending_verification', 'signing key rotation returns endpoint to pending verification');
|
|
421
428
|
|
|
422
429
|
select * into v_result
|
|
423
430
|
from agent_board.disable_callback_endpoint(v_board_endpoint, v_board_member, 'test disable', '{}'::jsonb);
|
|
424
|
-
|
|
425
|
-
|
|
431
|
+
insert into _tap(line) select is(v_result.endpoint_status, 'disabled', 'disable_callback_endpoint marks endpoint disabled');
|
|
432
|
+
insert into _tap(line) select ok(
|
|
426
433
|
exists(
|
|
427
434
|
select 1
|
|
428
435
|
from agent_board.callback_subscriptions
|
|
@@ -434,13 +441,13 @@ begin
|
|
|
434
441
|
|
|
435
442
|
select * into v_result
|
|
436
443
|
from agent_board.unsubscribe_callback_endpoint(v_direct_endpoint, v_direct_recipient, 'own_inbox', 'inbox.item.updated', null, '{}'::jsonb);
|
|
437
|
-
|
|
444
|
+
insert into _tap(line) select is(v_result.subscription_status, 'disabled', 'unsubscribe_callback_endpoint disables an active subscription');
|
|
438
445
|
|
|
439
446
|
select * into v_result
|
|
440
447
|
from agent_board.delete_callback_endpoint(v_outsider_endpoint, v_outsider);
|
|
441
|
-
|
|
448
|
+
insert into _tap(line) select ok(v_result.deleted, 'delete_callback_endpoint removes owned endpoints');
|
|
442
449
|
|
|
443
|
-
|
|
450
|
+
insert into _tap(line) select is(
|
|
444
451
|
(select count(*)::integer from agent_board.callback_delivery_attempts),
|
|
445
452
|
0,
|
|
446
453
|
'callback management remains metadata-only before runtime delivery exists'
|
|
@@ -448,5 +455,8 @@ begin
|
|
|
448
455
|
end;
|
|
449
456
|
$$;
|
|
450
457
|
|
|
458
|
+
-- Emit the buffered TAP lines (tests 20-51) that the `do` block produced.
|
|
459
|
+
select line from _tap order by seq;
|
|
460
|
+
|
|
451
461
|
select * from finish();
|
|
452
462
|
rollback;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
-- INVITEPOLICY (#90) pgTAP: a target can block a specific inviter on a specific board.
|
|
2
|
+
--
|
|
3
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
4
|
+
-- (''/service_role => true, 'authenticated' => false), request.jwt.claim.sub picks the
|
|
5
|
+
-- acting agent (so ACL grants do not gate these calls — behavior is under test, not ACLs).
|
|
6
|
+
--
|
|
7
|
+
-- The DENY assertion (blocked inviter's invite raises 42501) is the mutation pin: delete the
|
|
8
|
+
-- guard line in invite_board_member and it stops raising -> the test fails. The ALLOW twins
|
|
9
|
+
-- (un-blocked inviter, other inviter, service_role operator, unblock, and a merely-'left'
|
|
10
|
+
-- target) guard against over-blocking, including the REJECTSEM overload (a voluntary leaver
|
|
11
|
+
-- must stay re-invitable).
|
|
12
|
+
create extension if not exists pgtap with schema extensions;
|
|
13
|
+
set search_path = extensions, public, agent_board;
|
|
14
|
+
|
|
15
|
+
begin;
|
|
16
|
+
|
|
17
|
+
select plan(21);
|
|
18
|
+
|
|
19
|
+
create temp table _ip_fixture (
|
|
20
|
+
board uuid,
|
|
21
|
+
creator uuid, -- owner (seeded by create_board); acts as inviter1
|
|
22
|
+
inviter2 uuid, -- non-creator 'member'; a second, distinct inviter
|
|
23
|
+
target uuid, -- the invited agent that sets a block
|
|
24
|
+
target2 uuid, -- a different agent (actor-binding negative test)
|
|
25
|
+
leftguy uuid, -- a 'left' member (REJECTSEM re-invite guard)
|
|
26
|
+
creator_user uuid,
|
|
27
|
+
inviter2_user uuid,
|
|
28
|
+
target_user uuid,
|
|
29
|
+
target2_user uuid
|
|
30
|
+
) on commit drop;
|
|
31
|
+
|
|
32
|
+
do $$
|
|
33
|
+
declare
|
|
34
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
35
|
+
v_creator_user uuid;
|
|
36
|
+
v_inviter2_user uuid;
|
|
37
|
+
v_target_user uuid;
|
|
38
|
+
v_target2_user uuid;
|
|
39
|
+
v_creator uuid := gen_random_uuid();
|
|
40
|
+
v_inviter2 uuid := gen_random_uuid();
|
|
41
|
+
v_target uuid := gen_random_uuid();
|
|
42
|
+
v_target2 uuid := gen_random_uuid();
|
|
43
|
+
v_leftguy uuid := gen_random_uuid();
|
|
44
|
+
v_board uuid;
|
|
45
|
+
begin
|
|
46
|
+
v_creator_user := public.ensure_github_user('ip-creator-' || v_suffix, 'ip-creator-' || v_suffix || '@example.test', 'ip-creator-' || v_suffix, 'IP Creator');
|
|
47
|
+
v_inviter2_user := public.ensure_github_user('ip-inviter2-' || v_suffix, 'ip-inviter2-' || v_suffix || '@example.test', 'ip-inviter2-' || v_suffix, 'IP Inviter2');
|
|
48
|
+
v_target_user := public.ensure_github_user('ip-target-' || v_suffix, 'ip-target-' || v_suffix || '@example.test', 'ip-target-' || v_suffix, 'IP Target');
|
|
49
|
+
v_target2_user := public.ensure_github_user('ip-target2-' || v_suffix, 'ip-target2-' || v_suffix || '@example.test', 'ip-target2-' || v_suffix, 'IP Target2');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
52
|
+
values
|
|
53
|
+
(v_creator, 'agent', 'ip-creator-' || v_suffix, 'IP Creator', 'active', v_creator_user, v_creator_user),
|
|
54
|
+
(v_inviter2, 'agent', 'ip-inviter2-' || v_suffix, 'IP Inviter2', 'active', v_inviter2_user, v_inviter2_user),
|
|
55
|
+
(v_target, 'agent', 'ip-target-' || v_suffix, 'IP Target', 'active', v_target_user, v_target_user),
|
|
56
|
+
(v_target2, 'agent', 'ip-target2-' || v_suffix, 'IP Target2', 'active', v_target2_user, v_target2_user),
|
|
57
|
+
(v_leftguy, 'agent', 'ip-leftguy-' || v_suffix, 'IP Leftguy', 'active', null, null);
|
|
58
|
+
|
|
59
|
+
-- Gate-OFF board (role_gate_enforced defaults false): creator bypasses; inviter2 needs an
|
|
60
|
+
-- active membership.
|
|
61
|
+
select board_id into v_board
|
|
62
|
+
from agent_board.create_board(v_creator, 'ip-board-' || v_suffix, 'IP Board', 'invite policy test', '{}'::jsonb, 'private');
|
|
63
|
+
|
|
64
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
65
|
+
values (v_board, v_inviter2, 'joined', v_creator, 'member');
|
|
66
|
+
-- a 'left' row requires left_at (constraint board_memberships_state_left_at).
|
|
67
|
+
-- leftguy: REJECTSEM re-invite guard. target2: has a row so update_board_membership reaches
|
|
68
|
+
-- the guard in the bypass-closed regression check below.
|
|
69
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role, left_at)
|
|
70
|
+
values (v_board, v_leftguy, 'left', v_creator, 'member', now()),
|
|
71
|
+
(v_board, v_target2, 'left', v_creator, 'member', now());
|
|
72
|
+
|
|
73
|
+
insert into _ip_fixture values
|
|
74
|
+
(v_board, v_creator, v_inviter2, v_target, v_target2, v_leftguy,
|
|
75
|
+
v_creator_user, v_inviter2_user, v_target_user, v_target2_user);
|
|
76
|
+
end
|
|
77
|
+
$$;
|
|
78
|
+
|
|
79
|
+
-- existence -----------------------------------------------------------------
|
|
80
|
+
select has_function('agent_board', 'block_board_inviter', array['uuid', 'uuid', 'uuid', 'jsonb'], 'block_board_inviter exists');
|
|
81
|
+
select has_function('agent_board', 'unblock_board_inviter', array['uuid', 'uuid', 'uuid'], 'unblock_board_inviter exists');
|
|
82
|
+
select has_function('agent_board', '_invite_permitted', array['uuid', 'uuid', 'uuid'], '_invite_permitted exists');
|
|
83
|
+
|
|
84
|
+
-- default permissive: no block row -> permitted -----------------------------
|
|
85
|
+
select is(
|
|
86
|
+
(select agent_board._invite_permitted(board, target, inviter2) from _ip_fixture),
|
|
87
|
+
true, '_invite_permitted default true with no block row');
|
|
88
|
+
|
|
89
|
+
-- baseline: inviter1 (creator) may invite the target ------------------------
|
|
90
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
91
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _ip_fixture)::text, true);
|
|
92
|
+
select lives_ok(
|
|
93
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
94
|
+
board, target, creator) from _ip_fixture),
|
|
95
|
+
'baseline: creator invites target (no block)');
|
|
96
|
+
|
|
97
|
+
-- target blocks inviter1 (creator) -----------------------------------------
|
|
98
|
+
select set_config('request.jwt.claim.sub', (select target_user from _ip_fixture)::text, true);
|
|
99
|
+
select isnt(
|
|
100
|
+
(select b.id from _ip_fixture f, agent_board.block_board_inviter(f.board, f.creator) b),
|
|
101
|
+
null, 'target blocks the creator inviter (row created)');
|
|
102
|
+
|
|
103
|
+
select is(
|
|
104
|
+
(select agent_board._invite_permitted(board, target, creator) from _ip_fixture),
|
|
105
|
+
false, '_invite_permitted false for the blocked (target, creator) pair');
|
|
106
|
+
|
|
107
|
+
-- THE FIX (mutation pin): the blocked inviter can no longer invite the target
|
|
108
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _ip_fixture)::text, true);
|
|
109
|
+
select throws_ok(
|
|
110
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
111
|
+
board, target, creator) from _ip_fixture),
|
|
112
|
+
'42501', 'target has blocked invitations from this inviter on this board',
|
|
113
|
+
'blocked inviter (creator) cannot invite the target');
|
|
114
|
+
|
|
115
|
+
-- operator carve-out: service_role invites despite the block ----------------
|
|
116
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
117
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
118
|
+
select lives_ok(
|
|
119
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
120
|
+
board, target, creator) from _ip_fixture),
|
|
121
|
+
'service_role operator invites the target despite the block (null-actor carve-out)');
|
|
122
|
+
|
|
123
|
+
-- per-inviter scope: a DIFFERENT, un-blocked inviter still succeeds ----------
|
|
124
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
125
|
+
select set_config('request.jwt.claim.sub', (select inviter2_user from _ip_fixture)::text, true);
|
|
126
|
+
select lives_ok(
|
|
127
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
128
|
+
board, target, inviter2) from _ip_fixture),
|
|
129
|
+
'a different (un-blocked) inviter still invites the target (per-inviter scope)');
|
|
130
|
+
|
|
131
|
+
-- actor-binding: a third party cannot block on the target''s behalf ---------
|
|
132
|
+
select set_config('request.jwt.claim.sub', (select target2_user from _ip_fixture)::text, true);
|
|
133
|
+
select throws_ok(
|
|
134
|
+
(select format($q$select agent_board.block_board_inviter(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
135
|
+
board, creator, target) from _ip_fixture),
|
|
136
|
+
'42501', null,
|
|
137
|
+
'a third party cannot block on another target''s behalf (actor bound to session)');
|
|
138
|
+
|
|
139
|
+
-- unblock lifts the block ---------------------------------------------------
|
|
140
|
+
select set_config('request.jwt.claim.sub', (select target_user from _ip_fixture)::text, true);
|
|
141
|
+
select is(
|
|
142
|
+
(select agent_board.unblock_board_inviter(board, creator) from _ip_fixture),
|
|
143
|
+
true, 'target unblocks the creator inviter (row removed)');
|
|
144
|
+
|
|
145
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _ip_fixture)::text, true);
|
|
146
|
+
select lives_ok(
|
|
147
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
148
|
+
board, target, creator) from _ip_fixture),
|
|
149
|
+
'the previously blocked inviter can invite again after unblock');
|
|
150
|
+
|
|
151
|
+
select set_config('request.jwt.claim.sub', (select target_user from _ip_fixture)::text, true);
|
|
152
|
+
select is(
|
|
153
|
+
(select agent_board.unblock_board_inviter(board, creator) from _ip_fixture),
|
|
154
|
+
false, 'unblock returns false when no block row exists');
|
|
155
|
+
|
|
156
|
+
-- REJECTSEM guard: a merely-''left'' target (no block) stays re-invitable ----
|
|
157
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _ip_fixture)::text, true);
|
|
158
|
+
select lives_ok(
|
|
159
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid)$q$,
|
|
160
|
+
board, leftguy, creator) from _ip_fixture),
|
|
161
|
+
'a voluntary leaver (state left, no block) is still re-invitable (REJECTSEM preserved)');
|
|
162
|
+
|
|
163
|
+
-- BYPASS CLOSED (#90 panel finding): update_board_membership must not re-seat an
|
|
164
|
+
-- inbound-disclosure membership ('invited'/'requested') for a target that blocked the actor.
|
|
165
|
+
select set_config('request.jwt.claim.sub', (select target2_user from _ip_fixture)::text, true);
|
|
166
|
+
select isnt(
|
|
167
|
+
(select b.id from _ip_fixture f, agent_board.block_board_inviter(f.board, f.creator) b),
|
|
168
|
+
null, 'target2 blocks the creator (setup for the update-path bypass check)');
|
|
169
|
+
|
|
170
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _ip_fixture)::text, true);
|
|
171
|
+
select throws_ok(
|
|
172
|
+
(select format($q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'invited')$q$,
|
|
173
|
+
board, target2) from _ip_fixture),
|
|
174
|
+
'42501', 'target has blocked invitations from this inviter on this board',
|
|
175
|
+
'update_board_membership cannot re-seat invited for a blocking target (bypass closed)');
|
|
176
|
+
|
|
177
|
+
select throws_ok(
|
|
178
|
+
(select format($q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'requested')$q$,
|
|
179
|
+
board, target2) from _ip_fixture),
|
|
180
|
+
'42501', 'target has blocked invitations from this inviter on this board',
|
|
181
|
+
'update_board_membership cannot re-seat requested for a blocking target either');
|
|
182
|
+
|
|
183
|
+
-- allow-twin: an UN-blocked target can still be re-seated via update_board_membership.
|
|
184
|
+
select lives_ok(
|
|
185
|
+
(select format($q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'invited')$q$,
|
|
186
|
+
board, leftguy) from _ip_fixture),
|
|
187
|
+
'update_board_membership still admits invited for an un-blocked target');
|
|
188
|
+
|
|
189
|
+
-- operator carve-out: the guard sits inside the authenticated branch, so service_role is unaffected.
|
|
190
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
191
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
192
|
+
select lives_ok(
|
|
193
|
+
(select format($q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'invited')$q$,
|
|
194
|
+
board, target2) from _ip_fixture),
|
|
195
|
+
'service_role may re-seat invited despite the block (operator carve-out)');
|
|
196
|
+
|
|
197
|
+
-- the new SECURITY DEFINER helper carries no PUBLIC EXECUTE (MBHARDEN #118) --
|
|
198
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
199
|
+
select ok(
|
|
200
|
+
not has_function_privilege('public', 'agent_board._invite_permitted(uuid,uuid,uuid)', 'EXECUTE'),
|
|
201
|
+
'_invite_permitted carries no PUBLIC EXECUTE (class invariant)');
|
|
202
|
+
|
|
203
|
+
select * from finish();
|
|
204
|
+
|
|
205
|
+
rollback;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
-- LEAVEIDEM (#81) pgTAP — terminal membership states are true no-ops; left_at and the reserved
|
|
2
|
+
-- ops_* metadata namespace are not caller-controlled.
|
|
3
|
+
--
|
|
4
|
+
-- HAND-COUNTED plan: 20 assertions (mechanically re-counted; an earlier draft was off by one,
|
|
5
|
+
-- which CI cannot catch because its grep does not see pgTAP plan diagnostics). Breakdown:
|
|
6
|
+
-- 3 controls | 4 LEAVEIDEM core/no-op | 2 #79 guards | 4 ops-metadata + self-promotion
|
|
7
|
+
-- | 2 left_at invariants (preserve + genuine restamp) | 1 oracle guard | 4 no-regression
|
|
8
|
+
--
|
|
9
|
+
-- METHODOLOGY (load-bearing, and two traps this suite exists to avoid):
|
|
10
|
+
-- (a) GUC-only pattern (claims set, session stays postgres) rather than `set local role
|
|
11
|
+
-- authenticated`, because `_is_service_role()` EXECUTE is revoked from `authenticated`
|
|
12
|
+
-- (20260512000000:603) so the control below could not run under a real SET ROLE. Both
|
|
13
|
+
-- vacuous-pass controls are asserted: `_is_service_role() = false` (else every assertion
|
|
14
|
+
-- silently runs the SERVICE branch) and a non-null `current_agent_id()` (else a fixture whose
|
|
15
|
+
-- sub resolves no active agent yields a green throws_ok('42501') from the GATENULL guard while
|
|
16
|
+
-- the tested behaviour is absent). Every throws_ok matches the MESSAGE, because 42501 is
|
|
17
|
+
-- raised by six other guards in this function family.
|
|
18
|
+
-- (b) `now()` is transaction_timestamp() and is CONSTANT for the whole suite, so a "was it
|
|
19
|
+
-- restamped?" assertion written as now-vs-now can never fail. Every left_at assertion below
|
|
20
|
+
-- therefore seeds a DISTINCT past value first and compares against that.
|
|
21
|
+
|
|
22
|
+
create extension if not exists pgtap with schema extensions;
|
|
23
|
+
set search_path = extensions, public, agent_board;
|
|
24
|
+
|
|
25
|
+
begin;
|
|
26
|
+
|
|
27
|
+
select plan(22);
|
|
28
|
+
|
|
29
|
+
create temp table _li (k text primary key, v text) on commit drop;
|
|
30
|
+
|
|
31
|
+
do $$
|
|
32
|
+
declare
|
|
33
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
34
|
+
v_o uuid; v_b uuid; v_a uuid; v_uid uuid; v_c uuid; v_cuid uuid;
|
|
35
|
+
begin
|
|
36
|
+
insert into agent_board.agents(agent_type, kind, handle, display_name, status)
|
|
37
|
+
values ('agent', 'agent'::agent_board.agent_kind, 'li-own-' || s, 'Owner', 'active') returning id into v_o;
|
|
38
|
+
insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
|
|
39
|
+
values ('li-b-' || s, 'LI Board', v_o, 'open', 'active') returning id into v_b;
|
|
40
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
41
|
+
values (v_b, v_o, 'owner', 'joined', v_o);
|
|
42
|
+
|
|
43
|
+
v_uid := gen_random_uuid();
|
|
44
|
+
insert into auth.users(id, instance_id, aud, role, email, created_at, updated_at)
|
|
45
|
+
values (v_uid, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
|
|
46
|
+
'li-' || s || '@example.test', now(), now());
|
|
47
|
+
insert into agent_board.agents(agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
48
|
+
values ('agent', 'agent'::agent_board.agent_kind, 'li-a-' || s, 'Member', 'active', v_uid, v_uid) returning id into v_a;
|
|
49
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
50
|
+
values (v_b, v_a, 'member', 'joined', v_o);
|
|
51
|
+
|
|
52
|
+
-- an ACTIVE authenticated agent with NO membership on this board (for the oracle assertion)
|
|
53
|
+
v_cuid := gen_random_uuid();
|
|
54
|
+
insert into auth.users(id, instance_id, aud, role, email, created_at, updated_at)
|
|
55
|
+
values (v_cuid, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
|
|
56
|
+
'li-c-' || s || '@example.test', now(), now());
|
|
57
|
+
insert into agent_board.agents(agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
58
|
+
values ('agent', 'agent'::agent_board.agent_kind, 'li-c-' || s, 'Outsider', 'active', v_cuid, v_cuid) returning id into v_c;
|
|
59
|
+
|
|
60
|
+
insert into _li values ('board', v_b::text), ('owner', v_o::text), ('agent', v_a::text),
|
|
61
|
+
('uid', v_uid::text), ('outsider_uid', v_cuid::text);
|
|
62
|
+
end
|
|
63
|
+
$$;
|
|
64
|
+
|
|
65
|
+
select set_config('request.jwt.claim.sub', (select v from _li where k = 'uid'), true);
|
|
66
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
67
|
+
|
|
68
|
+
-- 1,2 — vacuous-pass controls (see methodology (a))
|
|
69
|
+
select is(agent_board._is_service_role(), false, 'CONTROL: acting on the authenticated path, not service_role');
|
|
70
|
+
select isnt(agent_board.current_agent_id(), null, 'CONTROL: the actor resolves to an active agent');
|
|
71
|
+
|
|
72
|
+
-- 3,4 — #81: leave_board is idempotent again (assertion 4 FAILS on main with 42501)
|
|
73
|
+
select lives_ok(
|
|
74
|
+
$$ select agent_board.leave_board((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent')) $$,
|
|
75
|
+
'first leave_board succeeds');
|
|
76
|
+
select lives_ok(
|
|
77
|
+
$$ select agent_board.leave_board((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent')) $$,
|
|
78
|
+
'LEAVEIDEM: a second leave_board is idempotent (was 42501 on main)');
|
|
79
|
+
|
|
80
|
+
-- 5,6 — and it is a TRUE no-op: neither the seeded departure time nor updated_at moves.
|
|
81
|
+
-- Seed a DISTINCT past left_at first (methodology (b)) so a restamp would be detectable.
|
|
82
|
+
update agent_board.board_memberships
|
|
83
|
+
set left_at = timestamptz '2020-01-01 00:00:00+00', updated_at = timestamptz '2020-01-01 00:00:00+00'
|
|
84
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent');
|
|
85
|
+
select lives_ok(
|
|
86
|
+
$$ select agent_board.leave_board((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent')) $$,
|
|
87
|
+
'a further leave_board on the seeded row still succeeds');
|
|
88
|
+
select is(
|
|
89
|
+
(select left_at from agent_board.board_memberships
|
|
90
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
91
|
+
timestamptz '2020-01-01 00:00:00+00',
|
|
92
|
+
'LEAVEIDEM: the no-op does not touch the row at all (seeded left_at survives)');
|
|
93
|
+
|
|
94
|
+
-- 7,8,9,10,11 — no-regression + the reserved ops_* strip on a REAL transition
|
|
95
|
+
select lives_ok(
|
|
96
|
+
$$ select agent_board.join_board((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'joined') $$,
|
|
97
|
+
'NO-REGRESSION: an agent may rejoin an OPEN board after leaving');
|
|
98
|
+
select lives_ok(
|
|
99
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'),
|
|
100
|
+
'left', '{"ops_retained_at":"2000-01-01","note":"keepme"}'::jsonb) $$,
|
|
101
|
+
'a real transition accepts caller metadata');
|
|
102
|
+
select is(
|
|
103
|
+
(select metadata->>'ops_retained_at' from agent_board.board_memberships
|
|
104
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
105
|
+
null,
|
|
106
|
+
'reserved ops_* metadata from a caller is STRIPPED (retention skip key not writable; FAILS on main)');
|
|
107
|
+
select is(
|
|
108
|
+
(select metadata->>'note' from agent_board.board_memberships
|
|
109
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
110
|
+
'keepme',
|
|
111
|
+
'legitimate caller metadata is still merged (the strip is not over-broad)');
|
|
112
|
+
select throws_ok(
|
|
113
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'joined') $$,
|
|
114
|
+
'42501', 'self membership change not permitted; use the join/approval path',
|
|
115
|
+
'self promotion from a terminal state is still refused');
|
|
116
|
+
|
|
117
|
+
-- ---------------------------------------------------------------------------
|
|
118
|
+
-- The `removed` state on the AUTHENTICATED path — the behaviour this migration adds.
|
|
119
|
+
-- ---------------------------------------------------------------------------
|
|
120
|
+
reset role;
|
|
121
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
122
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
123
|
+
select lives_ok(
|
|
124
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'removed') $$,
|
|
125
|
+
'NO-REGRESSION: service_role may still evict on behalf of an operator');
|
|
126
|
+
|
|
127
|
+
-- seed a distinct past left_at on the now-`removed` row for the invariants that follow
|
|
128
|
+
update agent_board.board_memberships
|
|
129
|
+
set left_at = timestamptz '2019-06-01 00:00:00+00', updated_at = timestamptz '2019-06-01 00:00:00+00'
|
|
130
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent');
|
|
131
|
+
|
|
132
|
+
select set_config('request.jwt.claim.sub', (select v from _li where k = 'uid'), true);
|
|
133
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
134
|
+
|
|
135
|
+
-- 12,13 — #79 kick-marker guard intact for a REMOVED agent (neither transition is same-state)
|
|
136
|
+
select throws_ok(
|
|
137
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'left') $$,
|
|
138
|
+
'42501', 'self membership change not permitted; use the join/approval path',
|
|
139
|
+
'#79 INTACT: a removed agent cannot launder its kick marker to left');
|
|
140
|
+
select throws_ok(
|
|
141
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'joined') $$,
|
|
142
|
+
'42501', 'self membership change not permitted; use the join/approval path',
|
|
143
|
+
'#79 INTACT: a removed agent cannot self-readmit');
|
|
144
|
+
|
|
145
|
+
-- 14,15 — removed -> removed is a permitted TRUE no-op
|
|
146
|
+
select lives_ok(
|
|
147
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'removed') $$,
|
|
148
|
+
'LEAVEIDEM: removed -> removed is permitted (same-state terminal no-op)');
|
|
149
|
+
select is(
|
|
150
|
+
(select updated_at from agent_board.board_memberships
|
|
151
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
152
|
+
timestamptz '2019-06-01 00:00:00+00',
|
|
153
|
+
'LEAVEIDEM: the no-op does NOT rewrite the row (seeded updated_at survives)');
|
|
154
|
+
|
|
155
|
+
-- 16 — ORACLE GUARD: an unauthorized caller targeting an agent ALREADY in the no-op state must
|
|
156
|
+
-- still get 42501. This is the assertion that distinguishes placing the early return AFTER the
|
|
157
|
+
-- authorization block from placing it before.
|
|
158
|
+
select set_config('request.jwt.claim.sub', (select v from _li where k = 'outsider_uid'), true);
|
|
159
|
+
select throws_ok(
|
|
160
|
+
$$ select agent_board.update_board_membership((select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'removed') $$,
|
|
161
|
+
'42501', 'board membership change requires owner or admin role',
|
|
162
|
+
'the no-op path is NOT an oracle: an unauthorized caller targeting an already-removed agent still gets 42501');
|
|
163
|
+
|
|
164
|
+
-- 17,18,19 — service path: control, the mutator's left_at preservation, and a genuine restamp.
|
|
165
|
+
reset role;
|
|
166
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
167
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
168
|
+
select is(agent_board._is_service_role(), true, 'CONTROL: bare postgres is service_role-equivalent');
|
|
169
|
+
-- The operator is deliberately NOT short-circuited by the no-op (so its p_metadata is not dropped),
|
|
170
|
+
-- so it reaches the mutator — which must PRESERVE the seeded left_at on a same-state call.
|
|
171
|
+
-- This is the only assertion that exercises coalesce(); it FAILS on main, which restamps.
|
|
172
|
+
select lives_ok($$
|
|
173
|
+
select agent_board.update_board_membership(
|
|
174
|
+
(select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'removed') $$,
|
|
175
|
+
'operator reaches the mutator on a same-state call [call succeeds]');
|
|
176
|
+
select is((select left_at from agent_board.board_memberships
|
|
177
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
178
|
+
timestamptz '2019-06-01 00:00:00+00',
|
|
179
|
+
'mutator PRESERVES left_at when the terminal state is unchanged (retention cutoff not restampable)');
|
|
180
|
+
-- A GENUINE terminal->different-terminal transition must restamp: the kick time is the audit answer.
|
|
181
|
+
update agent_board.board_memberships
|
|
182
|
+
set membership_state = 'left', left_at = timestamptz '2019-06-01 00:00:00+00'
|
|
183
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent');
|
|
184
|
+
select lives_ok($$
|
|
185
|
+
select agent_board.update_board_membership(
|
|
186
|
+
(select v::uuid from _li where k='board'), (select v::uuid from _li where k='agent'), 'removed') $$,
|
|
187
|
+
'a genuine left -> removed kick [call succeeds]');
|
|
188
|
+
select isnt((select left_at from agent_board.board_memberships
|
|
189
|
+
where board_id = (select v::uuid from _li where k='board') and agent_id = (select v::uuid from _li where k='agent')),
|
|
190
|
+
timestamptz '2019-06-01 00:00:00+00',
|
|
191
|
+
'a genuine left -> removed kick RESTAMPS left_at (does not inherit the departure time)');
|
|
192
|
+
|
|
193
|
+
select * from finish();
|
|
194
|
+
rollback;
|