@consiliency/agent-board-schema 1.28.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 (32) hide show
  1. package/fixtures/already-applied.json +28 -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/package.json +1 -1
  10. package/schema-manifest.json +66 -3
  11. package/src/tooling.test.mjs +47 -2
  12. package/tests/agent_board_atomic_fleet_onboarding.test.sql +144 -29
  13. package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
  14. package/tests/agent_board_enroll_resumption.test.sql +7 -1
  15. package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
  16. package/tests/agent_board_fedsession.test.sql +17 -2
  17. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
  18. package/tests/agent_board_fk_lock_edges.test.sql +422 -0
  19. package/tests/agent_board_ledger_authority.test.sql +142 -71
  20. package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
  21. package/tests/agent_board_signer_lifecycle.test.sql +23 -6
  22. package/tests/agent_board_source_strip_helper.test.sql +157 -1
  23. package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
  24. package/tests/catalog/p_metadata_parameters.json +12 -0
  25. package/tests/fixtures/test-prereqs.sql +420 -11
  26. package/tests/isolation/expected/ledger_authority_revoke.out +219 -1
  27. package/tests/isolation/expected/task_thread_lock_order.out +203 -0
  28. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
  29. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
  30. package/tests/isolation/specs/ledger_authority_revoke.spec +115 -0
  31. package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
  32. package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
@@ -194,7 +194,13 @@ select is(
194
194
  join pg_namespace n on n.oid = p.pronamespace
195
195
  where n.nspname = 'agent_board'
196
196
  and p.prokind = 'f'
197
- and agent_board_test.strip_sql(p.prosrc) ~* 'insert into agent_board\.agent_signing_keys'),
197
+ -- stripped_body(p.oid), NOT strip_sql(p.prosrc): a SQL-standard-body function
198
+ -- keeps its body in prosqlbody and leaves prosrc EMPTY, so the prosrc form read
199
+ -- '' for it and this exact-set assertion would have reported the expected set
200
+ -- while a third signing-key creator was live (message-board#293). This site was
201
+ -- invisible to the #196 lint because that lint only inspects POSITIONAL and
202
+ -- NEGATIVE assertions, and this is a PRESENCE match -- see the note on the lint.
203
+ and agent_board_test.stripped_body(p.oid) ~* 'insert into agent_board\.agent_signing_keys'),
198
204
  '_enroll_federated_identity_once, mint_delegated_child',
199
205
  'EC-2: only the enroll creator and delegated-child mint insert signing keys (strip_sql-derived)');
200
206
 
