@consiliency/agent-board-schema 1.63.0 → 1.68.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.
Files changed (63) hide show
  1. package/README.md +25 -0
  2. package/fixtures/already-applied.json +20 -0
  3. package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
  4. package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
  5. package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
  6. package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
  7. package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
  8. package/package.json +1 -1
  9. package/schema-manifest.json +48 -3
  10. package/src/deployment-drift-main.test.mjs +156 -0
  11. package/src/deployment-drift-script.test.mjs +14 -1
  12. package/src/policy-comment.test.mjs +17 -0
  13. package/src/tooling.test.mjs +10 -2
  14. package/tests/agent_board_atomic_fleet_onboarding.test.sql +3 -1
  15. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
  16. package/tests/agent_board_consent_gating.test.sql +4 -4
  17. package/tests/agent_board_fk_lock_edges.test.sql +3 -2
  18. package/tests/agent_board_gatenull_membstate.test.sql +7 -4
  19. package/tests/agent_board_grant_target_role.test.sql +4 -4
  20. package/tests/agent_board_hosted_delegates.test.sql +5 -3
  21. package/tests/agent_board_invite_no_downgrade.test.sql +5 -3
  22. package/tests/agent_board_ledger_authority.test.sql +7 -0
  23. package/tests/agent_board_policy_correlation.test.sql +189 -0
  24. package/tests/agent_board_request_removed.test.sql +96 -0
  25. package/tests/agent_board_resolution_authority.test.sql +190 -0
  26. package/tests/agent_board_role_guard.test.sql +319 -0
  27. package/tests/agent_board_rolereset.test.sql +17 -27
  28. package/tests/agent_board_roomdeleg.test.sql +8 -0
  29. package/tests/agent_board_roomown.test.sql +3 -1
  30. package/tests/agent_board_rowid.test.sql +4 -1
  31. package/tests/agent_board_session_provenance.test.sql +106 -0
  32. package/tests/catalog/function_execute_privileges.json +11 -2
  33. package/tests/catalog/p_metadata_parameters.json +4 -0
  34. package/tests/isolation/expected/consent_approval_lock.out +1 -1
  35. package/tests/isolation/expected/last_owner_departure.out +562 -0
  36. package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
  37. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
  38. package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
  39. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
  40. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
  41. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
  42. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
  43. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
  44. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
  45. package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
  46. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
  47. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
  48. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
  49. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
  50. package/tests/isolation/specs/consent_approval_lock.spec +13 -8
  51. package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
  52. package/tests/isolation/specs/grant_target_role_race.spec +0 -1
  53. package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
  54. package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
  55. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
  56. package/tests/isolation/specs/last_owner_departure.spec +137 -0
  57. package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
  58. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
  59. package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
  60. package/tests/isolation/specs/session_identity_links.spec +3 -1
  61. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +0 -1
  62. package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
  63. package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
@@ -36,6 +36,7 @@ declare
36
36
  v_requester uuid := gen_random_uuid();
37
37
  v_nomember uuid := gen_random_uuid();
38
38
  v_suspended uuid := gen_random_uuid();
39
+ v_coowner uuid := gen_random_uuid();
39
40
  v_board uuid;
40
41
  v_slug text := 'roomdeleg-board-' || v_suffix;
41
42
  begin
@@ -54,6 +55,9 @@ begin
54
55
  (v_requester, 'agent', 'rd-requester-' || v_suffix, 'RD Requester', 'active', null, null),
55
56
  (v_nomember, 'agent', 'rd-nomember-' || v_suffix, 'RD NoMember', 'active', null, null);
56
57
 
58
+ insert into agent_board.agents(id, agent_type, handle, display_name, status)
59
+ values (v_coowner, 'agent', 'rd-coowner-' || v_suffix, 'RD Coowner', 'active');
60
+
57
61
  -- Alice creates board (service_role null-actor here -> supplied arg honored):
58
62
  -- create_board seeds Alice's joined owner membership (ROOMOWN).
59
63
  select board_id into v_board
@@ -66,6 +70,9 @@ begin
66
70
  (v_board, v_bob, 'joined', v_alice, 'member'),
