@consiliency/agent-board-schema 1.27.0 → 1.35.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 (35) hide show
  1. package/fixtures/already-applied.json +32 -0
  2. package/migrations/20260909000100_agent_board_ledger_revoked_authority.sql +1036 -0
  3. package/migrations/20260910000100_agent_board_ceiling_lock_comment_truth.sql +89 -0
  4. package/migrations/20260911000100_agent_board_ledger_revoked_board_creation.sql +330 -0
  5. package/migrations/20260912000100_agent_board_receipt_subject_gate.sql +348 -0
  6. package/migrations/20260913000100_agent_board_task_thread_lock_order.sql +292 -0
  7. package/migrations/20260914000100_agent_board_claim_subject_gate.sql +500 -0
  8. package/migrations/20260915000100_agent_board_fedsession_stored_identity_gate.sql +353 -0
  9. package/migrations/20260916000100_agent_board_federated_identity_injective_slug.sql +1646 -0
  10. package/package.json +1 -1
  11. package/schema-manifest.json +75 -3
  12. package/src/tooling.test.mjs +47 -2
  13. package/tests/agent_board_atomic_fleet_onboarding.test.sql +161 -30
  14. package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
  15. package/tests/agent_board_enroll_resumption.test.sql +7 -1
  16. package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
  17. package/tests/agent_board_fedsession.test.sql +17 -2
  18. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
  19. package/tests/agent_board_fk_lock_edges.test.sql +422 -0
  20. package/tests/agent_board_ledger_authority.test.sql +1081 -0
  21. package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
  22. package/tests/agent_board_signer_lifecycle.test.sql +29 -6
  23. package/tests/agent_board_source_strip_helper.test.sql +157 -1
  24. package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
  25. package/tests/catalog/p_metadata_parameters.json +20 -0
  26. package/tests/fixtures/test-prereqs.sql +420 -11
  27. package/tests/isolation/expected/ledger_authority_revoke.out +701 -0
  28. package/tests/isolation/expected/signer_lifecycle_revoke.out +1 -1
  29. package/tests/isolation/expected/task_thread_lock_order.out +203 -0
  30. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
  31. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
  32. package/tests/isolation/specs/ledger_authority_revoke.spec +405 -0
  33. package/tests/isolation/specs/signer_lifecycle_revoke.spec +10 -0
  34. package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
  35. package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
