@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.
Files changed (109) hide show
  1. package/fixtures/already-applied.json +136 -0
  2. package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
  3. package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
  4. package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
  5. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  6. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  7. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  8. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  9. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  10. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  11. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  12. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  13. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  14. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  15. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  16. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  17. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  18. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  19. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  20. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  21. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  22. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  23. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  24. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  25. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  26. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  27. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  28. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  29. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  30. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  31. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  32. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  33. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  34. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  35. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  36. package/package.json +1 -1
  37. package/schema-manifest.json +309 -3
  38. package/src/tooling.test.mjs +123 -2
  39. package/tests/agent_board_actorspoof.test.sql +349 -0
  40. package/tests/agent_board_agentsdir.test.sql +154 -0
  41. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  42. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  43. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  44. package/tests/agent_board_claimkey.test.sql +229 -0
  45. package/tests/agent_board_consent_backfill.test.sql +48 -0
  46. package/tests/agent_board_consent_emission.test.sql +601 -0
  47. package/tests/agent_board_consent_gating.test.sql +489 -0
  48. package/tests/agent_board_consent_substrate.test.sql +720 -0
  49. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  50. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  51. package/tests/agent_board_delegation_policies.test.sql +107 -1
  52. package/tests/agent_board_fedsession.test.sql +6 -4
  53. package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
  54. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  55. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  56. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  57. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  58. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  59. package/tests/agent_board_fleet_policy.test.sql +2 -2
  60. package/tests/agent_board_gatenull_membstate.test.sql +352 -0
  61. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  62. package/tests/agent_board_identity_providers.test.sql +9 -0
  63. package/tests/agent_board_inbox_reads.test.sql +29 -12
  64. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  65. package/tests/agent_board_invitepolicy.test.sql +205 -0
  66. package/tests/agent_board_leaveidem.test.sql +194 -0
  67. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  68. package/tests/agent_board_mint_harden.test.sql +303 -0
  69. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  70. package/tests/agent_board_obsgate.test.sql +178 -0
  71. package/tests/agent_board_obsgate2.test.sql +153 -0
  72. package/tests/agent_board_obsgate3.test.sql +170 -0
  73. package/tests/agent_board_obsgate4.test.sql +196 -0
  74. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  75. package/tests/agent_board_pubexec_class.test.sql +53 -0
  76. package/tests/agent_board_rejectsem.test.sql +271 -0
  77. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  78. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  79. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  80. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  81. package/tests/agent_board_rolereset.test.sql +351 -0
  82. package/tests/agent_board_roomdeleg.test.sql +5 -3
  83. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  84. package/tests/agent_board_roomlink.test.sql +25 -9
  85. package/tests/agent_board_rowid.test.sql +1274 -0
  86. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  87. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  88. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  89. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  90. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  91. package/tests/agent_board_signing_store.test.sql +6 -6
  92. package/tests/fixtures/isolation-substrate.sql +93 -0
  93. package/tests/fixtures/test-prereqs.sql +33 -0
  94. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  95. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  96. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  97. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  98. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  99. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  100. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  101. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  102. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  103. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  104. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  105. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  106. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  107. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  108. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  109. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -0,0 +1,271 @@