67
71
  (v_board, v_requester, 'requested', v_alice, 'member');
68
72
 
73
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
74
+ values (v_board, v_coowner, 'joined', v_alice, 'owner');
75
+
69
76
  insert into _roomdeleg_fixture values
70
77
  (v_board, v_alice, v_bob, v_target, v_requester, v_nomember, v_suspended,
71
78
  v_alice_user, v_bob_user, v_suspended_user);
@@ -127,6 +134,7 @@ select lives_ok(
127
134
 
128
135
  -- Demote the creator to member (bare statement, not an assertion). created_by is
129
136
  -- immutable, so this models a creator who has lost owner authority.
137
+ select agent_board._lock_board_authority_ceiling(board) from _roomdeleg_fixture;
130
138
  update agent_board.board_memberships bm
131
139
  set role = 'member'
132
140
  from _roomdeleg_fixture f
@@ -73,7 +73,9 @@ begin
73
73
  insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
74
74
  values
75
75
  (v_board, v_member, 'joined', v_alice, 'member'),
76
- (v_board, v_grantee, 'joined', v_alice, 'observer');
76
+ (v_board, v_grantee, 'joined', v_alice, 'observer'),
77
+ -- Keep another owner for the creator self-demotion / slug-collision control.
78
+ (v_board, v_b2owner, 'joined', v_alice, 'owner');
77
79
 
78
80
  -- Board B2: an owner-LESS board (direct insert, no membership) for the bootstrap sweep.
79
81
  insert into agent_board.boards(id, slug, name, description, created_by_agent_id, visibility)
@@ -323,6 +323,9 @@ begin
323
323
  v_left_creator, 'rowid-bootstrap-left-' || s, 'ROWID Bootstrap Left',
324
324
  'leave-again recovery', '{}'::jsonb, 'open'
325
325
  );
326
+ -- ROLEGUARD: this creator may depart while another active owner remains.
327
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
328
+ values (v_left_board, v_writer_owner, 'joined', 'owner');
326
329
 
327
330
  -- Writer 1: a genuine create_board insert.
328
331
  select board_id into v_writer_board
@@ -660,7 +663,7 @@ select is(
660
663
  on bm.board_id = f.left_board and bm.agent_id = f.left_creator
661
664
  where bm.membership_state in ('joined', 'subscribed')),
662
665
  'member',