@@ -0,0 +1,388 @@
1
+ -- message-board#246 (change 2 of 3) — the federated identity is derived through ONE
2
+ -- injective serialization, and the ambiguous one is gone from the catalog.
3
+ --
4
+ -- WHAT THIS FILE HAS TO PROVE, and the shape of each proof:
5
+ --
6
+ -- INJ-* The helper is injective where the old encoding was not. INJ-1 is not a
7
+ -- "these two differ" curiosity: INJ-0 first proves the same two pairs DID
8
+ -- collide under the old expression, so the fixture is the adversarial case
9
+ -- from the issue and not an arbitrary pair. INJ-2 generalises to a family
10
+ -- whose members differ only in where the separator falls, and names the
11
+ -- offenders on failure instead of reporting a total.
12
+ -- BF-* The SHIPPED backfill moved four NAMED pre-existing rows -- one per recovery
13
+ -- arm, plus one whose meta/identity values diverge from its slug -- across all
14
+ -- four surfaces that carry the slug -- and, the assertion this
15
+ -- whole change exists for, the #244 resumption lookup still resolves each pair
16
+ -- to the SAME owner uuid it resolved to before. Re-deriving the handle without
17
+ -- re-keying auth.users.email orphans the owner; that failure looks exactly
18
+ -- like a cosmetic relabel, so it is asserted per row, by uuid, not as a
19
+ -- property.
20
+ -- CLS-* Catalog-derived class invariants. The completeness claim is NOT "I grepped
21
+ -- the files": pg_proc is asked which functions still contain a derivation and
22
+ -- which route through the helper, and the answers are compared as NAME LISTS,
23
+ -- so a sixth site fails and names itself. Source scans go through
24
+ -- agent_board_test.stripped_body, the suite's one-pass comment/literal strip,
25
+ -- because a prosrc scan that does not strip is defeated by prose -- the
26
+ -- migration's own header quotes the old expression in a comment.
27
+ --
28
+ -- WHY THE FIXTURES ARE A PRE-MIGRATION SEED, not rows created here: the backfill runs
29
+ -- once, inside the migration. Rows created inside this transaction would be created
30
+ -- after it had already run. See tests/seeds/20260916000100_*.pre.sql for the four
31
+ -- identities and why each exists.
32
+ --
33
+ -- WHAT IS NOT CLAIMED. The backfill's REFUSAL arms (a row whose pair cannot be
34
+ -- recovered, or one with two distinct reproducing pairs) are not directly asserted
35
+ -- here, because a fixture that triggers them aborts the migration and therefore the
36
+ -- whole replay -- there is no green run left in which to assert. They are pinned
37
+ -- INDIRECTLY and that is stated rather than glossed: F1 carries two agents with the
38
+ -- same pair, so a backfill counting rows instead of DISTINCT pairs refuses F1 and every
39
+ -- BF-1 assertion dies with the migration; F2 is reachable only through owner_user_id;
40
+ -- F3 carries a decoy stamp that only the "reproduces the current slug" filter excludes.
41
+ -- Each of those three mutations makes the migration itself abort with a message naming
42
+ -- the offending uid, which is a louder failure than a `not ok`, but it is a failure of a
43
+ -- different kind and the mutation table reports it as such, with an assertion count of 0.
44
+
45
+ create extension if not exists pgtap with schema extensions;
46
+ set search_path = extensions, public;
47
+
48
+ begin;
49
+
50
+ select plan(34);
51
+
52
+ -- ---------------------------------------------------------------------------
53
+ -- INJ: the serialization
54
+ -- ---------------------------------------------------------------------------
55
+
56
+ -- INJ-0. The premise. Under the OLD expression these two distinct pairs -- the exact
57
+ -- example in the issue -- produce ONE slug. Without this, INJ-1 proves nothing: any two
58
+ -- unrelated pairs differ under any hash.
59
+ select is(
60
+ 'fed-human-' || substr(encode(extensions.digest(convert_to(
61
+ 'https://idp.example' || '|' || 'user|123', 'UTF8'), 'sha256'), 'hex'), 1, 32),
62
+ 'fed-human-' || substr(encode(extensions.digest(convert_to(
63
+ 'https://idp.example|user' || '|' || '123', 'UTF8'), 'sha256'), 'hex'), 1, 32),
64
+ 'INJ-0: under the OLD encoding ("https://idp.example","user|123") and ("https://idp.example|user","123") derive the SAME slug');
65
+
66
+ -- INJ-1. The same two pairs under the helper. Named individually so the failure line
67
+ -- shows which slug moved.
68
+ select isnt(
69
+ agent_board._federated_identity_slug('https://idp.example', 'user|123'),
70
+ agent_board._federated_identity_slug('https://idp.example|user', '123'),
71
+ 'INJ-1: under the helper ("https://idp.example","user|123") and ("https://idp.example|user","123") derive DIFFERENT slugs');
72
+
73
+ -- INJ-2. The family, not the instance: nine pairs whose concatenations are equal or
74
+ -- near-equal and which differ only in where the boundary falls. The assertion compares
75
+ -- a NAME LIST of colliding tag-pairs against '(none)', so a regression prints the tags
76
+ -- that collided rather than a count that says only "some did".
77
+ --
78
+ -- Tags h/i carry their own weight: they collide under a COLON separator, not under a
79
+ -- pipe one, so they fail a serialization that swapped the delimiter for the one the
80
+ -- netstring form also uses while dropping the length prefixes. Without them, INJ-2 would
81
+ -- red only for mutations INJ-1 already reds, and would be decoration.
82
+ select is(
83
+ (with p(tag, iss, sub) as (values
84
+ ('a', 'https://idp.example', 'user|123'),
85
+ ('b', 'https://idp.example|user', '123'),
86
+ ('c', 'https://idp.example|', 'user|123'),
87
+ ('d', 'https://idp.example', '|user|123'),
88
+ ('e', 'ab', 'c'),
89
+ ('f', 'a', 'bc'),
90
+ ('g', 'a|b', 'c'),
91
+ ('h', 'x', 'y:z'),
92
+ ('i', 'x:y', 'z'))
93
+ select coalesce(string_agg(x.pair, ', ' order by x.pair), '(none)')
94
+ from (select distinct p1.tag || '/' || p2.tag as pair
95
+ from p p1 join p p2
96
+ on p1.tag < p2.tag
97
+ and agent_board._federated_identity_slug(p1.iss, p1.sub)
98
+ = agent_board._federated_identity_slug(p2.iss, p2.sub)) x),
99
+ '(none)',
100
+ 'INJ-2: no two of the nine boundary-shifted pairs share a slug (offending tag pairs are named)');
101
+
102
+ -- INJ-3..6. Fail closed on a missing component rather than deriving one shared
103
+ -- degenerate owner. Four separate assertions because they are four separate conjuncts:
104
+ -- a single "null input raises" would be satisfied by a guard on issuer alone.
105
+ select throws_ok(
106
+ $$select agent_board._federated_identity_slug(null, 'sub')$$,
107
+ '22023',
108
+ 'federated identity slug requires a non-empty issuer and subject',
109
+ 'INJ-3: a NULL issuer raises 22023 instead of deriving a slug');
110
+ select throws_ok(
111
+ $$select agent_board._federated_identity_slug('', 'sub')$$,
112
+ '22023',
113
+ 'federated identity slug requires a non-empty issuer and subject',
114
+ 'INJ-4: an EMPTY issuer raises 22023 instead of deriving a slug');
115
+ select throws_ok(
116
+ $$select agent_board._federated_identity_slug('https://idp.example', null)$$,
117
+ '22023',
118
+ 'federated identity slug requires a non-empty issuer and subject',
119
+ 'INJ-5: a NULL subject raises 22023 instead of deriving a slug');
120
+ select throws_ok(
121
+ $$select agent_board._federated_identity_slug('https://idp.example', '')$$,
122
+ '22023',
123
+ 'federated identity slug requires a non-empty issuer and subject',
124
+ 'INJ-6: an EMPTY subject raises 22023 instead of deriving a slug');
125
+
126
+ -- ---------------------------------------------------------------------------
127
+ -- BF: the three named pre-existing identities the shipped backfill re-keyed
128
+ -- ---------------------------------------------------------------------------
129
+
130
+ -- F1 — the HUMAN arm. uid d2460001-0000-4000-8000-000000000001,
131
+ -- pair ("https://fixture-246.example/idp-a", "fixture-246-subject-a"),
132
+ -- slug fed-human-d50f81b6913e066dba4c19f5cb6b202a -> fed-human-6c056fe12e1fb58fb2c072ff8bd9a613.
133
+
134
+ select is(
135
+ (select u.email from auth.users u where u.id = 'd2460001-0000-4000-8000-000000000001'),
136
+ 'agent+fed-human-6c056fe12e1fb58fb2c072ff8bd9a613@agent-board.local',
137
+ 'BF-1a: F1 auth.users.email re-keyed to the new slug (the value _ensure_agent_auth_user looks up)');
138
+
139
+ select is(
140
+ (select u.raw_user_meta_data->>'slug' from auth.users u
141
+ where u.id = 'd2460001-0000-4000-8000-000000000001'),
142
+ 'fed-human-6c056fe12e1fb58fb2c072ff8bd9a613',
143
+ 'BF-1b: F1 auth.users.raw_user_meta_data->>slug re-keyed to the new slug');
144
+
145
+ select is(
146
+ (select i.provider_id || '|' || (i.identity_data->>'sub') || '|' || (i.identity_data->>'slug')
147
+ from auth.identities i where i.user_id = 'd2460001-0000-4000-8000-000000000001'),
148
+ 'fed-human-6c056fe12e1fb58fb2c072ff8bd9a613|fed-human-6c056fe12e1fb58fb2c072ff8bd9a613|fed-human-6c056fe12e1fb58fb2c072ff8bd9a613',
149
+ 'BF-1c: F1 auth.identities provider_id AND identity_data.sub AND identity_data.slug all re-keyed (each position shown)');
150
+
151
+ select is(
152
+ (select a.handle from agent_board.agents a
153
+ where a.id = 'd2460101-0000-4000-8000-000000000001'),
154
+ 'fed-human-6c056fe12e1fb58fb2c072ff8bd9a613',
155
+ 'BF-1d: F1 human agent handle re-keyed (the handle IS the slug on this path)');
156
+
157
+ -- THE ASSERTION THIS CHANGE EXISTS FOR. enroll_federated_identity resolves the resuming
158
+ -- owner by a pure lookup on format('agent+%s@agent-board.local', <derived slug>). If the
159
+ -- derivation moved and the email did not, this returns NULL, the non-null owner
160
+ -- conjunct fails, and the owner cannot resume. Asserted by uuid, per row.
161
+ select is(
162
+ (select u.id from auth.users u
163
+ where lower(u.email) = format('agent+%s@agent-board.local',
164
+ agent_board._federated_identity_slug('https://fixture-246.example/idp-a', 'fixture-246-subject-a'))),
165
+ 'd2460001-0000-4000-8000-000000000001'::uuid,
166
+ 'BF-1e: F1 the #244 resumption lookup still resolves the pair to the SAME owner uuid');
167
+
168
+ -- A count of 2 is satisfied by one agent twice, or by two agents that are not these
169
+ -- two -- the message says "both", so the body names both. (message-board#246 panel; the
170
+ -- same shape shipped twice in #306.)
171
+ select is(
172
+ (select string_agg(a.id::text, ', ' order by a.id::text) from agent_board.agents a
173
+ where a.owner_user_id = 'd2460001-0000-4000-8000-000000000001'),
174
+ 'd2460101-0000-4000-8000-000000000001, d2460102-0000-4000-8000-000000000001',
175
+ 'BF-1f: F1 still owns exactly its human row d2460101 and its owned agent d2460102 (the uuid moved nowhere, so no FK moved)');
176
+
177
+ -- F2 — the OWNER-ONLY arm. uid d2460002-0000-4000-8000-000000000002, no agent carries
178
+ -- this auth_user_id; the pair is reachable only through owner_user_id.
179
+ -- slug fed-human-7ab9c8b527e1cce671305defc65fe7c0 -> fed-human-ec861cc1407f8f585e94cb9e8ece6039.
180
+
181
+ select is(
182
+ (select u.email from auth.users u where u.id = 'd2460002-0000-4000-8000-000000000002'),
183
+ 'agent+fed-human-ec861cc1407f8f585e94cb9e8ece6039@agent-board.local',
184
+ 'BF-2a: F2 auth.users.email re-keyed although the pair is reachable only via owner_user_id');
185
+
186
+ select is(
187
+ (select u.raw_user_meta_data->>'slug' from auth.users u
188
+ where u.id = 'd2460002-0000-4000-8000-000000000002'),
189
+ 'fed-human-ec861cc1407f8f585e94cb9e8ece6039',
190
+ 'BF-2b: F2 auth.users.raw_user_meta_data->>slug re-keyed');
191
+
192
+ select is(
193
+ (select i.provider_id || '|' || (i.identity_data->>'sub') || '|' || (i.identity_data->>'slug')
194
+ from auth.identities i where i.user_id = 'd2460002-0000-4000-8000-000000000002'),
195
+ 'fed-human-ec861cc1407f8f585e94cb9e8ece6039|fed-human-ec861cc1407f8f585e94cb9e8ece6039|fed-human-ec861cc1407f8f585e94cb9e8ece6039',
196
+ 'BF-2c: F2 auth.identities provider_id AND identity_data.sub AND identity_data.slug all re-keyed (each position shown)');
197
+
198
+ -- The complement of BF-1d: an agent whose handle is NOT the slug must not be touched.
199
+ -- "update agents set handle = new where handle = old" is narrow on purpose.
200
+ select is(
201
+ (select a.handle from agent_board.agents a
202
+ where a.id = 'd2460201-0000-4000-8000-000000000002'),
203
+ 'mb246-fixture-a2',
204
+ 'BF-2d: F2 owned agent keeps its own handle -- only a handle that IS the slug is rewritten');
205
+
206
+ select is(
207
+ (select u.id from auth.users u
208
+ where lower(u.email) = format('agent+%s@agent-board.local',
209
+ agent_board._federated_identity_slug('https://fixture-246.example/idp-b', 'fixture-246-subject-b'))),
210
+ 'd2460002-0000-4000-8000-000000000002'::uuid,
211
+ 'BF-2e: F2 the #244 resumption lookup still resolves the pair to the SAME owner uuid');
212
+
213
+ select is(
214
+ (select string_agg(a.id::text, ', ' order by a.id::text) from agent_board.agents a
215
+ where a.owner_user_id = 'd2460002-0000-4000-8000-000000000002'),
216
+ 'd2460201-0000-4000-8000-000000000002',
217
+ 'BF-2f: F2 still owns exactly its one agent d2460201 (named, not counted)');
218
+
219
+ -- F3 — the DECOY arm. uid d2460003-0000-4000-8000-000000000003 owns an agent stamped
220
+ -- with an unrelated pair; only the "reproduces the current slug" filter excludes it.
221
+ -- slug fed-human-def7953eb096d786392090ce33952d9d -> fed-human-d4211005ca6d96ac0f8a90d2065dceda.
222
+
223
+ select is(
224
+ (select u.email from auth.users u where u.id = 'd2460003-0000-4000-8000-000000000003'),
225
+ 'agent+fed-human-d4211005ca6d96ac0f8a90d2065dceda@agent-board.local',
226
+ 'BF-3a: F3 auth.users.email re-keyed to the slug of the pair that REPRODUCES its old slug, not the decoy');
227
+
228
+ select is(
229
+ (select u.raw_user_meta_data->>'slug' from auth.users u
230
+ where u.id = 'd2460003-0000-4000-8000-000000000003'),
231
+ 'fed-human-d4211005ca6d96ac0f8a90d2065dceda',
232
+ 'BF-3b: F3 auth.users.raw_user_meta_data->>slug re-keyed');
233
+
234
+ select is(
235
+ (select i.provider_id || '|' || (i.identity_data->>'sub') || '|' || (i.identity_data->>'slug')
236
+ from auth.identities i where i.user_id = 'd2460003-0000-4000-8000-000000000003'),
237
+ 'fed-human-d4211005ca6d96ac0f8a90d2065dceda|fed-human-d4211005ca6d96ac0f8a90d2065dceda|fed-human-d4211005ca6d96ac0f8a90d2065dceda',
238
+ 'BF-3c: F3 auth.identities provider_id AND identity_data.sub AND identity_data.slug all re-keyed (each position shown)');
239
+
240
+ select is(
241
+ (select a.handle from agent_board.agents a
242
+ where a.id = 'd2460301-0000-4000-8000-000000000003'),
243
+ 'fed-human-d4211005ca6d96ac0f8a90d2065dceda',
244
+ 'BF-3d: F3 human agent handle re-keyed');
245
+
246
+ select is(
247
+ (select u.id from auth.users u
248
+ where lower(u.email) = format('agent+%s@agent-board.local',
249
+ agent_board._federated_identity_slug('https://fixture-246.example/idp-c', 'fixture-246-subject-c'))),
250
+ 'd2460003-0000-4000-8000-000000000003'::uuid,
251
+ 'BF-3e: F3 the #244 resumption lookup still resolves the pair to the SAME owner uuid');
252
+
253
+ -- The stamp is the PAIR. Reading only the issuer left a subject-only rewrite green while
254
+ -- the message claimed "both ... its stamp"; both stamp fields are read here.
255
+ select is(
256
+ (select a.handle || '|' || (a.metadata->'federation'->>'issuer')
257
+ || '|' || (a.metadata->'federation'->>'subject')
258
+ from agent_board.agents a
259
+ where a.id = 'd2460302-0000-4000-8000-000000000003'),
260
+ 'mb246-fixture-f3-decoy|https://fixture-246.example/idp-decoy|fixture-246-subject-decoy',
261
+ 'BF-3f: F3 decoy-stamped agent is untouched in its handle AND in both fields of its stamp (each position shown)');
262
+
263
+ -- F4 — the DIVERGENT arm. uid d2460004-0000-4000-8000-000000000004, pair
264
+ -- ("https://fixture-246.example/idp-d", "fixture-246-subject-d"),
265
+ -- slug fed-human-38b45a2207cfb4847a4c62c9973cd676 -> fed-human-25bf058f71a25339995a9a2012ced65b.
266
+ --
267
+ -- Two of the backfill's four surfaces are rewritten only when they EQUAL the old slug --
268
+ -- raw_user_meta_data.slug and identity_data.sub/slug, one `case` arm each. F1-F3 all have
269
+ -- them equal, so making both arms UNCONDITIONAL passed every assertion in this file. This
270
+ -- fixture is the complement of BF-2d, which pins the same narrowness for agents.handle,
271
+ -- and it was simply missing.
272
+
273
+ select is(
274
+ (select u.email from auth.users u where u.id = 'd2460004-0000-4000-8000-000000000004'),
275
+ 'agent+fed-human-25bf058f71a25339995a9a2012ced65b@agent-board.local',
276
+ 'BF-4a: F4 auth.users.email re-keyed -- this row WAS visited, so BF-4b/4c below are not passing vacuously');
277
+
278
+ select is(
279
+ (select u.raw_user_meta_data->>'slug' from auth.users u
280
+ where u.id = 'd2460004-0000-4000-8000-000000000004'),
281
+ 'mb246-f4-legacy-meta-slug',
282
+ 'BF-4b: F4 raw_user_meta_data->>slug is NOT rewritten, because it never held the old slug');
283
+
284
+ select is(
285
+ (select i.provider_id || '|' || (i.identity_data->>'sub') || '|' || (i.identity_data->>'slug')
286
+ from auth.identities i where i.user_id = 'd2460004-0000-4000-8000-000000000004'),
287
+ 'fed-human-25bf058f71a25339995a9a2012ced65b|mb246-f4-legacy-sub|mb246-f4-legacy-slug',
288
+ 'BF-4c: F4 identities provider_id IS re-keyed while identity_data.sub and .slug are NOT -- each of the three positions shown');
289
+
290
+ -- Population-wide, not fixture-wide: every fed-human owner email in the database must be
291
+ -- reproducible from a stamp under the NEW derivation. Old and new slugs are
292
+ -- shape-identical, so "no old-form rows remain" is not something a pattern can see;
293
+ -- reproducibility is what can be checked, and the failure names the uids.
294
+ select is(
295
+ (select coalesce(string_agg(u.id::text, ', ' order by u.id::text), '(none)')
296
+ from auth.users u
297
+ where lower(u.email) like 'agent+fed-human-%@agent-board.local'
298
+ and not exists (
299
+ select 1 from agent_board.agents a
300
+ where (a.auth_user_id = u.id or a.owner_user_id = u.id)
301
+ and a.metadata->'federation'->>'issuer' is not null
302
+ and a.metadata->'federation'->>'subject' is not null
303
+ and agent_board._federated_identity_slug(
304
+ a.metadata->'federation'->>'issuer',
305
+ a.metadata->'federation'->>'subject') =
306
+ substring(lower(u.email) from '^agent\+(fed-human-[0-9a-f]{32})@agent-board\.local$'))),
307
+ '(none)',
308
+ 'BF-POP: every fed-human owner row in the database re-derives from a stored stamp under the NEW helper (offending uids are named)');
309
+
310
+ -- ---------------------------------------------------------------------------
311
+ -- CLS: catalog-derived class invariants
312
+ -- ---------------------------------------------------------------------------
313
+
314
+ -- CLS-1. The old encoding is GONE, not shadowed. Any inline derivation must build the
315
+ -- 'fed-human-' prefix, so the prefix literal is the class marker. Comments are stripped
316
+ -- (keep_literals => true) because the migration header quotes the old expression in
317
+ -- prose and an unstripped scan would be defeated by exactly that.
318
+ select is(
319
+ (select coalesce(string_agg(p.oid::regprocedure::text, ', ' order by p.oid::regprocedure::text), '(none)')
320
+ from pg_proc p
321
+ where p.pronamespace = 'agent_board'::regnamespace
322
+ and p.proname <> '_federated_identity_slug'
323
+ and agent_board_test.stripped_body(p.oid, true) like '%fed-human-%'),
324
+ '(none)',
325
+ 'CLS-1: no agent_board function except _federated_identity_slug builds a fed-human- slug (offenders are named)');
326
+
327
+ -- CLS-2. The complement of CLS-1: a re-introduced derivation that avoided the prefix
328
+ -- literal would still carry the ambiguous separator shape. Catches the same class from
329
+ -- the other side, so neither assertion is the only thing standing between the schema
330
+ -- and a copied expression.
331
+ select is(
332
+ (select coalesce(string_agg(p.oid::regprocedure::text, ', ' order by p.oid::regprocedure::text), '(none)')
333
+ from pg_proc p
334
+ where p.pronamespace = 'agent_board'::regnamespace
335
+ and agent_board_test.stripped_body(p.oid, true) ~ 'p_issuer\s*\|\|\s*''\|''\s*\|\|\s*p_subject'),
336
+ '(none)',
337
+ 'CLS-2: no agent_board function still concatenates p_issuer || ''|'' || p_subject (offenders are named)');
338
+
339
+ -- CLS-3. Completeness, positively and per site. Not a total: the list carries each
340
+ -- function's NAME and its own call count, so "five callers" cannot be satisfied by one
341
+ -- function calling five times, and enroll_federated_identity's SECOND call site (the
342
+ -- #244 resumption lookup, which is a different site from the mint) is named separately
343
+ -- from its first.
344
+ select is(
345
+ (select coalesce(string_agg(x.entry, ', ' order by x.entry), '(none)')
346
+ from (select p.proname || '=' ||
347
+ (select count(*) from regexp_matches(
348
+ agent_board_test.stripped_body(p.oid, true),
349
+ '_federated_identity_slug\(', 'g'))::text as entry
350
+ from pg_proc p
351
+ where p.pronamespace = 'agent_board'::regnamespace
352
+ and p.proname <> '_federated_identity_slug'
353
+ and agent_board_test.stripped_body(p.oid, true) like '%\_federated\_identity\_slug(%') x),
354
+ '_enroll_federated_human_after_revocation_gate=1, _enroll_federated_identity_once=1, enroll_federated_human=1, enroll_federated_identity=2, onboard_federated_fleet_agent=1',
355
+ 'CLS-3: exactly these five functions route through the helper, each with its own call count');
356
+
357
+ -- CLS-4. The helper is not a public lever. It is pure and mints nothing, but it is the
358
+ -- function that decides which account a pair maps to, and CREATE FUNCTION grants EXECUTE
359
+ -- to PUBLIC by default.
360
+ select ok(
361
+ not has_function_privilege('public', 'agent_board._federated_identity_slug(text,text)', 'EXECUTE'),
362
+ 'CLS-4: _federated_identity_slug carries no PUBLIC EXECUTE grant');
363
+
364
+ -- CLS-5. create or replace (never drop) left every rebuilt function with a COMMENT -- a
365
+ -- DROP would have silently discarded the comment AND the ACL. Named per function so a
366
+ -- single surviving comment cannot stand in for five.
367
+ --
368
+ -- What this pins, stated exactly rather than implied by the word "kept": the comment is
369
+ -- NOT NULL. It does not pin the TEXT, so a comment replaced with different prose passes.
370
+ -- Pinning the text would pin five paragraphs of prose to the byte and fail on every
371
+ -- wording change; non-null is the property that catches the failure mode that actually
372
+ -- occurs here, which is a drop+recreate discarding the comment altogether.
373
+ select is(
374
+ (select coalesce(string_agg(x.name, ', ' order by x.name), '(none)')
375
+ from (select p.proname as name from pg_proc p
376
+ where p.pronamespace = 'agent_board'::regnamespace
377
+ and p.proname in ('enroll_federated_human',
378
+ '_enroll_federated_human_after_revocation_gate',
379
+ '_enroll_federated_identity_once',
380
+ 'enroll_federated_identity',
381
+ 'onboard_federated_fleet_agent')
382
+ and obj_description(p.oid, 'pg_proc') is null) x),
383
+ '(none)',
384
+ 'CLS-5: every rebuilt derivation site still carries a non-null COMMENT (functions with none are named)');
385
+
386
+ select * from finish();
387
+
388
+ rollback;
@@ -117,12 +117,27 @@ select is(
117
117
  (select count(*)::int from auth.users u where u.id = (select auth_user_id from _r where label='A')),
118
118
  1, 'the synthetic auth.users row for agent A exists');
119
119
  -- 11
120
+ -- message-board#246: the derivation moved from `issuer || '|' || subject` to the
121
+ -- netstring form `len(issuer):issuer:len(subject):subject`, because the old one was not
122
+ -- injective and the slug keys the auth.users row. The expected value is still spelled
123
+ -- out here rather than calling agent_board._federated_identity_slug: an assertion that
124
+ -- calls the function under test compares it with itself and would follow any future
125
+ -- change to it silently. This is an independent restatement of the contract.
126
+ --
127
+ -- The message no longer says "byte-identical to enroll_federated_identity". That was true
128
+ -- when both functions carried their own copy of the expression and it was the risk worth
129
+ -- naming; enroll_federated_identity now calls the same helper, so there is no second copy
130
+ -- for this one to be byte-identical TO, and a message asserting that would be claiming
131
+ -- something no code does.
120
132
  select is(
121
133
  (select handle from _r where label='A'),
122
134
  'fed-human-' || substr(
123
- encode(extensions.digest(convert_to((select iss from _f) || '|' || (select sub_a from _f), 'UTF8'), 'sha256'), 'hex'),
135
+ encode(extensions.digest(convert_to(
136
+ length((select iss from _f))::text || ':' || (select iss from _f) || ':' ||
137
+ length((select sub_a from _f))::text || ':' || (select sub_a from _f),
138
+ 'UTF8'), 'sha256'), 'hex'),
124
139
  1, 32),
125
- 'handle = fed-human-<sha256(issuer|subject)[:32]> (byte-identical to enroll_federated_identity)');
140
+ 'handle = fed-human-<sha256(len:issuer:len:subject)[:32]> (the contract every site now shares via _federated_identity_slug)');
126
141
 
127
142
  -- metadata carries the federation snapshot, boolean-only email, no PII --------------------
128
143
  -- 12