1
+ -- MBCONSENT-B(i) pgTAP: REJECTSEM -- a refusal is not an eviction (message-board#89).
2
+ --
3
+ -- The defect, reproduced on main before the fix: a subject requests to join an OPEN board, the
4
+ -- owner rejects, and the subject can no longer join a board open to anyone. One refusal was a
5
+ -- permanent, self-unrecoverable bar.
6
+ --
7
+ -- These assertions drive the REAL RPCs end to end. A structural check that resolve writes
8
+ -- `left` would not prove the ban is lifted -- only re-joining afterwards does.
9
+ create extension if not exists pgtap with schema extensions;
10
+ set search_path = extensions, public, agent_board;
11
+
12
+ begin;
13
+
14
+ select plan(26);
15
+
16
+ create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
17
+ begin
18
+ perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
19
+ end $$;
20
+ -- Persisted-state reader (message-board#113): allow-side assertions read the RESULT from the
21
+ -- table via this getter, not from the RPC's return in a bare select -- so an over-refusing gate
22
+ -- surfaces as a NAMED `not ok` from the paired lives_ok and the file is not aborted.
23
+ create function pg_temp.state(b uuid, a uuid) returns text language sql as $$
24
+ select membership_state from agent_board.board_memberships
25
+ where board_id = b and agent_id = a
26
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
27
+ created_at desc, id desc
28
+ limit 1;
29
+ $$;
30
+
31
+ insert into auth.users(id, instance_id, aud, role, email)
32
+ select ('b0000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
33
+ '00000000-0000-0000-0000-000000000000','authenticated','authenticated','rs'||n||'@t.test'
34
+ from generate_series(1,4) n;
35
+
36
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
37
+ select ('b1000000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
38
+ 'agent','agent','rs-agent-'||n,'RS Agent '||n,'active',
39
+ ('b0000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
40
+ from generate_series(1,4) n;
41
+
42
+ -- An OPEN board: anyone may join, which is what makes the ban indefensible.
43
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
44
+ values ('b2000000-0000-4000-8000-000000000001','rs-open','RS Open',
45
+ 'b1000000-0000-4000-8000-000000000001','open','active');
46
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
47
+ values ('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000001',
48
+ 'owner','joined','b1000000-0000-4000-8000-000000000001');
49
+
50
+ -- ===========================================================================
51
+ -- 1. The reported defect: request -> reject -> re-join
52
+ -- ===========================================================================
53
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000002');
54
+ select lives_ok($$
55
+ select agent_board.request_join_board(
56
+ 'b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000002') $$,
57
+ 'setup: the subject requests to join [call succeeds]');
58
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000002'),
59
+ 'requested', 'setup: the subject requests to join');
60
+
61
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000001');
62
+ select lives_ok($$
63
+ select agent_board.resolve_board_request(
64
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000002'::uuid,
65
+ 'removed'::text, null::uuid, '{}'::jsonb) $$,
66
+ 'REJECTSEM: rejecting a REQUEST succeeds [call succeeds]');
67
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000002'),
68
+ 'left',
69
+ 'REJECTSEM: rejecting a REQUEST writes `left`, not `removed`');
70
+
71
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000002');
72
+ select lives_ok($$
73
+ select agent_board.join_board(
74
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000002'::uuid,
75
+ 'joined'::text, null::uuid, '{}'::jsonb) $$,
76
+ 'REJECTSEM: the subject CAN re-join afterwards [call succeeds]');
77
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000002'),
78
+ 'joined',
79
+ 'REJECTSEM: the subject CAN re-join afterwards -- the ban is lifted');
80
+
81
+ -- ===========================================================================
82
+ -- 2. The same trap on the INVITED origin, which #89 does not name
83
+ -- ===========================================================================
84
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000001');
85
+ select lives_ok($$
86
+ select agent_board.invite_board_member(
87
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000003'::uuid,
88
+ 'b1000000-0000-4000-8000-000000000001'::uuid) $$,
89
+ 'setup: the owner invites a third agent [call succeeds]');
90
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000003'),
91
+ 'invited', 'setup: the owner invites a third agent');
92
+
93
+ select lives_ok($$
94
+ select agent_board.resolve_board_request(
95
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000003'::uuid,
96
+ 'removed'::text, null::uuid, '{}'::jsonb) $$,
97
+ 'REJECTSEM: cancelling an INVITATION succeeds [call succeeds]');
98
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000003'),
99
+ 'left',
100
+ 'REJECTSEM: cancelling an INVITATION also writes `left` -- same trap, unnamed in the issue');
101
+
102
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000003');
103
+ select lives_ok($$
104
+ select agent_board.join_board(
105
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000003'::uuid,
106
+ 'joined'::text, null::uuid, '{}'::jsonb) $$,
107
+ 'REJECTSEM: a cancelled invitee can join too [call succeeds]');
108
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000003'),
109
+ 'joined',
110
+ 'REJECTSEM: a cancelled invitee can join too');
111
+
112
+ -- ===========================================================================
113
+ -- 2b. The rewrite holds across EVERY board visibility
114
+ -- ===========================================================================
115
+ -- The first version of this suite tested only an OPEN board. The refusal rewrite is a property
116
+ -- of resolve_board_request, not of visibility, and asserting it on one board type is the same
117
+ -- instance-for-class error that has cost this work several rounds.
118
+ --
119
+ -- SCOPE, verified before writing this: on `private` and `directory_hidden` a subject cannot
120
+ -- self-accept via join_board even as a FIRST-TIME invitee with NO prior refusal. That block is
121
+ -- PRE-EXISTING join_board behaviour, not a residue of this change, and is tracked separately.
122
+ -- So this asserts what this PR owns -- a refusal writes `left` on every board type -- and does
123
+ -- not claim re-entry where acceptance was never possible.
124
+ -- message-board#113: the per-visibility invite+refusal MUTATIONS run once, under lives_ok
125
+ -- below, so an over-refusing gate surfaces as a NAMED `not ok` instead of aborting the file
126
+ -- (the previous version ran resolve_board_request inside this helper, inside the is() value --
127
+ -- a raise there took the whole file down with no `not ok`, transitively via the helper). This
128
+ -- helper is now a PURE READ of the resulting state per board visibility.
129
+ create function pg_temp.refusal_state(p_visibility text) returns text language sql as $$
130
+ select bm.membership_state
131
+ from agent_board.board_memberships bm
132
+ join agent_board.boards b on b.id = bm.board_id
133
+ where b.slug = 'rs-vis-' || p_visibility
134
+ and bm.agent_id = 'b1000000-0000-4000-8000-000000000002'::uuid
135
+ limit 1;
136
+ $$;
137
+
138
+ select lives_ok($$
139
+ do $do$
140
+ declare v text; b uuid;
141
+ begin
142
+ foreach v in array array['open','invite_only','private','directory_hidden'] loop
143
+ b := gen_random_uuid();
144
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
145
+ values (b, 'rs-vis-' || v, 'RS ' || v,
146
+ 'b1000000-0000-4000-8000-000000000001'::uuid, v, 'active');
147
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
148
+ values (b, 'b1000000-0000-4000-8000-000000000001'::uuid, 'owner', 'joined',
149
+ 'b1000000-0000-4000-8000-000000000001'::uuid);
150
+ perform set_config('request.jwt.claims',
151
+ '{"role":"authenticated","sub":"b0000000-0000-0000-0000-000000000001"}', true);
152
+ perform agent_board.invite_board_member(b, 'b1000000-0000-4000-8000-000000000002'::uuid,
153
+ 'b1000000-0000-4000-8000-000000000001'::uuid);
154
+ perform agent_board.resolve_board_request(b, 'b1000000-0000-4000-8000-000000000002'::uuid,
155
+ 'removed'::text, null::uuid, '{}'::jsonb);
156
+ end loop;
157
+ end $do$;
158
+ $$, 'VISIBILITY setup: invite+refusal across every board type [call succeeds]');
159
+
160
+ select is(
161
+ (select string_agg(v || '=' || pg_temp.refusal_state(v), ' ' order by v)
162
+ from unnest(array['open','invite_only','private','directory_hidden']) v),
163
+ 'directory_hidden=left invite_only=left open=left private=left',
164
+ 'VISIBILITY: a refusal writes `left` on EVERY board type, not just open');
165
+
166
+ -- ===========================================================================
167
+ -- 3. The refusal is still DURABLE -- the state changed, the record did not vanish
168
+ -- ===========================================================================
169
+ -- Scoped to the ORIGINAL board. An agent-wide count is polluted by the per-visibility helper
170
+ -- above, which refuses the same subject on four further boards -- and that failure reads
171
+ -- exactly like a real duplicate-emission defect.
172
+ select is(
173
+ (select count(*)::int from agent_board.board_membership_events
174
+ where agent_id = 'b1000000-0000-4000-8000-000000000002'
175
+ and board_id = 'b2000000-0000-4000-8000-000000000001'::uuid
176
+ and result_state = 'left'),
177
+ 1,
178
+ 'DURABLE: the rejection is recorded in the ledger, not erased');
179
+
180
+ -- #110 CLOSED. This assertion previously pinned `eviction` as current behaviour and named it a
181
+ -- known gap, so that it would FAIL when the kind was threaded through the sink properly. It
182
+ -- did exactly that -- a gap that announces itself beats one that is only written down.
183
+ select is(
184
+ (select event_kind from agent_board.board_membership_events
185
+ where agent_id = 'b1000000-0000-4000-8000-000000000002'
186
+ and board_id = 'b2000000-0000-4000-8000-000000000001'::uuid
187
+ and result_state = 'left' limit 1),
188
+ 'reject_withdrawn',
189
+ 'DURABLE: the ledger records a REJECTION, not an eviction (#110 closed)');
190
+
191
+ select is(
192
+ (select provenance from agent_board.board_membership_events
193
+ where agent_id = 'b1000000-0000-4000-8000-000000000002'
194
+ and board_id = 'b2000000-0000-4000-8000-000000000001'::uuid
195
+ and result_state = 'left' limit 1),
196
+ 'board-authored',
197
+ 'DURABLE: ...as a board-authored act, not the subject withdrawing');
198
+
199
+ -- ===========================================================================
200
+ -- 4. ACCEPTANCE is untouched -- this changes rejection only
201
+ -- ===========================================================================
202
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000001');
203
+ select lives_ok($$
204
+ select agent_board.update_board_membership(
205
+ 'b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000002','removed')
206
+ $$, 'setup: an owner evicts through the eviction path');
207
+
208
+ select is(
209
+ (select membership_state from agent_board.board_memberships
210
+ where board_id = 'b2000000-0000-4000-8000-000000000001'
211
+ and agent_id = 'b1000000-0000-4000-8000-000000000002'
212
+ order by created_at desc limit 1),
213
+ 'removed',
214
+ 'SCOPE: update_board_membership still writes `removed` -- it IS the eviction path');
215
+
216
+ -- ...and a genuine eviction still bars self-re-entry. The fix must not have made eviction
217
+ -- toothless; only REFUSAL is recoverable.
218
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000002');
219
+ select throws_ok($$
220
+ select agent_board.join_board(
221
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000002'::uuid,
222
+ 'joined'::text, null::uuid, '{}'::jsonb)
223
+ $$, 'board is not joinable by this agent',
224
+ 'SCOPE: a genuine EVICTION still bars self-re-entry -- only refusal is recoverable');
225
+
226
+ -- ===========================================================================
227
+ -- 5. The RPC enum is unchanged: callers still reject with `removed`
228
+ -- ===========================================================================
229
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000001');
230
+ select throws_ok($$
231
+ select agent_board.resolve_board_request(
232
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000003'::uuid,
233
+ 'left'::text, null::uuid, '{}'::jsonb)
234
+ $$, '22023',
235
+ 'resolve_board_request only accepts joined, subscribed, or removed',
236
+ 'API: the enum is unchanged -- a client passing `left` is still refused');
237
+
238
+ -- ===========================================================================
239
+ -- 6. Approval still works, and resolve remains non-eviction by construction
240
+ -- ===========================================================================
241
+ -- Agent 4 is FRESH. Agent 3 is already joined by section 2, and request_join_board is now a
242
+ -- no-op for an active member (MBCONSENT-B(iii) Option 3, closing the demotion/duplicate path),
243
+ -- so it can no longer be used to manufacture a fresh request+approve here.
244
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000004');
245
+ select lives_ok($$
246
+ select agent_board.request_join_board(
247
+ 'b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000004')
248
+ $$, 'setup: a fresh request to approve');
249
+
250
+ select pg_temp.claims('b0000000-0000-0000-0000-000000000001');
251
+ select lives_ok($$
252
+ select agent_board.resolve_board_request(
253
+ 'b2000000-0000-4000-8000-000000000001'::uuid,'b1000000-0000-4000-8000-000000000004'::uuid,
254
+ 'joined'::text, null::uuid, '{}'::jsonb) $$,
255
+ 'SCOPE: approval still succeeds [call succeeds]');
256
+ select is(pg_temp.state('b2000000-0000-4000-8000-000000000001','b1000000-0000-4000-8000-000000000004'),
257
+ 'joined',
258
+ 'SCOPE: approval is untouched -- this change only rewrites the REFUSAL state');
259
+
260
+ select is(
261
+ (select count(*)::int from pg_proc p join pg_namespace n on n.oid = p.pronamespace
262
+ where n.nspname = 'agent_board' and p.proname = 'resolve_board_request'
263
+ and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g') ~ 'v_effective_state'),
264
+ 1,
265
+ 'the rewrite happens in resolve_board_request, not in the shared sink');
266
+
267
+ select is((select version from agent_board.schema_version), 8,
268
+ 'schema_version stays 8 -- additive');
269
+
270
+ select finish();
271
+ rollback;
@@ -31,7 +31,7 @@ begin
31
31
 
32
32
  -- Enroll an active, proven signing key + a pending challenge so the agent can re-sign.
33
33
  insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
34
- values (v_key, v_agent, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(v_key::text,'-',''),43,'A'), 'active', 'proven');
34
+ values (v_key, v_agent, 'primary', 1, public.test_pubkey(v_key::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_key::text), 'active', 'proven');
35
35
  insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)