663
- 'left recovery: ROLERESET rejoin clamps the sole creator to member');
666
+ 'left recovery: ROLERESET rejoin clamps the departing creator to member');
664
667
  select lives_ok(
665
668
  (select format(
666
669
  $q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'left')$q$,
@@ -0,0 +1,106 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+ begin;
4
+ select plan(32);
5
+
6
+ create function pg_temp.a(n integer) returns uuid language sql immutable as $$
7
+ select ('66500000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
8
+ $$;
9
+ create function pg_temp.claim(n integer) returns void language plpgsql as $$
10
+ begin
11
+ perform set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub', pg_temp.a(n))::text, true);
12
+ end
13
+ $$;
14
+ insert into auth.users(id, instance_id, aud, role, email)
15
+ select pg_temp.a(n), '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'prov665-' || n || '@test.invalid'
16
+ from generate_series(1, 15) n;
17
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
18
+ select pg_temp.a(n), 'agent', 'prov665-' || n, 'Provenance ' || n, 'active', pg_temp.a(n)
19
+ from generate_series(1, 15) n;
20
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility)
21
+ values (pg_temp.a(100), 'prov665', 'Provenance', pg_temp.a(1), 'open');
22
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
23
+ values (pg_temp.a(100), pg_temp.a(1), 'joined', 'owner', pg_temp.a(1)),
24
+ (pg_temp.a(100), pg_temp.a(5), 'requested', 'member', pg_temp.a(5)),
25
+ (pg_temp.a(100), pg_temp.a(6), 'requested', 'member', pg_temp.a(6)),
26
+ (pg_temp.a(100), pg_temp.a(6), 'joined', 'member', pg_temp.a(6)),
27
+ (pg_temp.a(100), pg_temp.a(10), 'requested', 'member', pg_temp.a(10)),
28
+ (pg_temp.a(100), pg_temp.a(15), 'requested', 'member', pg_temp.a(15)),
29
+ (pg_temp.a(100), pg_temp.a(15), 'joined', 'member', pg_temp.a(15));
30
+ -- Subject-authored requests establish the consent an approval needs (#82).
31
+ do $$ declare a integer; begin
32
+ for a in 13..14 loop
33
+ perform pg_temp.claim(a);
34
+ perform agent_board.request_join_board(pg_temp.a(100), pg_temp.a(a));
35
+ end loop;
36
+ end $$;
37
+ create temp table active_before as
38
+ select id, created_by_agent_id from agent_board.board_memberships
39
+ where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'joined';
40
+
41
+ -- The population is catalog-derived. No exemption list or hard-coded RPC scan.
42
+ create temp table provenance_sites as
43
+ select p.oid, p.oid::regprocedure::text as signature,
44
+ agent_board_test.stripped_body(p.oid) as body
45
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
46
+ where n.nspname = 'agent_board'
47
+ and 'p_created_by_agent_id' = any(p.proargnames)
48
+ and has_function_privilege('authenticated', p.oid, 'EXECUTE')
49
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('agent_board._upsert_board_membership_state');
50
+ select is((select string_agg(p.proname::text, ',' order by p.proname) from provenance_sites s join pg_proc p on p.oid = s.oid),
51
+ 'invite_board_member,join_board,resolve_board_request', 'PROV-1 membership provenance class population');
52
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites where body ~ agent_board_test.ident_re('p_created_by_agent_id')),
53
+ '', 'PROV-2 no membership RPC consumes caller-asserted provenance; offenders named');
54
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites
55
+ where has_function_privilege('anon', oid, 'EXECUTE') or not has_function_privilege('service_role', oid, 'EXECUTE')),
56
+ '', 'PROV-3 membership provenance class has restricted ACLs');
57
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites where obj_description(oid, 'pg_proc') is null),
58
+ '', 'PROV-4 membership provenance class documents attribution');
59
+
60
+ select pg_temp.claim(1);
61
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(3), pg_temp.a(2))$$, 'PROV-5 invite accepts compatibility attribution');
62
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(3)), pg_temp.a(1), 'PROV-6 invite attribution is session actor');
63
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(4), null)$$, 'PROV-7 invite accepts NULL attribution');
64
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(4)), pg_temp.a(1), 'PROV-8 NULL invite attribution is session actor');
65
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(5), 'removed', pg_temp.a(2))$$, 'PROV-9 resolution accepts compatibility attribution');
66
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(5) and membership_state = 'left'), pg_temp.a(1), 'PROV-10 resolution attribution is session actor');
67
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(6), 'joined', pg_temp.a(2))$$, 'PROV-11 redundant pending retirement succeeds');
68
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(6) and membership_state = 'left'), pg_temp.a(1), 'PROV-12 retirement attribution is session actor');
69
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(6) and membership_state = 'joined'), pg_temp.a(6), 'PROV-13 no-op preserves active row attribution');
70
+
71
+ select pg_temp.claim(7);
72
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(7), 'joined', pg_temp.a(2))$$, 'PROV-14 self-join accepts compatibility attribution');
73
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(7)), pg_temp.a(7), 'PROV-15 join attribution is session actor');
74
+ select pg_temp.claim(8);
75
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(8), 'joined', null)$$, 'PROV-16 self-join accepts NULL attribution');
76
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(8)), pg_temp.a(8), 'PROV-17 NULL join attribution is session actor');
77
+
78
+ select set_config('request.jwt.claims', '{"role":"service_role"}', true);
79
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(9), pg_temp.a(2))$$, 'PROV-18 unresolved service invitation succeeds');
80
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(9)), null::uuid, 'PROV-19 service invitation does not invent agent attribution');
81
+ -- #668 (owner-confirmed 2026-10-26): resolution requires a RESOLVED same-board owner/admin, so an
82
+ -- unresolved service-role session is refused and the pending row keeps its state and provenance.
83
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(10), 'removed', pg_temp.a(2))$$, '42501', null, 'PROV-20 unresolved service resolution refused');
84
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(10)),
85
+ 'requested:' || pg_temp.a(10)::text, 'PROV-21 refused service resolution leaves the pending row and its provenance unchanged');
86
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(11), 'joined', pg_temp.a(2))$$, 'PROV-22 unresolved service join succeeds');
87
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(11)), null::uuid, 'PROV-23 service join does not invent agent attribution');
88
+ select set_config('request.jwt.claims', '{"role":"authenticated"}', true);
89
+ select throws_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(12), pg_temp.a(1))$$, '42501', null, 'PROV-24 unresolved authenticated invite denied');
90
+ select is((select count(*)::integer from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(12)), 0, 'PROV-25 denied invitation writes nothing');
91
+
92
+ -- A RESOLVED service-role owner session: attribution is that session's agent, never the forged
93
+ -- compatibility argument (pg_temp.a(2) is not even a member).
94
+ select set_config('request.jwt.claims', json_build_object('role', 'service_role', 'sub', pg_temp.a(1))::text, true);
95
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(13), 'joined', pg_temp.a(2))$$, 'PROV-26 resolved service owner approves with forged attribution');
96
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(13)),
97
+ 'joined:' || pg_temp.a(1)::text, 'PROV-27 service owner approval attribution is the session actor');
98
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(14), 'removed', pg_temp.a(2))$$, 'PROV-28 resolved service owner rejects with forged attribution');
99
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(14)),
100
+ 'left:' || pg_temp.a(1)::text, 'PROV-29 service owner rejection attribution is the session actor');
101
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(15), 'joined', pg_temp.a(2))$$, 'PROV-30 resolved service owner no-op retires the redundant request');
102
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'left'), pg_temp.a(1), 'PROV-31 service owner retirement attribution is the session actor');
103
+ select is((select array_agg(row(id, created_by_agent_id)::text) from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'joined'),
104
+ (select array_agg(row(id, created_by_agent_id)::text) from active_before), 'PROV-32 no-op keeps the active row identity and attribution');
105
+ select * from finish();
106
+ rollback;
@@ -109,6 +109,15 @@
109
109
  "authenticated": true
