@consiliency/agent-board-schema 1.28.0 → 1.38.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 (43) hide show
  1. package/fixtures/already-applied.json +40 -0
  2. package/migrations/20260910000100_agent_board_ceiling_lock_comment_truth.sql +89 -0
  3. package/migrations/20260911000100_agent_board_ledger_revoked_board_creation.sql +330 -0
  4. package/migrations/20260912000100_agent_board_receipt_subject_gate.sql +348 -0
  5. package/migrations/20260913000100_agent_board_task_thread_lock_order.sql +292 -0
  6. package/migrations/20260914000100_agent_board_claim_subject_gate.sql +500 -0
  7. package/migrations/20260915000100_agent_board_fedsession_stored_identity_gate.sql +353 -0
  8. package/migrations/20260916000100_agent_board_federated_identity_injective_slug.sql +1646 -0
  9. package/migrations/20260917000100_agent_board_invite_no_downgrade.sql +398 -0
  10. package/migrations/20260918000100_agent_board_signed_write_enforced_repoint.sql +139 -0
  11. package/migrations/20260919000100_agent_board_signed_write_model_key.sql +107 -0
  12. package/package.json +1 -1
  13. package/schema-manifest.json +93 -3
  14. package/src/deployment-drift-script.test.mjs +424 -0
  15. package/src/deployment-drift.test.mjs +301 -0
  16. package/src/tooling.test.mjs +83 -2
  17. package/tests/agent_board_atomic_fleet_onboarding.test.sql +144 -29
  18. package/tests/agent_board_bundle_endpoint_compat.test.sql +73 -18
  19. package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
  20. package/tests/agent_board_consent_substrate.test.sql +8 -1
  21. package/tests/agent_board_enroll_resumption.test.sql +7 -1
  22. package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
  23. package/tests/agent_board_fedsession.test.sql +17 -2
  24. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
  25. package/tests/agent_board_fk_lock_edges.test.sql +422 -0
  26. package/tests/agent_board_invite_no_downgrade.test.sql +689 -0
  27. package/tests/agent_board_ledger_authority.test.sql +142 -71
  28. package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
  29. package/tests/agent_board_signed_write_enforced.test.sql +366 -0
  30. package/tests/agent_board_signer_lifecycle.test.sql +23 -6
  31. package/tests/agent_board_source_strip_helper.test.sql +256 -1
  32. package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
  33. package/tests/agent_board_write_enforcement_class.test.sql +1016 -0
  34. package/tests/catalog/p_metadata_parameters.json +12 -0
  35. package/tests/fixtures/isolation-substrate.sql +1 -1
  36. package/tests/fixtures/test-prereqs.sql +442 -11
  37. package/tests/isolation/expected/ledger_authority_revoke.out +219 -1
  38. package/tests/isolation/expected/task_thread_lock_order.out +203 -0
  39. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
  40. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
  41. package/tests/isolation/specs/ledger_authority_revoke.spec +115 -0
  42. package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
  43. package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