36
36
  values (v_ch, v_key, v_agent, v_ch::text, 'agent-signing-key-proof.v1:'||v_ch||':n', 'pending', now() + interval '10 minutes');
37
37
 
@@ -0,0 +1,71 @@
1
+ -- #116 B1 — a no-op approve (owner "approves" a subject who is ALREADY active) must retire the
2
+ -- redundant pending row so it stops dangling in pending listings, while leaving the authoritative
3
+ -- active row untouched. Assertion 2 is the mutation pin: remove the B1 retire and the pending row
4
+ -- survives -> count 1 -> fails.
5
+ create extension if not exists pgtap with schema extensions;
6
+ set search_path = extensions, public, agent_board;
7
+
8
+ begin;
9
+
10
+ select plan(4);
11
+
12
+ create temp table _r116 (board uuid, subject uuid, creator uuid, creator_user uuid) on commit drop;
13
+
14
+ do $$
15
+ declare
16
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
17
+ v_creator_user uuid;
18
+ v_creator uuid := gen_random_uuid();
19
+ v_subject uuid := gen_random_uuid();
20
+ v_board uuid;
21
+ begin
22
+ v_creator_user := public.ensure_github_user('r116-creator-' || v_suffix, 'r116-creator-' || v_suffix || '@example.test', 'r116-creator-' || v_suffix, 'R116 Creator');
23
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
24
+ values
25
+ (v_creator, 'agent', 'agent', 'r116-creator-' || v_suffix, 'R116 Creator', 'active', v_creator_user, v_creator_user),
26
+ (v_subject, 'agent', 'agent', 'r116-subject-' || v_suffix, 'R116 Subject', 'active', null, null);
27
+
28
+ select board_id into v_board
29
+ from agent_board.create_board(v_creator, 'r116-board-' || v_suffix, 'R116 Board', 'resolve no-op test', '{}'::jsonb, 'private');
30
+
31
+ -- The subject holds BOTH an active 'joined' row AND a dangling 'requested' row (a legal
32
+ -- duplicate — the partial unique index constrains only active rows). This is exactly the state
33
+ -- the Option-1 no-op is for.
34
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
35
+ values
36
+ (v_board, v_subject, 'joined', v_creator, 'member'),
37
+ (v_board, v_subject, 'requested', v_creator, 'member');
38
+
39
+ insert into _r116 values (v_board, v_subject, v_creator, v_creator_user);
40
+ end
41
+ $$;
42
+
43
+ -- Precondition: the subject has a pending (requested) row before the no-op.
44
+ select is(
45
+ (select count(*)::int from agent_board.board_memberships bm, _r116 f
46
+ where bm.board_id = f.board and bm.agent_id = f.subject and bm.membership_state in ('requested', 'invited')),
47
+ 1, 'precondition: the subject has a dangling pending row');
48
+
49
+ -- The owner "approves" the already-active subject: a no-op success.
50
+ select set_config('request.jwt.claim.role', 'authenticated', true);
51
+ select set_config('request.jwt.claim.sub', (select creator_user from _r116)::text, true);
52
+ select lives_ok(
53
+ (select format($q$select agent_board.resolve_board_request(%L::uuid, %L::uuid, 'joined', %L::uuid)$q$,
54
+ board, subject, creator) from _r116),
55
+ 'no-op approve of an already-active subject succeeds');
56
+
57
+ -- B1 PIN: the redundant pending row is retired (no requested/invited row remains).
58
+ select is(
59
+ (select count(*)::int from agent_board.board_memberships bm, _r116 f
60
+ where bm.board_id = f.board and bm.agent_id = f.subject and bm.membership_state in ('requested', 'invited')),
61
+ 0, 'the redundant pending row is retired after a no-op approve (B1)');
62
+
63
+ -- The authoritative active row is untouched.
64
+ select is(
65
+ (select count(*)::int from agent_board.board_memberships bm, _r116 f
66
+ where bm.board_id = f.board and bm.agent_id = f.subject and bm.membership_state in ('joined', 'subscribed')),
67
+ 1, 'the active membership row is left untouched');
68
+
69
+ select * from finish();
70
+
71
+ rollback;
@@ -0,0 +1,53 @@
1
+ -- MBHARDEN / #97 — mark_retention_metadata must be operator-only (service_role),
2
+ -- never callable by an ordinary authenticated agent.
3
+ --
4
+ -- The function is SECURITY DEFINER and performs unscoped bulk metadata/updated_at writes
5
+ -- across eight core tables. Its defining migration granted service_role but left the default
6
+ -- PUBLIC EXECUTE in place, so any authenticated caller could drive a global write. The
7
+ -- revoke migration (20260811000100) closes that.
8
+ --
9
+ -- These assertions are two-sided and mutation-pinned:
10
+ -- * removing the `revoke ... from public` flips the authenticated/anon/public asserts to
11
+ -- true -> those `not ...` assertions fail.
12
+ -- * over-revoking (dropping the service_role grant) flips the last assertion -> it fails.
13
+
14
+ create extension if not exists pgtap with schema extensions;
15
+ set search_path = extensions, public;
16
+
17
+ begin;
18
+
19
+ select plan(5);
20
+
21
+ select has_function(
22
+ 'agent_board',
23
+ 'mark_retention_metadata',
24
+ array['text', 'timestamp with time zone', 'uuid', 'text'],
25
+ 'mark_retention_metadata exists'
26
+ );
27
+
28
+ -- Core hole: an ordinary authenticated agent must NOT be able to drive the sweep.
29
+ select ok(
30
+ not has_function_privilege('authenticated', 'agent_board.mark_retention_metadata(text,timestamp with time zone,uuid,text)', 'EXECUTE'),
31
+ 'authenticated agents cannot execute mark_retention_metadata'
32
+ );
33
+
34
+ select ok(
35
+ not has_function_privilege('anon', 'agent_board.mark_retention_metadata(text,timestamp with time zone,uuid,text)', 'EXECUTE'),
36
+ 'anonymous callers cannot execute mark_retention_metadata'
37
+ );
38
+
39
+ -- Pin the class property directly: PUBLIC (the =X ACL entry) must carry no EXECUTE.
40
+ select ok(
41
+ not has_function_privilege('public', 'agent_board.mark_retention_metadata(text,timestamp with time zone,uuid,text)', 'EXECUTE'),
42
+ 'PUBLIC carries no EXECUTE on mark_retention_metadata'
43
+ );
44
+
45
+ -- Guard against over-revoke: the operator (service_role) retains its grant.
46
+ select ok(
47
+ has_function_privilege('service_role', 'agent_board.mark_retention_metadata(text,timestamp with time zone,uuid,text)', 'EXECUTE'),
48
+ 'service_role retains EXECUTE on mark_retention_metadata'
49
+ );
50
+
51
+ select * from finish();
52
+
53
+ rollback;
@@ -56,7 +56,7 @@ begin
56
56
  insert into agent_board.delegation_policies(root_agent_id, mode, max_active_children, allowed_scopes, child_ttl_ceiling, require_child_signing_key)