110
110
  }
111
111
  },
112
+ {
113
+ "name": "_board_memberships_last_owner_guard",
114
+ "signature": "_board_memberships_last_owner_guard()",
115
+ "executable_by": {
116
+ "anon": false,
117
+ "service_role": false,
118
+ "authenticated": false
119
+ }
120
+ },
112
121
  {
113
122
  "name": "_build_fetch_descriptor",
114
123
  "signature": "_build_fetch_descriptor(p_surface text, p_target_id uuid, p_view text, p_reason text, p_filters jsonb)",
@@ -286,7 +295,7 @@
286
295
  "executable_by": {
287
296
  "anon": false,
288
297
  "service_role": true,
289
- "authenticated": false
298
+ "authenticated": true
290
299
  }
291
300
  },
292
301
  {
@@ -322,7 +331,7 @@
322
331
  "executable_by": {
323
332
  "anon": false,
324
333
  "service_role": true,
325
- "authenticated": false
334
+ "authenticated": true
326
335
  }
327
336
  },
328
337
  {
@@ -49,6 +49,10 @@
49
49
  "signature": "_board_membership_events_provenance_guard()",
50
50
  "declares_p_metadata": false
51
51
  },
52
+ {
53
+ "signature": "_board_memberships_last_owner_guard()",
54
+ "declares_p_metadata": false
55
+ },
52
56
  {
53
57
  "signature": "_build_fetch_descriptor(p_surface text, p_target_id uuid, p_view text, p_reason text, p_filters jsonb)",
54
58
  "declares_p_metadata": false
@@ -1,7 +1,7 @@
1
1
  Parsed test spec with 3 sessions
2
2
 
3
3
  starting permutation: a_begin a_resolve w_begin w_leave a_commit w_commit chk
4
- step a_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
4
+ step a_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '115d0000-0000-4000-8000-000000000001';
5
5
  step a_resolve:
6
6
  select membership_state
7
7
  from agent_board.resolve_board_request(