@@ -0,0 +1,689 @@
1
+ -- #317 — an invite must not weaken an active membership.
2
+ --
3
+ -- The defect: `_upsert_board_membership_state` set `membership_state = p_membership_state`
4
+ -- unconditionally, so inviting an agent who was ALREADY joined moved them back to `invited`
5
+ -- and silently revoked board delivery. Observed in production (`invite_offer -> invited` on a
6
+ -- membership joined since 2026-08-05) and reproduced at this bundle before the fix.
7
+ --
8
+ -- Both directions are asserted. A guard that refuses everything would satisfy "no downgrade"
9
+ -- while breaking every legitimate transition, so the untouched paths are pinned as positively
10
+ -- as the blocked one.
11
+
12
+ begin;
13
+ select plan(53);
14
+
15
+ select agent_board.register_agent('agent', 'nd-owner', 'ND Owner');
16
+ select agent_board.register_agent('agent', 'nd-member', 'ND Member');
17
+
18
+ create temp table _nd on commit drop as
19
+ select (select id from agent_board.agents where handle = 'nd-owner') as owner_id,
20
+ (select id from agent_board.agents where handle = 'nd-member') as member_id;
21
+
22
+ select agent_board.create_board((select owner_id from _nd), 'nd-board', 'ND Board');
23
+
24
+ create temp table _ndb on commit drop as
25
+ select id as board_id from agent_board.boards where slug = 'nd-board';
26
+
27
+ -- ORDER BY is not decoration. An unordered single-row read over (board, agent) is
28
+ -- non-deterministic the moment two rows exist for the pair -- the DUPROW (#88) shape --
29
+ -- and would make every assertion below flip with physical row order rather than fail.
30
+ -- Same ordering the consent suite uses (agent_board_consent_gating.test.sql pg_temp.state).
31
+ create or replace function pg_temp.nd_state() returns text language sql as $$
32
+ select membership_state from agent_board.board_memberships
33
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
34
+ order by case when membership_state in ('joined', 'subscribed') then 0 else 1 end,
35
+ created_at desc, id desc
36
+ limit 1;
37
+ $$;
38
+
39
+ -- ---------------------------------------------------------------------------
40
+ -- THE DEFECT: an offer applied to an existing membership must not weaken it.
41
+ -- ---------------------------------------------------------------------------
42
+ select lives_ok(
43
+ $$ select agent_board._upsert_board_membership_state(
44
+ (select board_id from _ndb), (select member_id from _nd), 'joined', (select owner_id from _nd)) $$,
45
+ 'a member joins');
46
+ select is(pg_temp.nd_state(), 'joined', 'the member is joined');
47
+
48
+ select lives_ok(
49
+ $$ select agent_board.invite_board_member(
50
+ (select board_id from _ndb), (select member_id from _nd), (select owner_id from _nd)) $$,
51
+ 'inviting an already-joined member does not raise — invite stays idempotent');
52
+ select is(pg_temp.nd_state(), 'joined',
53
+ '#317: inviting a JOINED member leaves them joined — an offer never weakens active standing');
54
+
55
+ -- Re-join FIRST, so this arm is evidenced against a JOINED row independently of the
56
+ -- assertion above. Without it, a mutation that reverts only the `invited` arm leaves the
57
+ -- row already `invited` here, and this pair then tests requested-against-INVITED while
58
+ -- reading as though it tested requested-against-joined. Found by the review board: the
59
+ -- earlier PR text claimed one mutation evidenced both arms, and it could not.
60
+ select lives_ok(
61
+ $$ select agent_board._upsert_board_membership_state(
62
+ (select board_id from _ndb), (select member_id from _nd), 'joined', (select owner_id from _nd)) $$,
63
+ 'the member is (re-)joined, so the `requested` arm below is evidenced against JOINED');
64
+
65
+ -- `requested` is the other offer state and must behave identically.
66
+ select lives_ok(
67
+ $$ select agent_board._upsert_board_membership_state(
68
+ (select board_id from _ndb), (select member_id from _nd), 'requested', (select member_id from _nd)) $$,
69
+ 'a `requested` offer against a joined member does not raise');
70
+ select is(pg_temp.nd_state(), 'joined',
71
+ '#317: `requested` against a JOINED member also leaves them joined');
72
+
73
+ -- ---------------------------------------------------------------------------
74
+ -- THE LEDGER, on the no-op path. The row keeping `joined` is only half the claim:
75
+ -- `board_membership_events` is what the #317 investigation actually read to establish
76
+ -- what happened, it is APPEND-ONLY, and before this was asserted the sink emitted the
77
+ -- ARGUMENT rather than the resulting row -- appending `invite_offer -> invited` beside a
78
+ -- row that says `joined`. A ledger contradicting the table it describes is worse than the
79
+ -- demotion this migration removes, because it cannot be corrected afterwards.
80
+ --
81
+ -- Asserted on RESULT_STATE and not on event_kind, deliberately. The kind is derived from
82
+ -- the session: these calls run with no JWT, so the emitter records `operator_designation`,
83
+ -- while the production event that exposed #317 was an authenticated owner's `invite_offer`.
84
+ -- Pinning the kind here would pin the test harness's own principal, not the property.
85
+ -- ---------------------------------------------------------------------------
86
+ select is(
87
+ (select result_state from agent_board.board_membership_events
88
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
89
+ order by seq desc limit 1),
90
+ pg_temp.nd_state(),
91
+ '#317: the latest ledger event records the state the ROW actually has — the ledger cannot contradict the table');
92
+
93
+ select is(
94
+ (select count(*)::text from agent_board.board_membership_events
95
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
96
+ and result_state in ('invited', 'requested')),
97
+ '0',
98
+ '#317: NO event so far records an offer state — before the fix the no-op invite appended `-> invited` beside a joined row, append-only and uncorrectable');
99
+
100
+ -- ---------------------------------------------------------------------------
101
+ -- WHAT "NO-OP" ACTUALLY MEANS, both halves. The migration claims a suppressed offer leaves the
102
+ -- membership "exactly as strong as it was"; the row is nonetheless still written. Asserting only
103
+ -- the preserved half would leave the prose broader than the evidence, which is the failure this
104
+ -- file has been corrected for twice. So both are pinned: the authority-bearing columns do NOT
105
+ -- move, and the bookkeeping columns DO.
106
+ -- ---------------------------------------------------------------------------
107
+ create temp table _ndbefore on commit drop as
108
+ select membership_state, left_at, role, metadata, updated_at, created_by_agent_id
109
+ from agent_board.board_memberships
110
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd);
111
+
112
+ select lives_ok(
113
+ $$ select agent_board._upsert_board_membership_state(
114
+ (select board_id from _ndb), (select member_id from _nd), 'invited', (select owner_id from _nd),
115
+ '{"nd_probe": true}'::jsonb) $$,
116
+ 'a suppressed offer carrying metadata is applied to the active member');
117
+
118
+ select is(
119
+ (select (b.membership_state is not distinct from a.membership_state)
120
+ and (b.left_at is not distinct from a.left_at)
121
+ and (b.role is not distinct from a.role)
122
+ from _ndbefore b
123
+ cross join lateral (
124
+ select membership_state, left_at, role from agent_board.board_memberships
125
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
126
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
127
+ created_at desc, id desc
128
+ limit 1) a)::text,
129
+ 'true',
130
+ '#317: a suppressed offer moves NONE of the authority-bearing columns — membership_state, left_at, role');
131
+
132
+ select is(
133
+ (select (a.metadata ? 'nd_probe')::text
134
+ from agent_board.board_memberships a
135
+ where a.board_id = (select board_id from _ndb) and a.agent_id = (select member_id from _nd)
136
+ order by case when a.membership_state in ('joined','subscribed') then 0 else 1 end,
137
+ a.created_at desc, a.id desc
138
+ limit 1),
139
+ 'true',
140
+ '#317 scope: the row IS still written — caller metadata merges on the suppressed path, which the migration states rather than implies');
141
+
142
+ -- ---------------------------------------------------------------------------
143
+ -- AN OFFER AGAINST THE OWNER'S OWN MEMBERSHIP. membership_state is written from the RESOLVED
144
+ -- next state while `role` and `left_at` key off the caller's REQUEST; this pins that an offer
145
+ -- costs the owner neither their standing nor their role.
146
+ --
147
+ -- Honest about what this does NOT cover, because the first version of this comment claimed more
148
+ -- and a mutation refuted it: re-keying the #79 clamp to the resolved state leaves these
149
+ -- assertions GREEN, since the clamp also requires a non-active prior state (or currently-removed
150
+ -- standing) and a suppressed offer always has an active prior. The two spellings diverge only in
151
+ -- the `v_currently_removed` corner, which this fixture does not build. Recorded rather than
152
+ -- papered over -- an assertion that cannot fail for the reason its comment gives is exactly the
153
+ -- shape this repo keeps finding.
154
+ -- ---------------------------------------------------------------------------
155
+ select is(
156
+ (select role from agent_board.board_memberships
157
+ where board_id = (select board_id from _ndb) and agent_id = (select owner_id from _nd)
158
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end, created_at desc, id desc
159
+ limit 1),
160
+ 'owner',
161
+ 'fixture: the board creator holds the owner role');
162
+
163
+ select lives_ok(
164
+ $$ select agent_board._upsert_board_membership_state(
165
+ (select board_id from _ndb), (select owner_id from _nd), 'invited', (select owner_id from _nd)) $$,
166
+ 'an offer is applied to the OWNER''s own active membership');
167
+
168
+ select is(
169
+ (select role || '/' || membership_state from agent_board.board_memberships
170
+ where board_id = (select board_id from _ndb) and agent_id = (select owner_id from _nd)
171
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end, created_at desc, id desc
172
+ limit 1),
173
+ 'owner/joined',
174
+ '#317: a suppressed offer leaves the owner at owner/joined — role and standing both survive an offer');
175
+
176
+ -- ---------------------------------------------------------------------------
177
+ -- THE MINTED-CONSENT TRAP, which is why the no-op emits NOTHING rather than the row's
178
+ -- own state. The second draft of this migration "fixed" the false ledger by emitting
179
+ -- v_membership.membership_state -- and that MINTS CONSENT: with no JWT the provenance is
180
+ -- `operator-designated`, and `_membership_consent_present` treats operator-designated +
181
+ -- joined/subscribed as an actual PLACEMENT. An operator merely OFFERING would have become
182
+ -- an admission grant, reopening the exact narrowing MBCONSENT shipped to make.
183
+ --
184
+ -- It needs its OWN fixture: the member in the main scenario joined through the sink, so
185
+ -- their ledger already carries a grant and consent reads true no matter what happens here.
186
+ -- The trap is only visible on a membership whose ledger is SILENT -- which is reachable,
187
+ -- because five shipped writers seat a row without routing through the sink.
188
+ -- ---------------------------------------------------------------------------
189
+ select agent_board.register_agent('agent', 'nd-silent', 'ND Silent');
190
+
191
+ create temp table _ndsil on commit drop as
192
+ select (select id from agent_board.agents where handle = 'nd-silent') as agent_id;
193
+
194
+ -- seated directly, so the consent ledger says nothing about this agent
195
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
196
+ select (select board_id from _ndb), (select agent_id from _ndsil), 'joined', 'member', (select owner_id from _nd);
197
+
198
+ select is(
199
+ agent_board._membership_consent_present((select board_id from _ndb), (select agent_id from _ndsil))::text,
200
+ 'false',
201
+ '#317 fixture: the silently-seated member has NO consent on record — the precondition the trap needs');
202
+
203
+ select lives_ok(
204
+ $$ select agent_board.invite_board_member(
205
+ (select board_id from _ndb), (select agent_id from _ndsil), (select owner_id from _nd)) $$,
206
+ 'an operator invites the silently-seated JOINED member');
207
+
208
+ select is(
209
+ (select count(*)::text from agent_board.board_membership_events
210
+ where board_id = (select board_id from _ndb) and agent_id = (select agent_id from _ndsil)),
211
+ '0',
212
+ '#317: a suppressed offer appends NO event at all — the ledger records state CHANGES, and nothing changed');
213
+
214
+ select is(
215
+ agent_board._membership_consent_present((select board_id from _ndb), (select agent_id from _ndsil))::text,
216
+ 'false',
217
+ '#317: the offer did NOT mint consent — emitting the row state here would have turned an operator OFFER into an admission grant');
218
+
219
+ -- THE SECOND CONJUNCT of the suppression test, which nothing else reaches. `joined -> joined`
220
+ -- also does not move the state, but it IS the caller getting exactly what they asked for, and it
221
+ -- must still emit -- join_board and update_board_membership both permit it. Defining suppression
222
+ -- as "the state did not move" alone silently stops emitting for that call, and left the whole
223
+ -- suite green until this assertion existed.
224
+ select lives_ok(
225
+ $$ select agent_board._upsert_board_membership_state(
226
+ (select board_id from _ndb), (select agent_id from _ndsil), 'joined', (select owner_id from _nd)) $$,
227
+ 'the silently-seated member is re-asserted as joined');
228
+
229
+ select is(
230
+ (select count(*)::text from agent_board.board_membership_events
231
+ where board_id = (select board_id from _ndb) and agent_id = (select agent_id from _ndsil)),
232
+ '1',
233
+ '#317: joined -> joined DOES emit — it moves no state but is not a suppressed offer, so the ledger records it');
234
+
235
+ -- ---------------------------------------------------------------------------
236
+ -- THE SUBSCRIBED ARM. The guard names two active states, and until this existed only
237
+ -- `joined` was exercised: narrowing the guard to `in ('joined')` -- restoring the whole
238
+ -- defect for every subscribed member -- left all 19 assertions green. Found by the review
239
+ -- board, and it is the sharpest kind of miss: the rule was stated correctly in the
240
+ -- migration and half of it was unverified.
241
+ -- ---------------------------------------------------------------------------
242
+ select lives_ok(
243
+ $$ select agent_board._upsert_board_membership_state(
244
+ (select board_id from _ndb), (select member_id from _nd), 'subscribed', (select member_id from _nd)) $$,
245
+ 'the member self-toggles to subscribed');
246
+ select is(pg_temp.nd_state(), 'subscribed', 'the member is subscribed');
247
+
248
+ select lives_ok(
249
+ $$ select agent_board.invite_board_member(
250
+ (select board_id from _ndb), (select member_id from _nd), (select owner_id from _nd)) $$,
251
+ 'inviting a SUBSCRIBED member does not raise');
252
+ select is(pg_temp.nd_state(), 'subscribed',
253
+ '#317: inviting a SUBSCRIBED member leaves them subscribed — the guard covers BOTH active states, not just joined');
254
+
255
+ -- Restore to subscribed FIRST, for the same reason the joined pair re-joins above: a mutation
256
+ -- that drops only the `invited` arm would have demoted this member on the assertion before
257
+ -- this one, and the pair below would then be testing requested-against-INVITED while reading
258
+ -- as though it tested requested-against-SUBSCRIBED. Round 1 fixed this on the joined pair and
259
+ -- left the subscribed pair contaminated; a review seat caught the other half.
260
+ select lives_ok(
261
+ $$ select agent_board._upsert_board_membership_state(
262
+ (select board_id from _ndb), (select member_id from _nd), 'subscribed', (select member_id from _nd)) $$,
263
+ 'restored to subscribed, so the `requested` arm below is evidenced against SUBSCRIBED');
264
+
265
+ select lives_ok(
266
+ $$ select agent_board._upsert_board_membership_state(
267
+ (select board_id from _ndb), (select member_id from _nd), 'requested', (select member_id from _nd)) $$,
268
+ 'a `requested` offer against a subscribed member does not raise');
269
+ select is(pg_temp.nd_state(), 'subscribed',
270
+ '#317: `requested` against a SUBSCRIBED member also leaves them subscribed');
271
+
272
+ -- Finding 1A: the ledger was pinned only at the JOINED stage, so the SUBSCRIBED arm of the
273
+ -- suppression could emit a contradicting event with nothing reading it. It can now be checked
274
+ -- here because nothing else writes this membership between the toggle and this point.
275
+ select is(
276
+ (select result_state from agent_board.board_membership_events
277
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
278
+ order by seq desc limit 1),
279
+ pg_temp.nd_state(),
280
+ '#317: the latest event still matches the row at the SUBSCRIBED stage too — the suppression arms are pinned symmetrically');
281
+
282
+ -- restore to joined so the transitions below start from the state they describe
283
+ select lives_ok(
284
+ $$ select agent_board._upsert_board_membership_state(
285
+ (select board_id from _ndb), (select member_id from _nd), 'joined', (select member_id from _nd)) $$,
286
+ 'back to joined for the transition checks');
287
+
288
+ -- ---------------------------------------------------------------------------
289
+ -- THE OTHER DIRECTION: everything legitimate still works. Without these, a guard
290
+ -- that simply refused all offers would pass the assertions above.
291
+ -- ---------------------------------------------------------------------------
292
+ select lives_ok(
293
+ $$ select agent_board._upsert_board_membership_state(
294
+ (select board_id from _ndb), (select member_id from _nd), 'subscribed', (select member_id from _nd)) $$,
295
+ 'joined -> subscribed does not raise');
296
+ select is(pg_temp.nd_state(), 'subscribed', 'joined -> subscribed self-toggle still works');
297
+
298
+ select lives_ok(
299
+ $$ select agent_board._upsert_board_membership_state(
300
+ (select board_id from _ndb), (select member_id from _nd), 'removed', (select owner_id from _nd)) $$,
301
+ 'active -> removed does not raise');
302
+ select is(pg_temp.nd_state(), 'removed', 'active -> removed still works');
303
+
304
+ -- Finding 1B: widening the suppression predicate to swallow a REAL transition left the suite
305
+ -- green, because nothing asserted that a genuine state change still reaches the ledger. An
306
+ -- append-only record that silently omits a removal is the same class of defect as one that
307
+ -- records a state the row does not have.
308
+ select is(
309
+ (select event_kind || ' -> ' || result_state from agent_board.board_membership_events
310
+ where board_id = (select board_id from _ndb) and agent_id = (select member_id from _nd)
311
+ order by seq desc limit 1),
312
+ 'operator_removal -> removed',
313
+ '#317: a REAL transition still emits — suppression must not swallow an actual state change');
314
+
315
+ select lives_ok(
316
+ $$ select agent_board._upsert_board_membership_state(
317
+ (select board_id from _ndb), (select member_id from _nd), 'invited', (select owner_id from _nd)) $$,
318
+ 're-inviting a REMOVED member does not raise — an OVER-BROAD guard trips board_memberships_state_left_at here, and a bare call would abort the file with NO `not ok` (verified)');
319
+ select is(pg_temp.nd_state(), 'invited',
320
+ 're-inviting a REMOVED member still works — the guard is about weakening, not about offers');
321
+
322
+ -- ---------------------------------------------------------------------------
323
+ -- THE SECOND DOOR: invite-link redemption reaches an active membership by a
324
+ -- different statement shape (INSERT … ON CONFLICT DO UPDATE), so the guard in the
325
+ -- UPDATE path says nothing about it. Asserted behaviourally rather than by reading
326
+ -- the source: `_redeem_unbounded_invite_for_agent` lifts only `role`, and this is
327
+ -- what makes that claim evidence instead of a comment.
328
+ -- ---------------------------------------------------------------------------
329
+ select lives_ok(
330
+ $$ select agent_board._upsert_board_membership_state(
331
+ (select board_id from _ndb), (select member_id from _nd), 'joined', (select owner_id from _nd)) $$,
332
+ 'the member is restored to joined for the invite-link probe');
333
+
334
+ -- The preset is `subscribed` while the member is `joined`, DELIBERATELY: a link presetting
335
+ -- the state the member already has cannot show whether redemption writes the column, so the
336
+ -- probe would pass no matter what the function did. (`invited` would be the sharper contrast
337
+ -- but create_invite_link rejects it -- "invalid preset membership state" -- so `subscribed`
338
+ -- is the strongest observable difference available.)
339
+ --
340
+ -- `select * from`, not a bare call: create_invite_link RETURNS TABLE, so a scalar
341
+ -- projection is a pseudo-type record and `create table as` errors -- which ABORTS the
342
+ -- transaction. The run then stops early and pgTAP emits NO `not ok` line at all, so the
343
+ -- failure is INVISIBLE rather than loud (message-board#113, and observed here).
344
+ --
345
+ -- Deliberately no assertion count in that sentence. It used to name one, and the number was
346
+ -- stale within a day of being written -- a hand-maintained count of a machine-derivable fact
347
+ -- is rule 1's own anti-pattern applied to prose. The invariant is that the failure prints
348
+ -- nothing; the exact stopping point moves every time an assertion is added.
349
+ -- lives_ok, not a bare statement. create_invite_link checks the creator's role against the
350
+ -- board authority ceiling, so ANY mutation that weakens the owner's own membership earlier in
351
+ -- this file makes it RAISE here -- and as a bare call that aborted the transaction at assertion
352
+ -- 38, hiding every catalog and structural assertion after it. Measured: three separate
353
+ -- behavioural mutations truncated the run to 38 of 52 that way, so a regression in the guard
354
+ -- could conceal a simultaneous regression in the class assertions. This is the file's own
355
+ -- subject matter applied to its own fixture.
356
+ create temp table _ndlink(invite_id uuid, board_id uuid, code text, preset_role text,
357
+ preset_membership_state text, max_uses int, uses_count int,
358
+ expires_at timestamptz, created_at timestamptz) on commit drop;
359
+
360
+ select lives_ok(
361
+ $$ insert into _ndlink select * from agent_board.create_invite_link(
362
+ (select board_id from _ndb), 'member', 'subscribed', null, null, (select owner_id from _nd)) $$,
363
+ 'the owner can still create an invite link — a weakened owner membership fails HERE by name rather than aborting the file');
364
+
365
+ -- lives_ok, not a bare `is((select … from rpc(…)))`: redemption is a state-mutating
366
+ -- admission RPC, and a raise inside an assertion's value expression aborts the whole
367
+ -- file with no `not ok` (message-board#113).
368
+ select lives_ok(
369
+ $$ create temp table _ndcap as select * from agent_board._redeem_unbounded_invite_for_agent(
370
+ (select code from _ndlink), (select member_id from _nd)) $$,
371
+ 'a JOINED member redeems a board invite link — redemption stays idempotent');
372
+ select is(pg_temp.nd_state(), 'joined',
373
+ '#317 second door: redeeming a `subscribed`-preset link leaves a JOINED member joined — the ON CONFLICT lifts role only');
374
+
375
+ -- ---------------------------------------------------------------------------
376
+ -- THE CLASS, derived from pg_proc rather than from a list written here.
377
+ --
378
+ -- The guard lives in the shared sink so every caller inherits it. A future function that
379
+ -- writes board_memberships.MEMBERSHIP_STATE directly would bypass it, and a hand-written list
380
+ -- of "the writers that matter" is what this repo's rule 1 forbids -- so the set is enumerated
381
+ -- from the catalog and these assertions NAME any new member.
382
+ --
383
+ -- WHY THE PREDICATE IS SHAPED THIS WAY -- each clause was forced by a mutation that
384
+ -- ESCAPED an earlier draft, not chosen for tidiness:
385
+ --
386
+ -- * `stripped_body`, not `pg_get_functiondef`. The generated header carries
387
+ -- `SET search_path = …` with no semicolon before the body, so `set[^;]*membership_state`
388
+ -- over the full definition matched functions that only READ the column (join_board,
389
+ -- onboard_federated_fleet_agent). The earlier draft hid that false positive behind an
390
+ -- `update agent_board.board_memberships` conjunct -- and that conjunct was itself the
391
+ -- hole below. stripped_body also strips comments and literals, so prose near a write
392
+ -- cannot satisfy a claim (message-board#196/#199), and it RAISES rather than returning
393
+ -- NULL for a body it cannot read, which a `where … ~ …` scan would drop silently.
394
+ --
395
+ -- * The statement alternation and the optional-qualifier spelling are TAKEN VERBATIM from
396
+ -- agent_board_receipt_subject_gate.test.sql rather than written fresh. That file already
397
+ -- paid for this: it records `merge into`, `update only`, `delete from only`,
398
+ -- `agent_board."board_memberships"` and `"agent_board" . "board_memberships"` as
399
+ -- VERIFIED-LEGAL spellings that were each invisible to a narrower pattern. A second pattern
400
+ -- written here would be a second list to drift -- rule 1 applied to the predicate itself.
401
+ -- The first draft of THIS file required `update agent_board.board_memberships` alone, and
402
+ -- FOUR separate decoys each left it 19/19 green: `insert ... on conflict ... do update set`,
403
+ -- `merge into`, an UNQUALIFIED `update board_memberships` under a search_path, and dynamic
404
+ -- SQL inside a string literal. The review board found three of the four; the fourth came
405
+ -- from testing its findings rather than accepting them.
406
+ --
407
+ -- * `stripped_body(p.oid, TRUE)` -- literals KEPT, deliberately, and this is the one place the
408
+ -- usual advice inverts. A writer assembled as dynamic SQL lives ENTIRELY inside a string
409
+ -- literal, so the default literal-stripping mode cannot see it. Comments are still stripped,
410
+ -- so prose naming the pattern still cannot satisfy the claim. Keeping literals can only
411
+ -- produce a LOUD false positive on an exact-set assertion, never a silent miss.
412
+ --
413
+ -- * `lower(...)` with `~` rather than `~*`: same effect, and it matches the neighbouring file
414
+ -- so the two cannot drift on case handling.
415
+ --
416
+ -- WHAT THIS SCAN CANNOT SEE, stated exactly rather than generously, because a scan that reports
417
+ -- clean on its own boundary forms is worth less than one that says which forms it declines to
418
+ -- judge. All of these were DEMONSTRATED against this file, not imagined:
419
+ --
420
+ -- * a body that ASSEMBLES the statement rather than containing it. Two demonstrated forms:
421
+ -- fragment concatenation (`'update agent_board.board_mem' || 'berships set ...'`) and
422
+ -- `EXECUTE format('update %I.%I set %I = %L', 'agent_board', 'board_memberships',
423
+ -- 'membership_state', 'invited')`, where the identifiers live in format ARGUMENTS so neither
424
+ -- the table nor the column is ever adjacent to the keyword. Keeping literals is what makes a
425
+ -- single COMPLETE literal statement visible; it does nothing for an assembled one, because no
426
+ -- regex can concatenate or interpolate. Neither can be closed by a better pattern -- only by
427
+ -- a parser -- so both are disclosed and then BOUNDED structurally by the privilege assertion
428
+ -- further down, which holds however the SQL is spelt.
429
+ -- * a writer in a schema other than `agent_board` (the `nspname` filter). Same bound applies.
430
+ --
431
+ -- Both are recorded rather than closed. The privilege assertion is the reason that is an
432
+ -- acceptable answer instead of a shrug: it does not identify such a writer, but it proves one
433
+ -- could only exist as a SECURITY DEFINER function, which is the population this scan reads.
434
+ --
435
+ -- * `prokind in ('f','p')`. A PROCEDURE writer is exempt under `prokind = 'f'`.
436
+ --
437
+ -- * `\m` word boundaries. Without them the role canary below silently matched nothing.
438
+ --
439
+ -- A `BEGIN ATOMIC` writer is covered too: stripped_body falls back to
440
+ -- pg_get_function_sqlbody, verified with a BEGIN ATOMIC decoy that this assertion named.
441
+ select is(
442
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
443
+ from pg_proc p
444
+ join pg_namespace n on n.oid = p.pronamespace
445
+ where n.nspname = 'agent_board'
446
+ and p.prokind in ('f', 'p')
447
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
448
+ and lower(agent_board_test.stripped_body(p.oid, true))
449
+ ~ '(insert\s+into|merge\s+into|update|delete\s+from|copy|truncate(\s+table)?)(\s+only)?\s+(("?agent_board"?\s*\.\s*)?board_memberships\M|("?agent_board"?\s*\.\s*)?"board_memberships")'),
450
+ array['_fedsession_autojoin_default_board', '_redeem_unbounded_invite_for_agent',
451
+ '_upsert_board_membership_state', 'bootstrap_board_owner_memberships',
452
+ 'create_board', 'grant_board_role']::text[],
453
+ '#317: the set of functions that WRITE board_memberships is exactly these six — a new writer fails here NAMING itself');
454
+
455
+ -- The direction claim itself: of those writers, only the shared sink assigns
456
+ -- membership_state in a SET clause, so only the sink can move an EXISTING row between
457
+ -- states -- and it is the one carrying the guard. The other five are exempt because of
458
+ -- what they write, and that is asserted here as a set rather than by leaving names out;
459
+ -- an exemption claimed by absence is this repo's named soft spot.
460
+ select is(
461
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
462
+ from pg_proc p
463
+ join pg_namespace n on n.oid = p.pronamespace
464
+ where n.nspname = 'agent_board'
465
+ and p.prokind in ('f', 'p')
466
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
467
+ and lower(agent_board_test.stripped_body(p.oid, true))
468
+ ~ '(insert\s+into|merge\s+into|update|delete\s+from|copy|truncate(\s+table)?)(\s+only)?\s+(("?agent_board"?\s*\.\s*)?board_memberships\M|("?agent_board"?\s*\.\s*)?"board_memberships")'
469
+ and lower(agent_board_test.stripped_body(p.oid, true))
470
+ ~ '(set\s[^;]*\mmembership_state"?\s*=|set\s*\([^;()]*\mmembership_state"?[^;()]*\)\s*=)'),
471
+ array['_upsert_board_membership_state']::text[],
472
+ '#317: the shared sink is the ONLY writer that assigns membership_state IN A SET CLAUSE — i.e. the only one that can MOVE an existing row between states');
473
+
474
+ -- POSITIVE CANARY for the SET-clause predicate the assertion above depends on. Without
475
+ -- this, a regex that stopped matching SET clauses at all would make that assertion green
476
+ -- and empty -- the exact fail-open this file exists to prevent. These five DO assign
477
+ -- `role` in a SET clause, so the predicate is proven to still see real SET clauses.
478
+ select is(
479
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
480
+ from pg_proc p
481
+ join pg_namespace n on n.oid = p.pronamespace
482
+ where n.nspname = 'agent_board'
483
+ and p.prokind in ('f', 'p')
484
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
485
+ and lower(agent_board_test.stripped_body(p.oid, true))
486
+ ~ '(insert\s+into|merge\s+into|update|delete\s+from|copy|truncate(\s+table)?)(\s+only)?\s+(("?agent_board"?\s*\.\s*)?board_memberships\M|("?agent_board"?\s*\.\s*)?"board_memberships")'
487
+ and lower(agent_board_test.stripped_body(p.oid, true)) ~ 'set\s[^;]*\mrole\s*='),
488
+ array['_redeem_unbounded_invite_for_agent', '_upsert_board_membership_state',
489
+ 'bootstrap_board_owner_memberships', 'create_board', 'grant_board_role']::text[],
490
+ '#317 canary: the SET-clause predicate still matches real SET clauses (these five assign `role`) — so an empty membership_state result means empty, not blind');
491
+
492
+ -- THE DOORS THAT ARE NOT FUNCTION BODIES. Every predicate above reads function source, so
493
+ -- anything that writes membership_state WITHOUT a function body naming the table is invisible
494
+ -- to all of them. Three such doors exist and all three are asserted, because two of them were
495
+ -- found only by a review seat asking what was not a function:
496
+ --
497
+ -- * a TRIGGER assigning NEW.membership_state;
498
+ -- * a non-SELECT RULE rewriting a statement onto the table;
499
+ -- * an auto-updatable VIEW -- `update agent_board.<view> set membership_state = ...` is
500
+ -- rewritten by PostgreSQL onto the base table with NO trigger and NO mention of
501
+ -- `board_memberships` anywhere in the writer's body. Verified: a decoy view plus a writer
502
+ -- through it left the whole suite green before this assertion existed.
503
+ --
504
+ -- The view set is pinned by NAME rather than by "no views exist", because two legitimately do.
505
+ -- A new view over this table is then a loud event that forces its author to decide whether it
506
+ -- is writable, which is the decision that would otherwise be made silently.
507
+ select is(
508
+ (select coalesce(array_agg(t.tgname::text order by t.tgname::text), array[]::text[])
509
+ from pg_trigger t
510
+ join pg_class c on c.oid = t.tgrelid
511
+ join pg_namespace n on n.oid = c.relnamespace
512
+ where n.nspname = 'agent_board' and c.relname = 'board_memberships'
513
+ and not t.tgisinternal),
514
+ array[]::text[],
515
+ '#317: no user trigger on board_memberships — a trigger assigning NEW.membership_state would bypass every source predicate above');
516
+
517
+ select is(
518
+ (select coalesce(array_agg(r.rulename::text order by r.rulename::text), array[]::text[])
519
+ from pg_rewrite r
520
+ join pg_class c on c.oid = r.ev_class
521
+ join pg_namespace n on n.oid = c.relnamespace
522
+ where n.nspname = 'agent_board' and c.relname = 'board_memberships'
523
+ and r.ev_type <> '1'),
524
+ array[]::text[],
525
+ '#317: no INSERT/UPDATE/DELETE rule on board_memberships — a rule rewrites a statement onto the table with no function body naming it');
526
+
527
+ -- ---------------------------------------------------------------------------
528
+ -- THE STRUCTURAL BOUND, which is the one claim here that a regex cannot be talked out of.
529
+ --
530
+ -- Everything above enumerates writers by READING SOURCE, and a source scan has a floor: a
531
+ -- body that assembles `update agent_board.board_' || 'memberships set ...` from separate
532
+ -- string fragments and EXECUTEs the result is invisible to any pattern, because no regex can
533
+ -- concatenate. That escape was demonstrated against this very file (it stayed 35/35 green),
534
+ -- and it cannot be closed by a better regex -- only by a parser.
535
+ --
536
+ -- What CAN be stated without reading source: nobody but the table owner may write this table
537
+ -- at all. `authenticated`, `anon` and `service_role` hold no INSERT/UPDATE/DELETE, so every
538
+ -- write -- fragment-assembled or not -- must go through a SECURITY DEFINER function owned by
539
+ -- the owner. That does not identify the writer, but it bounds WHERE one can exist to the set
540
+ -- the scan above enumerates, and it holds regardless of how the statement is spelled.
541
+ --
542
+ -- has_table_privilege, not a pg_class.relacl or information_schema read: it resolves privileges
543
+ -- inherited through role membership and PUBLIC defaults, which a direct-grant read misses --
544
+ -- the fail-open shape #302 corrected and #118's drop/recreate class. Verified on a built
545
+ -- database that all three routes read false here, including the PUBLIC default, and that none
546
+ -- of these roles reaches the owner via pg_has_role.
547
+ --
548
+ -- SCOPE, because "no role may write it" would otherwise overstate: this asserts the three
549
+ -- APPLICATION principals. A SUPERUSER (supabase_admin) and the owner itself are outside any
550
+ -- in-database privilege check by construction -- no assertion written in SQL can bound them,
551
+ -- and saying so is more useful than a claim that quietly excludes them. `service_role` carries
552
+ -- BYPASSRLS, which is deliberately NOT the same thing: it defeats row-level policies and does
553
+ -- nothing about table privileges, so it still holds no UPDATE here.
554
+ -- ---------------------------------------------------------------------------
555
+ select is(
556
+ (select coalesce(array_agg(g.grant_found order by g.grant_found), array[]::text[])
557
+ from (
558
+ select r.rolname || ':' || t.tbl || ':' || t.priv as grant_found
559
+ from (values ('authenticated'), ('anon'), ('service_role')) as r(rolname)
560
+ cross join (values
561
+ -- direct writes to the table
562
+ ('agent_board.board_memberships', 'INSERT'),
563
+ ('agent_board.board_memberships', 'UPDATE'),
564
+ ('agent_board.board_memberships', 'DELETE'),
565
+ -- TRUNCATE is a WRITE by this file's own definition: the statement scan
566
+ -- above counts `truncate` as a writer. Checking only INSERT/UPDATE/DELETE
567
+ -- made the privilege bound NARROWER than the scan it is supposed to
568
+ -- backstop -- a role holding only TRUNCATE could empty the table and fail
569
+ -- neither. Found by a review seat comparing the two lists against each other.
570
+ ('agent_board.board_memberships', 'TRUNCATE'),
571
+ -- THE CASCADE PARENTS, and they are load-bearing rather than thorough.
572
+ -- board_id and agent_id are ON DELETE CASCADE, so deleting a board or an
573
+ -- agent DESTROYS membership rows through internal FK triggers -- with no
574
+ -- privilege on board_memberships and no SECURITY DEFINER function anywhere.
575
+ -- Measured: 2 membership rows -> 0 on one `delete from agent_board.boards`.
576
+ -- That is exactly the write class this assertion claimed could not exist,
577
+ -- reached through a table the assertion never mentioned. It is closed in
578
+ -- the shipped grant set, and now it is closed BY ASSERTION rather than by
579
+ -- the accident of nobody having granted it. (It also reproduces #317's
580
+ -- original symptom -- a member with no visible membership -- by a different
581
+ -- mechanism, which is worth knowing next time that symptom is reported.)
582
+ ('agent_board.boards', 'DELETE'),
583
+ ('agent_board.agents', 'DELETE')
584
+ ) as t(tbl, priv)
585
+ where has_table_privilege(r.rolname, t.tbl, t.priv)
586
+ ) g),
587
+ array[]::text[],
588
+ '#317 structural bound: no application role can write board_memberships directly, empty it, or destroy its rows through an ON DELETE CASCADE parent');
589
+
590
+ select is(
591
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
592
+ from pg_proc p
593
+ join pg_namespace n on n.oid = p.pronamespace
594
+ where n.nspname = 'agent_board'
595
+ and p.proname in ('_fedsession_autojoin_default_board', '_redeem_unbounded_invite_for_agent',
596
+ '_upsert_board_membership_state', 'bootstrap_board_owner_memberships',
597
+ 'create_board', 'grant_board_role')
598
+ and not p.prosecdef),
599
+ array[]::text[],
600
+ '#317: every one of the six FUNCTION writers is SECURITY DEFINER — a function writer that is not would need a direct table grant, which the assertion above forbids (FK cascade is covered there, not here)');
601
+
602
+ select is(
603
+ (select coalesce(array_agg(distinct c.relname::text order by c.relname::text), array[]::text[])
604
+ from pg_depend d
605
+ join pg_rewrite r on r.oid = d.objid
606
+ join pg_class c on c.oid = r.ev_class
607
+ join pg_class t on t.oid = d.refobjid
608
+ join pg_namespace tn on tn.oid = t.relnamespace
609
+ join pg_namespace cn on cn.oid = c.relnamespace
610
+ where tn.nspname = 'agent_board' and t.relname = 'board_memberships'
611
+ and cn.nspname = 'agent_board' and c.relkind in ('v', 'm')),
612
+ array['v_board_directory', 'v_board_memberships']::text[],
613
+ '#317: exactly these two views read board_memberships — a NEW view is auto-updatable by default, and an update through it reaches the table with nothing naming it');
614
+
615
+ -- ---------------------------------------------------------------------------
616
+ -- THE WHOLE INPUT SPACE, not a chosen path through it. Every assertion above walks ONE
617
+ -- scenario, and a scenario can only show that the cases someone thought of behave. This
618
+ -- drives all 6x6 (prior state, requested state) pairs through the sink and asserts two
619
+ -- things about the result set as a whole:
620
+ --
621
+ -- * the suppressed set is EXACTLY the four offer-against-active pairs, named. A guard
622
+ -- that is too narrow drops a pair; one that is too broad adds one; either way the
623
+ -- assertion prints the actual set, so it says what went wrong rather than just that
624
+ -- something did.
625
+ -- * no pair RAISES. The over-broad-guard mutation trips
626
+ -- `board_memberships_state_left_at` on a transition no scenario above visits, and a
627
+ -- raise inside a scenario is what truncates a pgTAP file silently.
628
+ --
629
+ -- The ledger is cleared between seating the prior state and the call under test, so the
630
+ -- count measures only that call.
631
+ -- ---------------------------------------------------------------------------
632
+ create temp table _ndmx(prior text, req text, after_state text, events int, err text) on commit drop;
633
+
634
+ select lives_ok($$
635
+ do $mx$
636
+ declare
637
+ st text[] := array['requested','invited','joined','subscribed','left','removed'];
638
+ p text; r text; v_b uuid; v_a uuid; v_o uuid; n int; after_s text; i int := 0;
639
+ begin
640
+ foreach p in array st loop
641
+ foreach r in array st loop
642
+ i := i + 1;
643
+ begin
644
+ perform agent_board.register_agent('agent','ndmx-o-'||i,'O');
645
+ perform agent_board.register_agent('agent','ndmx-m-'||i,'M');
646
+ select a.id into v_o from agent_board.agents a where a.handle='ndmx-o-'||i;
647
+ select a.id into v_a from agent_board.agents a where a.handle='ndmx-m-'||i;
648
+ perform agent_board.create_board(v_o,'ndmx-b-'||i,'B');
649
+ select b.id into v_b from agent_board.boards b where b.slug='ndmx-b-'||i;
650
+ perform agent_board._upsert_board_membership_state(v_b, v_a, p, v_o);
651
+ delete from agent_board.board_membership_events where board_id=v_b and agent_id=v_a;
652
+ perform agent_board._upsert_board_membership_state(v_b, v_a, r, v_o);
653
+ select membership_state into after_s from agent_board.board_memberships
654
+ where board_id=v_b and agent_id=v_a
655
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
656
+ created_at desc, id desc limit 1;
657
+ select count(*) into n from agent_board.board_membership_events
658
+ where board_id=v_b and agent_id=v_a;
659
+ insert into _ndmx values (p, r, after_s, n, null);
660
+ exception when others then
661
+ insert into _ndmx values (p, r, null, null, substr(sqlerrm,1,60));
662
+ end;
663
+ end loop;
664
+ end loop;
665
+ end $mx$;
666
+ $$, 'the 6x6 transition matrix runs to completion');
667
+
668
+ select is(
669
+ (select coalesce(array_agg(prior || '+' || req order by prior, req), array[]::text[])
670
+ from _ndmx where err is not null),
671
+ array[]::text[],
672
+ '#317 matrix: no (prior, requested) pair RAISES — a raise here is the silent-truncation failure mode, and no scenario above visits every pair');
673
+
674
+ select is(
675
+ (select coalesce(array_agg(prior || '+' || req order by prior, req), array[]::text[])
676
+ from _ndmx where err is null and events = 0),
677
+ array['joined+invited', 'joined+requested', 'subscribed+invited', 'subscribed+requested']::text[],
678
+ '#317 matrix: EXACTLY the four offer-against-active pairs are suppressed — too narrow drops one, too broad adds one, and this names which');
679
+
680
+ select is(
681
+ (select coalesce(array_agg(prior || '+' || req || '->' || coalesce(after_state,'?') order by prior, req), array[]::text[])
682
+ from _ndmx
683
+ where err is null
684
+ and events <> (case when after_state is not distinct from prior and req is distinct from after_state then 0 else 1 end)),
685
+ array[]::text[],
686
+ '#317 matrix: across all 36 pairs the ledger is written exactly when the state moved or the caller got what they asked for');
687
+
688
+ select * from finish();
689
+ rollback;