@@ -0,0 +1,606 @@
1
+ -- Consiliency/message-board#246 pgTAP: the federated-human return path returns an
2
+ -- existing agent ONLY when that agent's STORED (issuer, subject) equals the pair the
3
+ -- caller presented.
4
+ --
5
+ -- WHY THE COLLISION HERE IS CONSTRUCTED, NOT DERIVED. The owner uid is a truncated
6
+ -- sha256 over a serialization of (issuer, subject). When this file shipped that
7
+ -- serialization was `issuer || '|' || subject`, whose AMBIGUITY was reachable: an OPERATOR
8
+ -- registering a second provider whose issuer carried the complementary suffix produced two
9
+ -- real pairs on one uid. #246 change 2 (20260916000100) replaced it with a length-prefixed
10
+ -- form and that route is gone -- so today reaching one uid from two pairs would be a
11
+ -- preimage-class result against SHA-256, which is not something a test can do. EITHER WAY
12
+ -- this file's gate is the property under test and it does not depend on which
13
+ -- serialization is in force, which is the point: the state is built directly instead: enroll a real
14
+ -- federated human, then REWRITE the (issuer, subject) stored in its
15
+ -- metadata->'federation' stamp. The uid, the handle and the auth.users row are
16
+ -- untouched and still derive from the caller's pair.
17
+ --
18
+ -- That is sound for THIS branch, and the reason is narrow enough to state exactly.
19
+ -- The code under test reads two things and nothing else: the agent found on the
20
+ -- derived uid, and the caller's p_issuer/p_subject. It never re-derives, never
21
+ -- re-hashes, and never consults how the row came to be on that uid. At the point of
22
+ -- the check, a constructed stamp mismatch and a real hash collision are the SAME
23
+ -- STATE. What this file therefore does NOT prove is that a collision is reachable --
24
+ -- that is the encoding question, which is a separate change (#246 change 2) with its
25
+ -- own migration and Portal coordination. This file proves that IF two principals ever
26
+ -- land on one uid, the second is refused.
27
+ --
28
+ -- WHAT IS PINNED STRUCTURALLY RATHER THAN BEHAVIOURALLY, said plainly. There are TWO
29
+ -- return sites: step (7)'s idempotent branch and step (8)'s `exception when
30
+ -- unique_violation` race handler. Only (7) is reachable from a single session -- (7)'s
31
+ -- select is unfiltered on the uid, so any already-committed row on that uid is
32
+ -- intercepted there and the insert in (8) is never attempted. Reaching (8) requires a
33
+ -- concurrent transaction to commit the agent inside the window between (7)'s select
34
+ -- and (8)'s insert. So (8a) is pinned by assertion 5 below, which locates the gate
35
+ -- INSIDE the handler positionally -- not by a count, which cannot tell "both sites
36
+ -- gated" from "one site gated twice" (see the note above assertion 3). A differential
37
+ -- behavioural test of (8a) is a two-session isolationtester follow-up, not something
38
+ -- this file claims.
39
+ --
40
+ -- Runs as postgres (service_role-equivalent), mirroring agent_board_fedsession.test.sql.
41
+
42
+ create extension if not exists pgtap with schema extensions;
43
+ set search_path = extensions, public, agent_board;
44
+
45
+ begin;
46
+
47
+ select plan(29);
48
+
49
+ -- ---------------------------------------------------------------------------
50
+ -- Section A -- the CLASS, derived from pg_proc, not from a list written here
51
+ -- ---------------------------------------------------------------------------
52
+ -- Class C: an agent_board function that BOTH derives a `fed-human-` owner handle
53
+ -- AND reads agent_board.agents keyed on auth_user_id -- i.e. a function that can
54
+ -- hand back a row found only by the ambiguous derivation. Membership is computed
55
+ -- from the catalog on every run, so a NEW function of this shape fails EC-1/EC-2
56
+ -- and NAMES ITSELF rather than being silently outside a hand-written list.
57
+ --
58
+ -- Read through agent_board_test.stripped_body(oid, true) (#196/#199): comments and
59
+ -- quoted identifiers are removed in one pass so this file's own prose -- which
60
+ -- contains the words `fed-human-` and `auth_user_id` -- cannot satisfy it. Literals
61
+ -- are KEPT deliberately, and that is the correct mode here rather than a default:
62
+ -- every marker below IS a literal (the derivation call, the `'{federation,issuer}'`
63
+ -- jsonb path, the raise message, the delegation call names), so stripping literals
64
+ -- would make every assertion in sections A and A2 vacuous.
65
+ --
66
+ -- THE CLASS MARKER MOVED IN #246 (change 2 of 3), and the class did not. When this file
67
+ -- shipped, "derives a fed-human owner uid" was detectable as the `fed-human-` prefix
68
+ -- literal, because every site carried its own copy of the derivation. The derivation is
69
+ -- now the single function agent_board._federated_identity_slug, so that literal survives
70
+ -- in exactly one place and would make this scan find NOTHING -- passing every
71
+ -- "every member has X" assertion vacuously, which is the failure mode assertion 1
72
+ -- exists to stop. The marker is therefore the CALL, and membership is byte-for-byte the
73
+ -- same set it was before: the same five derivation sites, the same one class member.
74
+ create temp view _c246 as
75
+ select p.oid, p.proname, agent_board_test.stripped_body(p.oid, true) as body
76
+ from pg_proc p
77
+ join pg_namespace n on n.oid = p.pronamespace
78
+ where n.nspname = 'agent_board'
79
+ and p.prokind in ('f', 'p')
80
+ and agent_board_test.stripped_body(p.oid, true) like '%\_federated\_identity\_slug(%'
81
+ and agent_board_test.stripped_body(p.oid, true) ~ 'agent_board\.agents[^;]*auth_user_id\s*=';
82
+
83
+ -- 1 -- the class is NOT EMPTY. A class scan that finds nothing passes every
84
+ -- "every member has X" assertion vacuously; this is the assertion that stops that.
85
+ select is((select count(*)::int from _c246), 1,
86
+ 'exactly one agent_board function both derives a fed-human owner uid and reads agents on auth_user_id');
87
+
88
+ -- 2 -- and it is the one we think it is, named POSITIVELY (not "no unexpected members").
89
+ select is((select string_agg(proname, ',' order by proname) from _c246),
90
+ '_enroll_federated_human_after_revocation_gate',
91
+ 'that function is _enroll_federated_human_after_revocation_gate');
92
+
93
+ -- BOTH return sites carry the gate -- asserted PER SITE, not as a count.
94
+ --
95
+ -- A count of 2 was the first version of this and it was FAIL-OPEN in a way worth
96
+ -- recording, because it is the shape CLAUDE.md rule 1 warns about: `count = 2` is
97
+ -- equally satisfied by two gates at step (7) and NONE at step (8). The count cannot
98
+ -- tell "both sites gated" from "one site gated twice", so it does not assert the
99
+ -- property whose name it carries. Mutation M7 builds exactly that body and the count
100
+ -- stays green while the two positional assertions below go red.
101
+ --
102
+ -- The body is split on the handler's own `exception when unique_violation`, which is
103
+ -- the syntactic boundary between the two return sites: everything before it is step
104
+ -- (7)'s idempotent branch, everything after it is step (8)'s race handler.
105
+ -- Every assertion below is written as EVERY-MEMBER-vs-ALL-MEMBERS, comparing two
106
+ -- name lists rather than reading a scalar out of the class. That is not style: the
107
+ -- scalar form (`(select pre_handler from _c246parts)`) was the first version, and
108
+ -- mutation M8 -- which adds a SECOND function to the class -- made it raise
109
+ -- `more than one row returned by a subquery`, ABORTING the file after 2 of 27
110
+ -- assertions. The mutation was still caught, by assertions 1 and 2, but 25
111
+ -- assertions died with it, which is precisely the shape CLAUDE.md rule 3 exists to
112
+ -- stop. In this form a new class member reds these assertions and is NAMED in the
113
+ -- `have:` line instead of killing the run.
114
+ create temp view _c246parts as
115
+ select proname,
116
+ (regexp_split_to_array(body, 'exception\s+when\s+unique_violation'))[1] as pre_handler,
117
+ (regexp_split_to_array(body, 'exception\s+when\s+unique_violation'))[2] as in_handler,
118
+ array_length(regexp_split_to_array(body, 'exception\s+when\s+unique_violation'), 1) as parts
119
+ from _c246;
120
+
121
+ -- 3 -- the split boundary is UNAMBIGUOUS for every member. With a second `exception
122
+ -- when unique_violation` the two assertions below would be reading arbitrary halves,
123
+ -- so this is asserted before anything is read from them.
124
+ select is(
125
+ (select string_agg(proname, ',' order by proname) from _c246parts where parts = 2),
126
+ (select string_agg(proname, ',' order by proname) from _c246),
127
+ 'every class member has exactly one `exception when unique_violation`, so the per-site split is unambiguous');
128
+
129
+ -- 4 -- SITE (7a): the idempotent branch, everything before the handler.
130
+ select is(
131
+ (select string_agg(proname, ',' order by proname) from _c246parts
132
+ where pre_handler ~ 'coalesce\(\s*v_existing\.metadata\s*#>>\s*''\{federation,issuer\}''\s*=\s*p_issuer\s+and\s+v_existing\.metadata\s*#>>\s*''\{federation,subject\}''\s*=\s*p_subject\s*,\s*false\s*\)'),
133
+ (select string_agg(proname, ',' order by proname) from _c246),
134
+ 'every class member carries the gate at return site (7a) -- the idempotent branch, BEFORE the unique_violation handler');
135
+
136
+ -- 5 -- SITE (8a): the race handler, everything after. This is the site a single
137
+ -- session cannot reach (step (7)'s unfiltered select intercepts any committed row),
138
+ -- so this assertion is the ONLY thing standing between it and being ungated.
139
+ select is(
140
+ (select string_agg(proname, ',' order by proname) from _c246parts
141
+ where in_handler ~ 'coalesce\(\s*v_existing\.metadata\s*#>>\s*''\{federation,issuer\}''\s*=\s*p_issuer\s+and\s+v_existing\.metadata\s*#>>\s*''\{federation,subject\}''\s*=\s*p_subject\s*,\s*false\s*\)'),
142
+ (select string_agg(proname, ',' order by proname) from _c246),
143
+ 'every class member carries the gate at return site (8a) -- INSIDE the unique_violation race handler');
144
+
145
+ -- 6 and 7 -- both sites raise the SAME refusal, so neither becomes a
146
+ -- distinguishable oracle and one log phrase covers both. Split PER SITE for the
147
+ -- same reason assertions 4 and 5 are: this was written as a body-wide
148
+ -- `count(matches) = 2` and it was the identical fail-open -- M7 (gate twice at (7),
149
+ -- none at (8)) reded assertion 5 and left the count GREEN, so the count could not
150
+ -- see a site that raises nothing. Finding the shape and fixing it in one place
151
+ -- while leaving it in the assertion immediately below is the "does the fix
152
+ -- reproduce one level down" check, and this is that check's answer. A gate is only
153
+ -- half the property: a body could gate both sites and raise a DIFFERENT message at
154
+ -- one of them, which is why the raise is pinned per site rather than inferred from
155
+ -- the gate's position.
156
+ select is(
157
+ (select string_agg(proname, ',' order by proname) from _c246parts
158
+ where pre_handler ~ 'federated identity mismatch'),
159
+ (select string_agg(proname, ',' order by proname) from _c246),
160
+ 'every class member raises the "federated identity mismatch" refusal at site (7a) -- the idempotent branch');
161
+
162
+ -- 7
163
+ select is(
164
+ (select string_agg(proname, ',' order by proname) from _c246parts
165
+ where in_handler ~ 'federated identity mismatch'),
166
+ (select string_agg(proname, ',' order by proname) from _c246),
167
+ 'every class member raises the "federated identity mismatch" refusal at site (8a) -- INSIDE the race handler');
168
+
169
+ -- ---------------------------------------------------------------------------
170
+ -- Section A2 -- the four NON-members, each exempt for a POSITIVELY ASSERTED reason
171
+ -- ---------------------------------------------------------------------------
172
+ -- The class query above excludes four of the five fed-human derivation sites. An
173
+ -- exemption asserted only by ABSENCE from a scan is the soft spot in every class
174
+ -- test (CLAUDE.md rule 1), so each one's claim is stated positively here and read
175
+ -- from the CATALOG, never from the migration files -- the same reason the fix
176
+ -- targets the delegate and not the wrapper: a migration that introduced a check is
177
+ -- often not the one holding the live one.
178
+
179
+ -- 8 -- the derivation set itself, as an EXACT SET. A new fed-human derivation site
180
+ -- fails this and NAMES ITSELF, which is what makes the four exemptions below a
181
+ -- closed list rather than a list someone remembered to update.
182
+ -- #246: same marker move as the class view above -- a site "derives" by CALLING
183
+ -- agent_board._federated_identity_slug now. The expected set is unchanged, which is the
184
+ -- point: routing six copied expressions through one helper moved no site into or out of
185
+ -- this list. The helper itself is excluded by name because it is the DEFINITION, not a
186
+ -- site; agent_board_federated_identity_injective_slug.test.sql CLS-1/CLS-2 assert from
187
+ -- the other direction that no function outside it holds a derivation of its own.
188
+ select is(
189
+ (select string_agg(p.proname, ',' order by p.proname)
190
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
191
+ where n.nspname = 'agent_board' and p.prokind in ('f', 'p')
192
+ and p.proname <> '_federated_identity_slug'
193
+ and agent_board_test.stripped_body(p.oid, true) like '%\_federated\_identity\_slug(%'),
194
+ '_enroll_federated_human_after_revocation_gate,_enroll_federated_identity_once,enroll_federated_human,enroll_federated_identity,onboard_federated_fleet_agent',
195
+ 'the fed-human derivation set is exactly these five functions');
196
+
197
+ -- 9 -- enroll_federated_identity is exempt because it ALREADY compares the caller's
198
+ -- pair against the creation-time stamp (#176/#244). Asserted on BOTH conjuncts: a
199
+ -- check on the issuer alone would be an exemption whose claim is wider than its code.
200
+ select ok(
201
+ (select agent_board_test.stripped_body(p.oid, true)
202
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
203
+ where n.nspname = 'agent_board' and p.proname = 'enroll_federated_identity')
204
+ ~ 'v_creation_federation\s*->>\s*''issuer''\s*=\s*p_issuer'
205
+ and (select agent_board_test.stripped_body(p.oid, true)
206
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
207
+ where n.nspname = 'agent_board' and p.proname = 'enroll_federated_identity')
208
+ ~ 'v_creation_federation\s*->>\s*''subject''\s*=\s*p_subject',
209
+ 'EXEMPT: enroll_federated_identity already compares BOTH stored issuer and subject to the caller''s pair');
210
+
211
+ -- 10 -- onboard_federated_fleet_agent is exempt because it does not decide identity
212
+ -- itself: it delegates to enroll_federated_identity.
213
+ --
214
+ -- WHAT THIS PINS, exactly: that the CALL APPEARS. Call presence is NOT inheritance
215
+ -- -- a dead call, or one whose result is discarded, would satisfy this identically.
216
+ -- The inheritance argument rests on reading the function (its `select * into
217
+ -- v_enroll from agent_board.enroll_federated_identity(...)` is the sole identity
218
+ -- route, and it raises on that path rather than branching around it), which is a
219
+ -- reviewer's claim and not this assertion's. What the assertion DOES buy is that
220
+ -- the delegation cannot be silently removed without a named failure here, which is
221
+ -- the change that would make the exemption false.
222
+ select ok(
223
+ (select agent_board_test.stripped_body(p.oid, true)
224
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
225
+ where n.nspname = 'agent_board' and p.proname = 'onboard_federated_fleet_agent')
226
+ ~ 'agent_board\.enroll_federated_identity\s*\(',
227
+ 'EXEMPT: onboard_federated_fleet_agent CALLS agent_board.enroll_federated_identity (call presence only -- see comment)');
228
+
229
+ -- 11 -- _enroll_federated_identity_once is exempt because it has no existing-agent
230
+ -- return path to gate at all: it ALWAYS creates.
231
+ --
232
+ -- WHAT THIS PINS, exactly: that an INSERT into agent_board.agents is present. An
233
+ -- insert proves neither "always" nor "no return path" -- both of those are read off
234
+ -- the function, not measured here. The absence half comes from the CLASS QUERY
235
+ -- (assertions 1-2): this function derives a fed-human handle but does not read
236
+ -- agent_board.agents on auth_user_id, which is why it is not a member. So the
237
+ -- exemption rests on the class query for the absence and on this assertion for the
238
+ -- positive half, and neither is asked to carry the other's weight.
239
+ --
240
+ -- The trailing `\s*\(` is load-bearing, not tidiness. Without it the pattern was
241
+ -- `agent_board\.agents` unanchored, which a mutation renaming the target to
242
+ -- `agent_board.agents_MUTATED` still satisfied by PREFIX -- the assertion stayed
243
+ -- green while the claim it makes was false. Caught by mutation M11 producing no
244
+ -- failure at all, which is the only reason it is not still there.
245
+ select ok(
246
+ (select agent_board_test.stripped_body(p.oid, true)
247
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
248
+ where n.nspname = 'agent_board' and p.proname = '_enroll_federated_identity_once')
249
+ ~ 'insert\s+into\s+agent_board\.agents\s*\(',
250
+ 'EXEMPT: _enroll_federated_identity_once INSERTS into agent_board.agents (insert presence only -- see comment)');
251
+
252
+ -- 12 -- enroll_federated_human is exempt because 20260901000130 reduced it to a
253
+ -- wrapper: it derives the uid for the owner lock, then delegates. This is the
254
+ -- assertion that would have caught the brief''s misattribution.
255
+ --
256
+ -- `agent_board\.` is OPTIONAL on purpose. The function pins
257
+ -- `search_path = agent_board, public`, so a bare `_enroll_federated_human_after_
258
+ -- revocation_gate(...)` resolves identically; requiring the qualified spelling would
259
+ -- make this red on a legitimate refactor that changed nothing. That direction is a
260
+ -- false alarm rather than a hole, but a class assertion nobody trusts gets deleted.
261
+ select ok(
262
+ (select agent_board_test.stripped_body(p.oid, true)
263
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
264
+ where n.nspname = 'agent_board' and p.proname = 'enroll_federated_human')
265
+ ~ '(agent_board\.)?_enroll_federated_human_after_revocation_gate\s*\(',
266
+ 'EXEMPT: enroll_federated_human is a wrapper -- it delegates the return path to the gated function');
267
+
268
+ -- ---------------------------------------------------------------------------
269
+ -- Fixtures -- one approved provider, one active default board, seven identities
270
+ -- ---------------------------------------------------------------------------
271
+ create temp table _g (
272
+ iss text, aud text, board_id uuid,
273
+ sub_l text, agent_l uuid, handle_l text, -- L: legitimate owner, stamp untouched
274
+ sub_m text, agent_m uuid, -- M: BOTH stamp fields rewritten
275
+ sub_i text, agent_i uuid, -- I: only the stamp ISSUER rewritten
276
+ sub_s text, agent_s uuid, -- S: only the stamp SUBJECT rewritten
277
+ sub_x text, agent_x uuid, -- X: stamp rewritten AND status suspended
278
+ sub_n text, agent_n uuid, -- N: no federation stamp at all
279
+ sub_p text, agent_p uuid, -- P: federation stamp missing only `subject`
280
+ sub_q text, agent_q uuid -- Q: federation stamp missing only `issuer`
281
+ ) on commit drop;
282
+ grant all on table _g to authenticated, service_role;
283
+
284
+ create temp table _err (label text primary key, sqlstate text, msg text) on commit drop;
285
+ grant all on table _err to authenticated, service_role;
286
+
287
+ do $$
288
+ declare
289
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
290
+ v_iss text; v_board uuid; v_owner uuid;
291
+ v_l uuid; v_hl text; v_m uuid; v_i uuid; v_ss uuid; v_x uuid; v_n uuid; v_p uuid;
292
+ v_sub_l text; v_sub_m text; v_sub_i text; v_sub_s text; v_sub_x text; v_sub_n text; v_sub_p text;
293
+ v_sub_q text; v_q uuid;
294
+ v_other_iss text := 'https://other-idp-' || s || '.test';
295
+ v_other_sub text := 'other-principal-' || s;
296
+ begin
297
+ v_iss := 'https://fedid-' || s || '.test';
298
+ v_sub_l := 'g246-legit-' || s;
299
+ v_sub_m := 'g246-both-' || s;
300
+ v_sub_i := 'g246-issuer-' || s;
301
+ v_sub_s := 'g246-subject-' || s;
302
+ v_sub_x := 'g246-suspend-' || s;
303
+ v_sub_n := 'g246-nostamp-' || s;
304
+ v_sub_p := 'g246-partial-' || s;
305
+ v_sub_q := 'g246-noiss-' || s;
306
+
307
+ insert into agent_board.agents(agent_type, kind, handle, display_name, status)
308
+ values ('system', 'system'::agent_board.agent_kind, 'g246-owner-' || s, 'G246 Owner', 'active')
309
+ returning id into v_owner;
310
+
311
+ insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
312
+ values ('g246-board-' || s, 'G246 Board', v_owner, 'private', 'active')
313
+ returning id into v_board;
314
+
315
+ -- default_board_id is set so the auto-join side effect is LIVE on the return path;
316
+ -- without it EC-13 below would pass no matter what the gate did.
317
+ insert into agent_board.identity_providers
318
+ (issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes,
319
+ allowed_agent_kinds, default_board_id)
320
+ values (v_iss, 'message-board', true, true, '{}', '{}', '{human}', v_board);
321
+
322
+ select e.agent_id, e.handle into v_l, v_hl from agent_board.enroll_federated_human(
323
+ v_iss, 'message-board', v_sub_l,
324
+ jsonb_build_object('sub', v_sub_l, 'email_verified', false), 'Legit') e;
325
+ select e.agent_id into v_m from agent_board.enroll_federated_human(
326
+ v_iss, 'message-board', v_sub_m,
327
+ jsonb_build_object('sub', v_sub_m, 'email_verified', false), 'Both') e;
328
+ select e.agent_id into v_i from agent_board.enroll_federated_human(
329
+ v_iss, 'message-board', v_sub_i,
330
+ jsonb_build_object('sub', v_sub_i, 'email_verified', false), 'IssuerOnly') e;
331
+ select e.agent_id into v_ss from agent_board.enroll_federated_human(
332
+ v_iss, 'message-board', v_sub_s,
333
+ jsonb_build_object('sub', v_sub_s, 'email_verified', false), 'SubjectOnly') e;
334
+ select e.agent_id into v_x from agent_board.enroll_federated_human(
335
+ v_iss, 'message-board', v_sub_x,
336
+ jsonb_build_object('sub', v_sub_x, 'email_verified', false), 'Suspended') e;
337
+ select e.agent_id into v_n from agent_board.enroll_federated_human(
338
+ v_iss, 'message-board', v_sub_n,
339
+ jsonb_build_object('sub', v_sub_n, 'email_verified', false), 'NoStamp') e;
340
+ select e.agent_id into v_p from agent_board.enroll_federated_human(
341
+ v_iss, 'message-board', v_sub_p,
342
+ jsonb_build_object('sub', v_sub_p, 'email_verified', false), 'Partial') e;
343
+ select e.agent_id into v_q from agent_board.enroll_federated_human(
344
+ v_iss, 'message-board', v_sub_q,
345
+ jsonb_build_object('sub', v_sub_q, 'email_verified', false), 'NoIssuer') e;
346
+
347
+ -- THE CONSTRUCTION. Rewrite the stored stamp so it names a DIFFERENT principal
348
+ -- while the uid, handle and auth.users row still derive from the caller's pair --
349
+ -- the state a real collision produces, built directly (see the file header).
350
+ update agent_board.agents a
351
+ set metadata = jsonb_set(jsonb_set(a.metadata, '{federation,issuer}', to_jsonb(v_other_iss), true),
352
+ '{federation,subject}', to_jsonb(v_other_sub), true)
353
+ where a.id in (v_m, v_x);
354
+ -- Only the ISSUER differs (subject still matches the caller) -- pins that the
355
+ -- issuer conjunct is load-bearing on its own.
356
+ update agent_board.agents a
357
+ set metadata = jsonb_set(a.metadata, '{federation,issuer}', to_jsonb(v_other_iss), true)
358
+ where a.id = v_i;
359
+ -- Only the SUBJECT differs (issuer still matches) -- pins the subject conjunct.
360
+ update agent_board.agents a
361
+ set metadata = jsonb_set(a.metadata, '{federation,subject}', to_jsonb(v_other_sub), true)
362
+ where a.id = v_ss;
363
+ -- No stamp at all, and a stamp missing exactly one field: both must FAIL CLOSED.
364
+ update agent_board.agents a set metadata = a.metadata - 'federation' where a.id = v_n;
365
+ update agent_board.agents a
366
+ set metadata = jsonb_set(a.metadata, '{federation}', (a.metadata -> 'federation') - 'subject', true)
367
+ where a.id = v_p;
368
+ -- Q: stamp missing only `issuer`. The migration header claims fail-closed when the
369
+ -- stamp lacks issuer OR subject, and until this fixture existed only the subject
370
+ -- half had one -- a claim wider than any assertion behind it. Note the surviving
371
+ -- SUBJECT conjunct would refuse a genuinely different caller anyway, so this
372
+ -- fixture presents the CORRECT subject: the refusal can then only come from the
373
+ -- NULL issuer conjunct.
374
+ update agent_board.agents a
375
+ set metadata = jsonb_set(a.metadata, '{federation}', (a.metadata -> 'federation') - 'issuer', true)
376
+ where a.id = v_q;
377
+ -- X is additionally suspended, so its refusal can be compared against M's.
378
+ update agent_board.agents a set status = 'suspended', updated_at = now() where a.id = v_x;
379
+
380
+ -- Delete M's auto-join membership so a re-run of the return path WOULD recreate it
381
+ -- (auto-join is first-time-only; without this deletion EC-13 could not fail).
382
+ delete from agent_board.board_memberships bm where bm.agent_id = v_m and bm.board_id = v_board;
383
+
384
+ insert into _g values (
385
+ v_iss, 'message-board', v_board,
386
+ v_sub_l, v_l, v_hl, v_sub_m, v_m, v_sub_i, v_i, v_sub_s, v_ss,
387
+ v_sub_x, v_x, v_sub_n, v_n, v_sub_p, v_p, v_sub_q, v_q);
388
+ end
389
+ $$;
390
+
391
+ -- ---------------------------------------------------------------------------
392
+ -- Section B -- THE REGRESSION RISK: the legitimate owner must still be returned
393
+ -- ---------------------------------------------------------------------------
394
+ -- Asserted POSITIVELY and first, while every fixture is pristine. A careless
395
+ -- equality check breaks idempotency for a real owner, and that failure mode is worse
396
+ -- for this deployment than the collision it defends against.
397
+ create temp table _idem_before as
398
+ select count(*)::int as n from agent_board.enrollment_federation_audit e
399
+ where e.agent_id = (select agent_l from _g);
400
+
401
+ -- 13 -- #113: an allow-side call NEVER runs inside an is() value expression; a raise
402
+ -- there aborts the file with no `not ok`. lives_ok captures, then assertions read.
403
+ select lives_ok(
404
+ (select format($q$create temp table _idem as
405
+ select * from agent_board.enroll_federated_human(%L, 'message-board', %L, %L::jsonb, 'Legit')$q$,
406
+ iss, sub_l, jsonb_build_object('sub', sub_l, 'email_verified', false)::text) from _g),
407
+ 'the legitimate owner re-presenting its OWN (issuer, subject) is accepted [call succeeds]');
408
+
409
+ -- 14
410
+ select is((select agent_id from _idem), (select agent_l from _g),
411
+ 'the owner gets ITS OWN agent back (idempotency preserved)');
412
+
413
+ -- 15
414
+ select is((select kind || '/' || status from _idem), 'human/active',
415
+ 'the returned identity is still an active human');
416
+
417
+ -- 16
418
+ select is((select handle from _idem), (select handle_l from _g),
419
+ 'the returned handle is unchanged (the derivation is untouched by this change)');
420
+
421
+ -- 17 -- the idempotent return still appends its audit row: the gate refuses, it does
422
+ -- not silently degrade the accepted path into a no-op.
423
+ select is(
424
+ (select count(*)::int from agent_board.enrollment_federation_audit e
425
+ where e.agent_id = (select agent_l from _g)) - (select n from _idem_before),
426
+ 1,
427
+ 'the accepted re-provision appends exactly one audit row');
428
+
429
+ -- ---------------------------------------------------------------------------
430
+ -- Section C -- a MISMATCHED caller is refused, and each conjunct is load-bearing
431
+ -- ---------------------------------------------------------------------------
432
+ create temp table _audit_before as
433
+ select count(*)::int as n from agent_board.enrollment_federation_audit e
434
+ where e.agent_id = (select agent_m from _g);
435
+
436
+ -- 18
437
+ select throws_ok(
438
+ (select format($q$select agent_board.enroll_federated_human(%L, 'message-board', %L, %L::jsonb, null)$q$,
439
+ iss, sub_m, jsonb_build_object('sub', sub_m, 'email_verified', false)::text) from _g),
440
+ '42501', null,
441
+ 'a caller whose pair differs from the stored stamp in BOTH fields is refused (42501)');
442
+
443
+ -- 19 -- issuer differs, subject matches. Fails if the issuer conjunct is dropped.
444
+ select throws_ok(
445
+ (select format($q$select agent_board.enroll_federated_human(%L, 'message-board', %L, %L::jsonb, null)$q$,
446
+ iss, sub_i, jsonb_build_object('sub', sub_i, 'email_verified', false)::text) from _g),
447
+ '42501', null,
448
+ 'a caller whose stored stamp ISSUER differs (subject matches) is refused (42501)');
449
+
450
+ -- 20 -- subject differs, issuer matches. Fails if the subject conjunct is dropped.
451
+ select throws_ok(
452
+ (select format($q$select agent_board.enroll_federated_human(%L, 'message-board', %L, %L::jsonb, null)$q$,
453
+ iss, sub_s, jsonb_build_object('sub', sub_s, 'email_verified', false)::text) from _g),
454
+ '42501', null,
455
+ 'a caller whose stored stamp SUBJECT differs (issuer matches) is refused (42501)');
456
+
457
+ -- Capture the actual refusal texts for the message + no-oracle assertions below.
458
+ do $$
459
+ declare r record; v_state text; v_msg text;
460
+ begin
461
+ select * into r from _g;
462
+
463
+ -- An email is supplied HERE deliberately. Step (7)'s accepted branch MERGES it into
464
+ -- the owner's auth.users.raw_user_meta_data before returning, so under the defect a
465
+ -- colliding caller writes ITS OWN email onto the VICTIM's owner row. EC-16 asserts
466
+ -- that did not happen.
467
+ begin
468
+ perform agent_board.enroll_federated_human(r.iss, 'message-board', r.sub_m,
469
+ jsonb_build_object('sub', r.sub_m, 'email', 'attacker-' || r.sub_m || '@example.test',
470
+ 'email_verified', true), null);
471
+ exception when others then
472
+ get stacked diagnostics v_state = returned_sqlstate, v_msg = message_text;
473
+ insert into _err values ('mismatch_active', v_state, v_msg);
474
+ end;
475
+
476
+ begin
477
+ perform agent_board.enroll_federated_human(r.iss, 'message-board', r.sub_x,
478
+ jsonb_build_object('sub', r.sub_x, 'email_verified', false), null);
479
+ exception when others then
480
+ get stacked diagnostics v_state = returned_sqlstate, v_msg = message_text;
481
+ insert into _err values ('mismatch_suspended', v_state, v_msg);
482
+ end;
483
+
484
+ begin
485
+ perform agent_board.enroll_federated_human(r.iss, 'message-board', r.sub_n,
486
+ jsonb_build_object('sub', r.sub_n, 'email_verified', false), null);
487
+ exception when others then
488
+ get stacked diagnostics v_state = returned_sqlstate, v_msg = message_text;
489
+ insert into _err values ('no_stamp', v_state, v_msg);
490
+ end;
491
+
492
+ begin
493
+ perform agent_board.enroll_federated_human(r.iss, 'message-board', r.sub_p,
494
+ jsonb_build_object('sub', r.sub_p, 'email_verified', false), null);
495
+ exception when others then
496
+ get stacked diagnostics v_state = returned_sqlstate, v_msg = message_text;
497
+ insert into _err values ('partial_stamp', v_state, v_msg);
498
+ end;
499
+
500
+ begin
501
+ perform agent_board.enroll_federated_human(r.iss, 'message-board', r.sub_q,
502
+ jsonb_build_object('sub', r.sub_q, 'email_verified', false), null);
503
+ exception when others then
504
+ get stacked diagnostics v_state = returned_sqlstate, v_msg = message_text;
505
+ insert into _err values ('no_issuer', v_state, v_msg);
506
+ end;
507
+ end
508
+ $$;
509
+
510
+ -- 21 -- the refusal is the #246 refusal, not some other 42501 that happens to fire.
511
+ select matches(
512
+ (select msg from _err where label = 'mismatch_active'),
513
+ '^federated identity mismatch: ',
514
+ 'the refusal is the #246 stored-identity refusal, identifiable by its own phrase');
515
+
516
+ -- 22 -- IT LEAKS NOTHING ABOUT THE OTHER PRINCIPAL. The message is echoed verbatim to
517
+ -- the UNTRUSTED caller: supabase/functions/portal-session/index.ts returns
518
+ -- {error:"provision_rejected", detail:<message>} with HTTP 403. So it must name no
519
+ -- handle, no agent id, and neither of the stored pair's values.
520
+ select ok(
521
+ (select msg from _err where label = 'mismatch_active')
522
+ not like '%' || (select metadata #>> '{federation,issuer}' from agent_board.agents where id = (select agent_m from _g)) || '%'
523
+ and (select msg from _err where label = 'mismatch_active')
524
+ not like '%' || (select metadata #>> '{federation,subject}' from agent_board.agents where id = (select agent_m from _g)) || '%'
525
+ and (select msg from _err where label = 'mismatch_active')
526
+ not like '%' || (select handle from agent_board.agents where id = (select agent_m from _g)) || '%'
527
+ and (select msg from _err where label = 'mismatch_active')
528
+ not like '%' || (select agent_m from _g)::text || '%',
529
+ 'the refusal names neither the stored issuer/subject, nor the handle, nor the agent id of the row it refused');
530
+
531
+ -- ---------------------------------------------------------------------------
532
+ -- Section D -- the refusal has NO side effect, which is the damage #246 names
533
+ -- ---------------------------------------------------------------------------
534
+ -- 23 -- the audit row is the poisoning: under the defect, the mismatched caller's
535
+ -- (issuer, subject) was appended onto the VICTIM's agent_id, corrupting the stored
536
+ -- evidence enroll_federated_identity's #176/#244 resumption conjunction reads.
537
+ select is(
538
+ (select count(*)::int from agent_board.enrollment_federation_audit e
539
+ where e.agent_id = (select agent_m from _g)),
540
+ (select n from _audit_before),
541
+ 'a refused call appends NO audit row onto the agent it refused (no evidence poisoning)');
542
+
543
+ -- 24 -- nor does the refused caller's EMAIL reach the victim's owner row. Step (7)'s
544
+ -- accepted branch merges `fedsession_email` into auth.users.raw_user_meta_data before
545
+ -- returning; under the defect that is the attacker's address written onto the owner
546
+ -- auth.users row of the identity it collided with.
547
+ select is(
548
+ (select count(*)::int from auth.users u, agent_board.agents a, _g g
549
+ where a.id = g.agent_m and u.id = a.auth_user_id
550
+ and u.raw_user_meta_data ->> 'fedsession_email' is not null),
551
+ 0,
552
+ 'a refused call writes no fedsession_email onto the owner auth.users row it refused');
553
+
554
+ -- 25 -- the auto-join side effect does not fire either. M's membership was deleted in
555
+ -- the fixture, so the return path WOULD recreate it (auto-join is first-time-only).
556
+ select is(
557
+ (select count(*)::int from agent_board.board_memberships bm, _g g
558
+ where bm.agent_id = g.agent_m and bm.board_id = g.board_id),
559
+ 0,
560
+ 'a refused call performs no default-board auto-join for the agent it refused');
561
+
562
+ -- ---------------------------------------------------------------------------
563
+ -- Section E -- NO ORACLE: the gate runs before the status and kind branches
564
+ -- ---------------------------------------------------------------------------
565
+ -- 26 -- X is mismatched AND suspended. If the gate were placed after the status
566
+ -- branch, this would be the 'a non-active identity already exists' refusal instead,
567
+ -- telling a colliding caller the victim's account state.
568
+ select is(
569
+ (select sqlstate || ' ' || msg from _err where label = 'mismatch_suspended'),
570
+ (select sqlstate || ' ' || msg from _err where label = 'mismatch_active'),
571
+ 'a mismatched caller gets the IDENTICAL refusal whether the existing agent is active or suspended');
572
+
573
+ -- ---------------------------------------------------------------------------
574
+ -- Section F -- FAIL CLOSED on a missing or partial stamp
575
+ -- ---------------------------------------------------------------------------
576
+ -- Measured before choosing this posture: on the canonical project (2026-08-16) there
577
+ -- is 1 agent reachable on this path, 0 of them lack a stamp, and all 110 of its audit
578
+ -- rows agree with the stamp on both fields -- so refusing breaks no live owner. The
579
+ -- creation branch has written both fields unconditionally since 20260728000100:243.
580
+ -- 27
581
+ select is(
582
+ (select sqlstate || '|' || (msg like 'federated identity mismatch:%')::text
583
+ from _err where label = 'no_stamp'),
584
+ '42501|true',
585
+ 'an agent with NO metadata->federation stamp is refused, not admitted (fail closed)');
586
+
587
+ -- 28
588
+ select is(
589
+ (select sqlstate || '|' || (msg like 'federated identity mismatch:%')::text
590
+ from _err where label = 'partial_stamp'),
591
+ '42501|true',
592
+ 'an agent whose stamp is missing only `subject` is refused (a NULL conjunct reads as NOT-mine)');
593
+
594
+ -- 29 -- the missing-ISSUER-only half of the fail-closed claim. Added because the
595
+ -- migration header named it and no fixture covered it -- a claim wider than its
596
+ -- evidence, which is the exact shape this bundle argues against. The caller here
597
+ -- presents the CORRECT subject, so the surviving subject conjunct cannot be what
598
+ -- refuses: only the NULL issuer conjunct can.
599
+ select is(
600
+ (select sqlstate || '|' || (msg like 'federated identity mismatch:%')::text
601
+ from _err where label = 'no_issuer'),
602
+ '42501|true',
603
+ 'an agent whose stamp is missing only `issuer` is refused, with the correct subject presented');
604
+
605
+ select * from finish();
606
+ rollback;