57
57
  values (v_r, 'root_minted', 5, jsonb_build_array('scope:a'), interval '8 hours', true);
58
58
  insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
59
- values (v_rkey, v_r, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(v_rkey::text,'-',''),43,'A'), 'active', 'proven');
59
+ values (v_rkey, v_r, 'primary', 1, public.test_pubkey(v_rkey::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_rkey::text), 'active', 'proven');
60
60
 
61
61
  -- Delegated child C under R (gets an ephemeral revocable key + a grant).
62
62
  select child_agent_id, signing_key_id into v_c, v_ckey
@@ -78,13 +78,13 @@ begin
78
78
  insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
79
79
  values (v_i, 'agent', 'rev-i-'||s, 'I', 'active', u_i, u_owner);
80
80
  insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
81
- values (v_ikey, v_i, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(v_ikey::text,'-',''),43,'A'), 'active', 'proven');
81
+ values (v_ikey, v_i, 'primary', 1, public.test_pubkey(v_ikey::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_ikey::text), 'active', 'proven');
82
82
 
83
83
  -- Z: independent bystander (distinct owner) used only for the inert-ledger proof.
84
84
  insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
85
85
  values (v_z, 'agent', 'rev-z-'||s, 'Z', 'active', u_z, u_z);
86
86
  insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
87
- values (v_zkey, v_z, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(v_zkey::text,'-',''),43,'A'), 'active', 'proven');
87
+ values (v_zkey, v_z, 'primary', 1, public.test_pubkey(v_zkey::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_zkey::text), 'active', 'proven');
88
88
 
89
89
  -- Challenges (pending, unexpired), bound to (key, agent).
90
90
  insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)