@consiliency/agent-board-schema 1.57.0 → 1.59.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 (36) hide show
  1. package/fixtures/already-applied.json +8 -0
  2. package/migrations/20261008000100_agent_board_hosted_delegates.sql +3152 -0
  3. package/migrations/20261009000100_agent_board_hosted_followups.sql +665 -0
  4. package/package.json +1 -1
  5. package/schema-manifest.json +21 -3
  6. package/src/tooling.test.mjs +9 -2
  7. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -0
  8. package/tests/agent_board_consent_emission.test.sql +4 -2
  9. package/tests/agent_board_delegate_role_class.test.sql +736 -0
  10. package/tests/agent_board_federated_identity_injective_slug.test.sql +5 -2
  11. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +71 -14
  12. package/tests/agent_board_fk_lock_edges.test.sql +9 -0
  13. package/tests/agent_board_hosted_delegates.test.sql +1278 -0
  14. package/tests/agent_board_ledger_authority.test.sql +34 -5
  15. package/tests/agent_board_signer_lifecycle.test.sql +3 -0
  16. package/tests/agent_board_write_enforcement_class.test.sql +8 -3
  17. package/tests/catalog/function_execute_privileges.json +269 -2
  18. package/tests/catalog/p_metadata_parameters.json +44 -0
  19. package/tests/isolation/expected/hosted_delegate_add_tombstone_race.out +105 -0
  20. package/tests/isolation/expected/hosted_delegate_exchange_agent_revoke_race.out +66 -0
  21. package/tests/isolation/expected/hosted_delegate_exchange_race.out +93 -0
  22. package/tests/isolation/expected/hosted_delegate_first_exchange_race.out +129 -0
  23. package/tests/isolation/expected/hosted_delegate_owner_provisioning_race.out +91 -0
  24. package/tests/isolation/expected/hosted_delegate_owner_revoke_race.out +137 -0
  25. package/tests/isolation/expected/hosted_delegate_revoke_race.out +87 -0
  26. package/tests/isolation/expected/hosted_delegate_tombstone_revoke_race.out +170 -0
  27. package/tests/isolation/expected/hosted_delegate_withdraw_evict_race.out +235 -0
  28. package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +102 -0
  29. package/tests/isolation/specs/hosted_delegate_exchange_agent_revoke_race.spec +92 -0
  30. package/tests/isolation/specs/hosted_delegate_exchange_race.spec +64 -0
  31. package/tests/isolation/specs/hosted_delegate_first_exchange_race.spec +72 -0
  32. package/tests/isolation/specs/hosted_delegate_owner_provisioning_race.spec +73 -0
  33. package/tests/isolation/specs/hosted_delegate_owner_revoke_race.spec +93 -0
  34. package/tests/isolation/specs/hosted_delegate_revoke_race.spec +59 -0
  35. package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +158 -0
  36. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +145 -0
@@ -0,0 +1,1278 @@
1
+ -- HOSTED (v11) SL-2 pgTAP: hosted delegate instances, the exchange and tombstone RPCs,
2
+ -- add_delegate_to_board, attribution, the D7/D12/D15/D16 rules, and the #583 acceptance tests
3
+ -- (T1-T5) at the database. The CONTRACT is the HOSTED section of
4
+ -- specs/agent-client-contract.md; each section below names the decision or test it pins.
5
+ --
6
+ -- METHOD.
7
+ -- * Every probe is driven by a REAL operation: the exchange and tombstone RPCs as
8
+ -- service_role, add_delegate_to_board and the membership RPCs as the owner's session, and
9
+ -- the delegate's own calls AS agent_board_delegate. The owner is provisioned through the
10
+ -- real enroll_federated_human, never a hand-seeded row, so a change to the owner-uid
11
+ -- derivation fails the owner gate here (IF-0-HOSTED-12).
12
+ -- * A delegate call SWITCHES ROLE (`set local role agent_board_delegate`), because grants and
13
+ -- RLS are the property under test. Existing files probe with claims only, which leaves the
14
+ -- role postgres; that would test nothing here.
15
+ -- * Allow-side probes return 'ok' or `<sqlstate>:<message>` from a helper with its own
16
+ -- exception block, then a SEPARATE assertion checks the resulting state (AGENTS.md rule 3).
17
+ -- A refused probe is compared to its exact sqlstate and message, so a refusal for the wrong
18
+ -- reason fails.
19
+ create extension if not exists pgtap with schema extensions;
20
+ set search_path = extensions, public, agent_board;
21
+
22
+ begin;
23
+
24
+ select plan(143);
25
+
26
+ -- The test role switches to agent_board_delegate. On PostgreSQL 16+ the role's creator holds
27
+ -- ADMIN OPTION but not SET, so grant SET to the current role for this transaction only (rolled
28
+ -- back with everything else). A superuser needs nothing.
29
+ do $$
30
+ begin
31
+ if not (select rolsuper from pg_roles where rolname = current_user) then
32
+ execute format('grant agent_board_delegate to %I with set true', current_user);
33
+ end if;
34
+ end $$;
35
+
36
+ create temp table _h(k text primary key, v text);
37
+ create function pg_temp.v(p_k text) returns text language sql as $$ select v from _h where k = p_k $$;
38
+ create function pg_temp.u(p_k text) returns uuid language sql as $$ select v::uuid from _h where k = p_k $$;
39
+ create function pg_temp.put(p_k text, p_v text) returns void language sql as $$
40
+ insert into _h values (p_k, p_v) on conflict (k) do update set v = excluded.v $$;
41
+
42
+ create function pg_temp.svc() returns void language plpgsql as $$
43
+ begin perform set_config('request.jwt.claims', '', true); end $$;
44
+ create function pg_temp.authn(p_sub uuid) returns void language plpgsql as $$
45
+ begin
46
+ perform set_config('request.jwt.claims',
47
+ json_build_object('role', 'authenticated', 'sub', p_sub)::text, true);
48
+ end $$;
49
+ create function pg_temp.deleg(p_sub uuid, p_cred uuid) returns void language plpgsql as $$
50
+ begin
51
+ perform set_config('request.jwt.claims',
52
+ case when p_cred is null
53
+ then json_build_object('role', 'agent_board_delegate', 'sub', p_sub)::text
54
+ else json_build_object('role', 'agent_board_delegate', 'sub', p_sub, 'agent_credential_id', p_cred)::text
55
+ end, true);
56
+ end $$;
57
+ -- Run as the CURRENT role; 'ok' or '<sqlstate>:<message>'.
58
+ create function pg_temp.try(p_sql text) returns text language plpgsql as $$
59
+ begin
60
+ execute p_sql;
61
+ return 'ok';
62
+ exception when others then
63
+ return sqlstate || ':' || sqlerrm;
64
+ end $$;
65
+ -- Run AS agent_board_delegate (claims set by the caller); 'ok' or '<sqlstate>:<message>'.
66
+ create function pg_temp.as_d(p_sql text) returns text language plpgsql as $$
67
+ declare v text;
68
+ begin
69
+ set local role agent_board_delegate;
70
+ begin
71
+ execute p_sql;
72
+ v := 'ok';
73
+ exception when others then
74
+ v := sqlstate || ':' || sqlerrm;
75
+ end;
76
+ reset role;
77
+ return v;
78
+ end $$;
79
+ -- Run AS agent_board_delegate and return the single scalar the query yields.
80
+ create function pg_temp.as_d_val(p_sql text) returns text language plpgsql as $$
81
+ declare v text;
82
+ begin
83
+ set local role agent_board_delegate;
84
+ begin
85
+ execute p_sql into v;
86
+ exception when others then
87
+ v := 'ERR ' || sqlstate || ':' || sqlerrm;
88
+ end;
89
+ reset role;
90
+ return v;
91
+ end $$;
92
+
93
+ -- A state read that runs as its OWN query, so it sees what an action earlier in the same
94
+ -- statement wrote. A plain `(select ...)` in the statement that calls the action reads the
95
+ -- statement's snapshot -- the state BEFORE the action -- and would pass vacuously.
96
+ create function pg_temp.q(p_sql text) returns text language plpgsql as $$
97
+ declare v text;
98
+ begin
99
+ execute p_sql into v;
100
+ return coalesce(v, '(null)');
101
+ end $$;
102
+ create function pg_temp.claim(
103
+ p_iid text, p_gid text, p_gen bigint, p_owner_sub text, p_client text,
104
+ p_change jsonb default null, p_slot text default null, p_status text default 'active')
105
+ returns jsonb language sql as $$
106
+ select jsonb_build_object(
107
+ 'iss', 'https://portal.hosted.test',
108
+ 'sub', p_owner_sub,
109
+ 'slot_resource', p_slot,
110
+ 'agent_board_delegate_instance', jsonb_build_object(
111
+ 'instance_id', p_iid, 'grant_id', p_gid, 'generation', p_gen, 'owner_sub', p_owner_sub,
112
+ 'client_id', p_client, 'status', p_status, 'client_change', p_change))
113
+ $$;
114
+ -- The exchange as service_role. On success records <key>.agent / .auth / .cred and returns 'ok'.
115
+ create function pg_temp.exch(p_claims jsonb, p_key text) returns text language plpgsql as $$
116
+ declare r record;
117
+ begin
118
+ perform set_config('request.jwt.claims', '', true);
119
+ select * into r from agent_board.exchange_delegate_credential(p_claims);
120
+ perform pg_temp.put(p_key || '.agent', r.agent_id::text);
121
+ perform pg_temp.put(p_key || '.auth', r.auth_user_id::text);
122
+ perform pg_temp.put(p_key || '.cred', r.credential_id::text);
123
+ return 'ok';
124
+ exception when others then
125
+ return sqlstate || ':' || sqlerrm;
126
+ end $$;
127
+ create function pg_temp.tomb(p_iid text, p_owner_sub text, p_reason text default 'owner_revoked') returns text
128
+ language plpgsql as $$
129
+ declare r record;
130
+ begin
131
+ perform set_config('request.jwt.claims', '', true);
132
+ select * into r from agent_board.tombstone_delegate_instance(p_iid, 'https://portal.hosted.test', p_owner_sub, p_reason);
133
+ return 'first=' || r.first::text;
134
+ exception when others then
135
+ return sqlstate || ':' || sqlerrm;
136
+ end $$;
137
+ create function pg_temp.state(b uuid, a uuid) returns text language sql as $$
138
+ select membership_state || '/' || role from agent_board.board_memberships
139
+ where board_id = b and agent_id = a
140
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end, created_at desc, id desc
141
+ limit 1 $$;
142
+ create function pg_temp.last_event(b uuid, a uuid) returns text language sql as $$
143
+ select provenance || '/' || event_kind || '/' || coalesce(actor_agent_id::text, 'null') || '/' || result_state
144
+ from agent_board.board_membership_events where board_id = b and agent_id = a
145
+ order by seq desc limit 1 $$;
146
+
147
+ -- ---------------------------------------------------------------------------
148
+ -- Fixtures. The owners are federated humans provisioned through enroll_federated_human.
149
+ -- ---------------------------------------------------------------------------
150
+ insert into agent_board.identity_providers
151
+ (issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
152
+ values ('https://portal.hosted.test', 'message-board', true, true, '{}', '{}', '{agent,human}');
153
+
154
+ do $$
155
+ declare r record; s text;
156
+ begin
157
+ perform set_config('request.jwt.claims', '', true);
158
+ foreach s in array array['a', 'b', 'c'] loop
159
+ select * into r from agent_board.enroll_federated_human(
160
+ 'https://portal.hosted.test', 'message-board', 'hosted-owner-' || s,
161
+ jsonb_build_object('sub', 'hosted-owner-' || s, 'email', 'owner-' || s || '@hosted.test', 'email_verified', true),
162
+ 'Owner ' || upper(s));
163
+ perform pg_temp.put('O' || upper(s), r.agent_id::text);
164
+ perform pg_temp.put('O' || upper(s) || '.auth', r.auth_user_id::text);
165
+ end loop;
166
+ end $$;
167
+
168
+ -- Keyed agents: K (board owner/admin everywhere), K2 (member), Z1 (only on BZ).
169
+ insert into auth.users(id, instance_id, aud, role, email)
170
+ select ('5e000000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
171
+ '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'hosted-k' || n || '@t.test'
172
+ from generate_series(1, 3) n;
173
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
174
+ select ('5e100000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, 'agent', 'agent',
175
+ 'hosted-keyed-' || n, 'Hosted Keyed ' || n, 'active',
176
+ ('5e000000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
177
+ from generate_series(1, 3) n;
178
+ select pg_temp.put('K', '5e100000-0000-4000-8000-000000000001'),
179
+ pg_temp.put('K.auth', '5e000000-0000-4000-8000-000000000001'),
180
+ pg_temp.put('K2', '5e100000-0000-4000-8000-000000000002'),
181
+ pg_temp.put('K2.auth', '5e000000-0000-4000-8000-000000000002'),
182
+ pg_temp.put('Z1', '5e100000-0000-4000-8000-000000000003');
183
+
184
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
185
+ values ('5e200000-0000-4000-8000-000000000001', 'hosted-bx', 'Hosted BX', pg_temp.u('K'), 'private', 'active'),
186
+ ('5e200000-0000-4000-8000-000000000002', 'hosted-by', 'Hosted BY', pg_temp.u('K'), 'private', 'active'),
187
+ ('5e200000-0000-4000-8000-000000000003', 'hosted-bz', 'Hosted BZ', pg_temp.u('K'), 'private', 'active'),
188
+ ('5e200000-0000-4000-8000-000000000004', 'hosted-bw', 'Hosted BW', pg_temp.u('OA'), 'private', 'active');
189
+ select pg_temp.put('BX', '5e200000-0000-4000-8000-000000000001'),
190
+ pg_temp.put('BY', '5e200000-0000-4000-8000-000000000002'),
191
+ pg_temp.put('BZ', '5e200000-0000-4000-8000-000000000003'),
192
+ pg_temp.put('BW', '5e200000-0000-4000-8000-000000000004');
193
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
194
+ values (pg_temp.u('BX'), pg_temp.u('K'), 'owner', 'joined', pg_temp.u('K')),
195
+ (pg_temp.u('BX'), pg_temp.u('K2'), 'member', 'joined', pg_temp.u('K')),
196
+ (pg_temp.u('BX'), pg_temp.u('OA'), 'member', 'joined', pg_temp.u('K')),
197
+ (pg_temp.u('BX'), pg_temp.u('OB'), 'member', 'joined', pg_temp.u('K')),
198
+ (pg_temp.u('BY'), pg_temp.u('K'), 'owner', 'joined', pg_temp.u('K')),
199
+ (pg_temp.u('BY'), pg_temp.u('OA'), 'member', 'joined', pg_temp.u('K')),
200
+ (pg_temp.u('BZ'), pg_temp.u('K'), 'owner', 'joined', pg_temp.u('K')),
201
+ (pg_temp.u('BZ'), pg_temp.u('Z1'), 'member', 'joined', pg_temp.u('K')),
202
+ (pg_temp.u('BW'), pg_temp.u('OA'), 'owner', 'joined', pg_temp.u('OA')),
203
+ (pg_temp.u('BW'), pg_temp.u('K'), 'member', 'joined', pg_temp.u('OA'));
204
+
205
+ -- ===========================================================================
206
+ -- 1. EXECUTE surface of the three new RPCs (D10's pin, plus IF-0-HOSTED-12)
207
+ -- ===========================================================================
208
+ -- 1
209
+ select is(
210
+ (select string_agg(f || ':' || r || '=' || has_function_privilege(r, f, 'EXECUTE')::text, ' ' order by f, r)
211
+ from unnest(array['agent_board.exchange_delegate_credential(jsonb)',
212
+ 'agent_board.tombstone_delegate_instance(text,text,text,text)']) f
213
+ cross join unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r),
214
+ 'agent_board.exchange_delegate_credential(jsonb):agent_board_delegate=false agent_board.exchange_delegate_credential(jsonb):anon=false agent_board.exchange_delegate_credential(jsonb):authenticated=false agent_board.exchange_delegate_credential(jsonb):service_role=true agent_board.tombstone_delegate_instance(text,text,text,text):agent_board_delegate=false agent_board.tombstone_delegate_instance(text,text,text,text):anon=false agent_board.tombstone_delegate_instance(text,text,text,text):authenticated=false agent_board.tombstone_delegate_instance(text,text,text,text):service_role=true',
215
+ 'D10: the exchange and the tombstone are EXECUTE-able by service_role only, never authenticated, anon or agent_board_delegate');
216
+ -- 2
217
+ select is(
218
+ (select string_agg(r || '=' || has_function_privilege(r, 'agent_board.add_delegate_to_board(uuid,uuid)', 'EXECUTE')::text, ' ' order by r)
219
+ from unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r),
220
+ 'agent_board_delegate=false anon=false authenticated=true service_role=false',
221
+ 'IF-0-HOSTED-12: add_delegate_to_board is EXECUTE-able by authenticated only');
222
+ -- 3 -- a non-service caller is refused inside the function too
223
+ select pg_temp.authn(pg_temp.u('K.auth'));
224
+ select is(
225
+ pg_temp.try($$ select agent_board.exchange_delegate_credential('{}'::jsonb) $$),
226
+ '42501:exchange_delegate_credential is service_role-only',
227
+ 'the exchange refuses a non-service session in its own body');
228
+
229
+ -- ===========================================================================
230
+ -- 2. T1 -- three instances of ONE owner and ONE client are three principals
231
+ -- ===========================================================================
232
+ -- 4
233
+ select is(
234
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'I1')
235
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_02', 'grant_aaaaaaaaaaaaaa_02_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'I2')
236
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_03', 'grant_aaaaaaaaaaaaaa_03_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'I3'),
237
+ 'ok ok ok',
238
+ 'T1: three first exchanges of one owner and one client succeed');
239
+ -- 5
240
+ select is(
241
+ (select count(distinct v)::int from _h where k in ('I1.agent', 'I2.agent', 'I3.agent'))
242
+ + (select count(distinct v)::int from _h where k in ('I1.auth', 'I2.auth', 'I3.auth')),
243
+ 6,
244
+ 'T1: they are three distinct agents with three distinct auth users -- the (owner, client) pair alone is never the principal');
245
+ -- 6
246
+ select is(
247
+ (select string_agg(a.is_delegate::text || '/' || a.kind::text || '/' || a.agent_type || '/' || a.status
248
+ || '/' || (a.owner_user_id = pg_temp.u('OA.auth'))::text
249
+ || '/' || (a.handle = agent_board._delegate_instance_slug('https://portal.hosted.test', di.instance_id))::text
250
+ || '/' || (a.auth_user_id = (select u.id from auth.users u where u.email = 'agent+' || a.handle || '@agent-board.local'))::text,
251
+ ' ' order by di.instance_id)
252
+ from agent_board.delegate_instances di join agent_board.agents a on a.id = di.agent_id
253
+ where di.instance_id in ('inst_aaaaaaaaaaaaaaaa_01', 'inst_aaaaaaaaaaaaaaaa_02', 'inst_aaaaaaaaaaaaaaaa_03')),
254
+ 'true/agent/agent/active/true/true/true true/agent/agent/active/true/true/true true/agent/agent/active/true/true/true',
255
+ 'each delegate row: the delegate fact, kind=agent_type=agent, active, owned by the owner''s uid, handle = the instance slug, its own auth user');
256
+ -- 7 -- IF-0-HOSTED-12 / owner identity: the recorded owner is EXACTLY the uid enroll_federated_human provisioned
257
+ select is(
258
+ (select string_agg(distinct (di.owner_user_id = pg_temp.u('OA.auth'))::text, ',')
259
+ from agent_board.delegate_instances di
260
+ where di.instance_id like 'inst_aaaaaaaaaaaaaaaa_0%'),
261
+ 'true',
262
+ 'owner identity: delegate_instances.owner_user_id is the auth uid enroll_federated_human created for the owner (never the raw sub)');
263
+ -- 8 -- the credential row
264
+ select is(
265
+ (select string_agg(ac.grant_id || '/' || ac.generation || '/' || (ac.revoked_at is null)::text
266
+ || '/' || (ac.expires_at <= now() + interval '900 seconds')::text, ' ' order by ac.grant_id)
267
+ from agent_board.agent_credentials ac where ac.id in (pg_temp.u('I1.cred'), pg_temp.u('I2.cred'))),
268
+ 'grant_aaaaaaaaaaaaaa_01_1/1/true/true grant_aaaaaaaaaaaaaa_02_1/1/true/true',
269
+ 'IF-0-HOSTED-7: each credential row records grant_id and generation, is live, and expires within 900 s');
270
+
271
+ -- ===========================================================================
272
+ -- 3. add_delegate_to_board (IF-0-HOSTED-12; D2, D15) -- the owner, through the real path
273
+ -- ===========================================================================
274
+ select pg_temp.authn(pg_temp.u('OA.auth'));
275
+ -- 9
276
+ select is(
277
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I1.agent'), pg_temp.v('BX')))
278
+ || ' ' || pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I2.agent'), pg_temp.v('BX')))
279
+ || ' ' || pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I3.agent'), pg_temp.v('BY'))),
280
+ 'ok ok ok',
281
+ 'the owner (provisioned by enroll_federated_human) adds its delegates: the owner gate passes for the real owner uid');
282
+ -- 10
283
+ select is(
284
+ pg_temp.state(pg_temp.u('BX'), pg_temp.u('I1.agent')) || ' ' || pg_temp.state(pg_temp.u('BX'), pg_temp.u('I2.agent'))
285
+ || ' ' || pg_temp.state(pg_temp.u('BY'), pg_temp.u('I3.agent')),
286
+ 'joined/member joined/member joined/member',
287
+ 'each delegate is joined as member, never higher');
288
+ -- 11
289
+ select is(
290
+ pg_temp.last_event(pg_temp.u('BX'), pg_temp.u('I1.agent')),
291
+ 'principal-authored/principal_placement/' || pg_temp.v('OA') || '/joined',
292
+ 'D2: the placement is principal-authored, kind principal_placement, with the OWNER as actor');
293
+ -- 12
294
+ select is(
295
+ agent_board._membership_consent_present(pg_temp.u('BX'), pg_temp.u('I1.agent'))::text
296
+ || '/' || agent_board._membership_consent_kind(pg_temp.u('BX'), pg_temp.u('I1.agent')),
297
+ 'true/principal',
298
+ 'D2: _membership_consent_present counts the principal''s placement as consent, and _kind reports it as principal');
299
+ -- 13 -- non-owner: OB is a member of BX with the same authority, but is not I3's owner
300
+ select pg_temp.authn(pg_temp.u('OB.auth'));
301
+ select is(
302
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I3.agent'), pg_temp.v('BX'))),
303
+ '42501:only the delegate''s owner may add it to a board',
304
+ 'owner only: a board member who is not the instance''s owner is refused');
305
+ -- 14 -- no invite authority: OA is not a member of BZ
306
+ select pg_temp.authn(pg_temp.u('OA.auth'));
307
+ select is(
308
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I3.agent'), pg_temp.v('BZ'))),
309
+ '42501:adding a delegate requires the owner''s own member-invite authority on this board',
310
+ 'the owner''s own member-invite authority is required: refused on a board the owner is not on');
311
+ -- 15 -- service_role has no resolved actor
312
+ select pg_temp.svc();
313
+ select is(
314
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I3.agent'), pg_temp.v('BX'))),
315
+ '42501:current agent principal not resolved',
316
+ 'a service_role session (no actor) cannot add a delegate');
317
+ -- 16 -- only a delegate is added this way
318
+ select pg_temp.authn(pg_temp.u('OA.auth'));
319
+ select is(
320
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('K2'), pg_temp.v('BY'))),
321
+ '42501:add_delegate_to_board adds only a hosted delegate',
322
+ 'add_delegate_to_board refuses a target without the delegate fact');
323
+
324
+ -- ===========================================================================
325
+ -- 4. The allow set, each entry AS agent_board_delegate (lives, then a state check)
326
+ -- ===========================================================================
327
+ create temp table _ex(signature text primary key);
328
+ select pg_temp.deleg(pg_temp.u('I1.auth'), pg_temp.u('I1.cred'));
329
+ -- 17 -- create_or_find_thread
330
+ select pg_temp.put('T1', pg_temp.as_d_val($$ select thread_id::text from agent_board.create_or_find_thread(null, 'hosted delegate thread', 'hosted-key-1') $$));
331
+ insert into _ex values ('create_or_find_thread(uuid, text, text, text, jsonb)');
332
+ select is(
333
+ (select created_by_agent_id::text from agent_board.threads where id::text = pg_temp.v('T1')),
334
+ pg_temp.v('I1.agent'),
335
+ 'allow set: create_or_find_thread as a delegate creates a thread owned by the delegate');
336
+ -- 18 -- post_board_message by id
337
+ select is(
338
+ pg_temp.as_d(format($$ select agent_board.post_board_message(%L::uuid, null, %L::uuid, 'subj 1', 'delegate post one',
339
+ 'comment', 'medium', null, '{"delegate": {"delegate": false, "forged": true}}'::jsonb) $$,
340
+ pg_temp.v('T1'), pg_temp.v('BX'))),
341
+ 'ok',
342
+ 'allow set: post_board_message(by id) lives as a delegate');
343
+ insert into _ex values ('post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb)');
344
+ -- 19
345
+ select is(
346
+ (select count(*)::int from agent_board.messages m
347
+ where m.thread_id::text = pg_temp.v('T1') and m.author_agent_id = pg_temp.u('I1.agent') and m.body = 'delegate post one'
348
+ and exists (select 1 from agent_board.delivery_recipients dr where dr.source_message_id = m.id and dr.board_id = pg_temp.u('BX'))),
349
+ 1,
350
+ 'state: the post is authored by the delegate''s own agent id and delivered to the board');
351
+ -- 20 -- post_board_message by slug
352
+ select is(
353
+ pg_temp.as_d(format($$ select agent_board.post_board_message(%L::uuid, null::uuid, 'hosted-bx'::text, 'addressing_v1'::text, 'subj 2'::text, 'delegate post two'::text, 'comment', 'medium', null::text, '{}'::jsonb) $$,
354
+ pg_temp.v('T1'))),
355
+ 'ok',
356
+ 'allow set: post_board_message(by slug) lives as a delegate');
357
+ insert into _ex values ('post_board_message(uuid, uuid, text, text, text, text, text, text, text, jsonb)');
358
+ -- 21
359
+ select is(
360
+ (select count(*)::int from agent_board.messages where thread_id::text = pg_temp.v('T1') and author_agent_id = pg_temp.u('I1.agent') and body = 'delegate post two'),
361
+ 1,
362
+ 'state: the by-slug post exists, authored by the delegate');
363
+ -- 22 -- post_message (reply)
364
+ select is(
365
+ pg_temp.as_d(format($$ select agent_board.post_message(%L::uuid, null, 'delegate reply') $$, pg_temp.v('T1'))),
366
+ 'ok',
367
+ 'allow set: post_message (reply in a thread) lives as a delegate');
368
+ insert into _ex values ('post_message(uuid, uuid, text, text, uuid, jsonb)');
369
+ -- 23
370
+ select is(
371
+ (select count(*)::int from agent_board.messages where thread_id::text = pg_temp.v('T1') and author_agent_id = pg_temp.u('I1.agent') and body = 'delegate reply'),
372
+ 1,
373
+ 'state: the reply exists, authored by the delegate');
374
+ -- 24 -- the ambient schema preflight
375
+ select is(
376
+ pg_temp.as_d_val($$ select version::text from agent_board.get_schema_version() $$),
377
+ '8',
378
+ 'allow set (ambient): get_schema_version answers a delegate with the schema version');
379
+ insert into _ex values ('get_schema_version()');
380
+ -- 25 -- every rpc / ambient_rpc in the list was exercised above (the list, not this file, is the set)
381
+ select is(
382
+ (select coalesce(string_agg(signature, ' | ' order by signature), '(none)')
383
+ from agent_board.delegate_allowed_operations
384
+ where kind in ('rpc', 'ambient_rpc') and signature not in (select signature from _ex)),
385
+ '(none)',
386
+ 'every rpc and ambient_rpc entry of delegate_allowed_operations is exercised as a delegate in this file');
387
+ -- 26 -- the predicates, as the delegate
388
+ select is(
389
+ pg_temp.as_d_val(format($$ select concat_ws(' ',
390
+ (agent_board.current_agent_id() = %L::uuid)::text,
391
+ (%L::uuid = any (agent_board._delegate_visible_board_ids()))::text, (%L::uuid = any (agent_board._delegate_visible_board_ids()))::text,
392
+ agent_board._delegate_visible_agent(%L::uuid)::text, agent_board._delegate_visible_agent(%L::uuid)::text) $$,
393
+ pg_temp.v('I1.agent'), pg_temp.v('BX'), pg_temp.v('BZ'), pg_temp.v('K2'), pg_temp.v('Z1'))),
394
+ 'true true false true false',
395
+ 'predicates: current_agent_id is the delegate; BX visible, BZ not; a co-member visible, a BZ-only agent not');
396
+ -- 27 -- every read_view returns rows to the delegate (the list read from the table)
397
+ select is(
398
+ (select string_agg(o.name || '=' || (pg_temp.as_d_val(format('select count(*) from agent_board.%I', o.name))::int > 0)::text, ' ' order by o.name)
399
+ from agent_board.delegate_allowed_operations o where o.kind = 'read_view'),
400
+ 'v_agent_dashboard=true v_agent_inbox=true v_board_feed=true v_board_memberships=true v_thread_feed=true',
401
+ 'every read_view of the allow set returns rows to a delegate on its own board');
402
+ -- 28
403
+ select is(
404
+ pg_temp.as_d_val(format($$ select count(*) from agent_board.v_agent_dashboard where agent_id = %L $$, pg_temp.v('I1.agent'))),
405
+ '1',
406
+ 'v_agent_dashboard returns the delegate''s own row (resolveActingAgentId needs it)');
407
+
408
+ -- ===========================================================================
409
+ -- 5. Attribution (#583 requirement 4, §Attribution)
410
+ -- ===========================================================================
411
+ select pg_temp.deleg(pg_temp.u('I2.auth'), pg_temp.u('I2.cred'));
412
+ select pg_temp.put('T2', pg_temp.as_d_val($$ select thread_id::text from agent_board.create_or_find_thread(null, 'second', 'hosted-key-2') $$));
413
+ select pg_temp.as_d(format($$ select agent_board.post_board_message(%L::uuid, null, %L::uuid, 'subj', 'delegate two post') $$, pg_temp.v('T2'), pg_temp.v('BX')));
414
+ -- 29
415
+ select is(
416
+ (select string_agg(m.author_agent_id::text || '|' || (m.metadata -> 'delegate' ->> 'instance_id'), ' ' order by m.metadata -> 'delegate' ->> 'instance_id')
417
+ from agent_board.messages m where m.body in ('delegate post one', 'delegate two post')),
418
+ pg_temp.v('I1.agent') || '|inst_aaaaaaaaaaaaaaaa_01 ' || pg_temp.v('I2.agent') || '|inst_aaaaaaaaaaaaaaaa_02',
419
+ 'T1: two siblings'' posts are attributed to two distinct authors, each carrying its own instance id');
420
+ -- 30
421
+ select is(
422
+ (select m.metadata -> 'delegate' from agent_board.messages m where m.body = 'delegate post one'),
423
+ jsonb_build_object('delegate', true, 'instance_id', 'inst_aaaaaaaaaaaaaaaa_01', 'owner_agent_id', pg_temp.v('OA'),
424
+ 'client_id', 'client-claude-ai', 'client_verified', false),
425
+ 'the server writes the delegate mark (delegate:true, the owner link, the client, client_verified:false), overwriting the caller''s forged one');
426
+ -- 31
427
+ select is(
428
+ (select count(*)::int from agent_board.signature_evidence se
429
+ join agent_board.messages m on m.id = se.source_message_id
430
+ where m.author_agent_id in (pg_temp.u('I1.agent'), pg_temp.u('I2.agent'))),
431
+ 0,
432
+ 'signed:false -- a delegate''s posts carry no signature evidence');
433
+ -- 32 -- a keyed agent cannot forge the mark
434
+ select pg_temp.authn(pg_temp.u('K.auth'));
435
+ select pg_temp.try(format($$ select agent_board.post_board_message(%L::uuid, null, %L::uuid, 'k', 'keyed forged', 'comment', 'medium', null,
436
+ '{"delegate": {"delegate": true, "instance_id": "forged"}}'::jsonb) $$,
437
+ (select thread_id from agent_board.create_or_find_thread(pg_temp.u('K'), 'keyed', 'hosted-key-k')), pg_temp.v('BX')));
438
+ select is(
439
+ (select (metadata ? 'delegate')::text || '/' || author_agent_id::text from agent_board.messages where body = 'keyed forged'),
440
+ 'false/' || pg_temp.v('K'),
441
+ 'a keyed agent''s caller-supplied delegate key is removed: no keyed agent can forge the mark');
442
+
443
+ -- ===========================================================================
444
+ -- 6. T2 -- reconnect keeps the instance; generations supersede
445
+ -- ===========================================================================
446
+ -- 33
447
+ select is(
448
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai'), 'I1b'),
449
+ 'ok',
450
+ 'T2: a reconnect (a new grant at generation 2, same client) is exchanged');
451
+ -- 34
452
+ select is(pg_temp.v('I1b.agent') || '/' || pg_temp.v('I1b.auth'), pg_temp.v('I1.agent') || '/' || pg_temp.v('I1.auth'),
453
+ 'T2: the reconnect keeps the instance''s agent id and auth user');
454
+ -- 35
455
+ select is(
456
+ (select string_agg((revoked_at is not null)::text, ' ' order by generation) from agent_board.agent_credentials
457
+ where id in (pg_temp.u('I1.cred'), pg_temp.u('I1b.cred'))),
458
+ 'true false',
459
+ 'T2: the higher generation revoked the generation-1 credential in the same transaction, and its own is live');
460
+ -- 36 -- the old credential fails a direct board call; the new one works
461
+ select pg_temp.deleg(pg_temp.u('I1.auth'), pg_temp.u('I1.cred'));
462
+ select is(
463
+ pg_temp.as_d(format($$ select agent_board.post_message(%L::uuid, null, 'with the superseded credential') $$, pg_temp.v('T1'))),
464
+ '42501:current agent principal not resolved',
465
+ 'T2: the superseded generation-1 credential fails a direct board call');
466
+ -- 37
467
+ select pg_temp.deleg(pg_temp.u('I1.auth'), pg_temp.u('I1b.cred'));
468
+ select is(
469
+ pg_temp.as_d(format($$ select agent_board.post_message(%L::uuid, null, 'with the generation-2 credential') $$, pg_temp.v('T1'))),
470
+ 'ok',
471
+ 'T2: the generation-2 credential works');
472
+ -- 38 -- refresh: same grant, same generation
473
+ select is(
474
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai'), 'I1c')
475
+ || '/' || (pg_temp.v('I1c.agent') = pg_temp.v('I1.agent'))::text
476
+ || '/' || pg_temp.q(format($q$select (revoked_at is null)::text from agent_board.agent_credentials where id = %L$q$, pg_temp.v('I1b.cred'))),
477
+ 'ok/true/true',
478
+ 'T2: a refresh (same grant and generation) keeps the instance and revokes nothing');
479
+ -- 39
480
+ select is(
481
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'x'),
482
+ '42501:generation_superseded',
483
+ 'a lower generation is refused');
484
+ -- 40
485
+ select is(
486
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_X', 2, 'hosted-owner-a', 'client-claude-ai'), 'x'),
487
+ '42501:grant_conflict',
488
+ 'an equal generation with a different grant is refused');
489
+ -- 41
490
+ select is(
491
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-chatgpt'), 'x'),
492
+ '42501:client_mismatch',
493
+ 'an equal generation from another client is refused');
494
+ -- 42 -- a new instance never reuses a sibling's identity
495
+ select is(
496
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_04', 'grant_aaaaaaaaaaaaaa_04_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'I4')
497
+ || '/' || (pg_temp.v('I4.agent') not in (pg_temp.v('I1.agent'), pg_temp.v('I2.agent'), pg_temp.v('I3.agent')))::text,
498
+ 'ok/true',
499
+ 'T2: a "new assistant" is a new instance with a new agent, never a sibling''s');
500
+
501
+ -- ===========================================================================
502
+ -- 7. T4 -- substitution and client changes
503
+ -- ===========================================================================
504
+ -- 43
505
+ select is(
506
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_3', 3, 'hosted-owner-b', 'client-claude-ai'), 'x'),
507
+ '42501:owner_mismatch',
508
+ 'T4: an exchange naming another owner for an existing instance is refused (the owner is immutable)');
509
+ -- 44
510
+ select is(
511
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_03', 'grant_aaaaaaaaaaaaaa_03_2', 2, 'hosted-owner-a', 'client-chatgpt'), 'x'),
512
+ '42501:client_mismatch',
513
+ 'a higher generation from another client WITHOUT the owner''s cross-client confirmation is refused');
514
+ -- 45
515
+ select is(
516
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_03', 'grant_aaaaaaaaaaaaaa_03_2', 2, 'hosted-owner-a', 'client-chatgpt',
517
+ '{"from_client_id": "client-claude-ai", "confirmed_at": 1790000000, "confirmed_generation": 2}'::jsonb), 'I3b'),
518
+ 'ok',
519
+ '...and accepted WITH it at the higher generation');
520
+ -- 46
521
+ select is(
522
+ (select di.client_id || '/' || di.client_change_generation || '/' || (di.agent_id = pg_temp.u('I3.agent'))::text
523
+ from agent_board.delegate_instances di where di.instance_id = 'inst_aaaaaaaaaaaaaaaa_03')
524
+ || '/' || (select (revoked_at is not null)::text from agent_board.agent_credentials where id = pg_temp.u('I3.cred')),
525
+ 'client-chatgpt/2/true/true',
526
+ 'the new client and confirmation are recorded, the instance keeps its id, and the old generation''s credential is revoked');
527
+ -- 47 -- a sibling's agent id substituted in a direct board call
528
+ select pg_temp.deleg(pg_temp.u('I1.auth'), pg_temp.u('I1b.cred'));
529
+ select is(
530
+ pg_temp.as_d(format($$ select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 's', 'as my sibling') $$,
531
+ pg_temp.v('T1'), pg_temp.v('I2.agent'), pg_temp.v('BX'))),
532
+ '42501:p_author_agent_id must match current_agent_id()',
533
+ 'T4: a sibling instance''s agent id substituted in a direct board call is refused');
534
+ -- 48 -- a delegate row with NO credential claim resolves to null
535
+ select pg_temp.deleg(pg_temp.u('I1.auth'), null);
536
+ select is(
537
+ agent_board.current_agent_id(),
538
+ null,
539
+ 'IF-0-HOSTED-6: a delegate token without its credential claim resolves to NULL (never the null-claim branch)');
540
+ -- 49 -- ...and a keyed agent is unchanged
541
+ select pg_temp.authn(pg_temp.u('K.auth'));
542
+ select is(agent_board.current_agent_id(), pg_temp.u('K'),
543
+ 'T5: a keyed agent without a credential claim still resolves through the null-claim branch');
544
+ -- 50 -- every delegate row is an instance's agent, and no keyed row carries the fact
545
+ select is(
546
+ (select count(*)::int from agent_board.agents a
547
+ where a.is_delegate and not exists (select 1 from agent_board.delegate_instances di where di.agent_id = a.id)),
548
+ 0,
549
+ 'T5: every row with the delegate fact belongs to an instance (keyed rows were backfilled false)');
550
+ -- 51
551
+ select is(
552
+ pg_temp.try(format($$ update agent_board.agents set is_delegate = false where id = %L $$, pg_temp.v('I1.agent'))),
553
+ '23514:agents.is_delegate never changes on an existing row',
554
+ 'the delegate fact never changes on an existing row');
555
+
556
+ -- ===========================================================================
557
+ -- 8. What stays refused (§What stays refused for a delegate; D15)
558
+ -- ===========================================================================
559
+ -- 52 -- role writer
560
+ select pg_temp.authn(pg_temp.u('K.auth'));
561
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
562
+ pg_temp.try(format($$ select agent_board.grant_board_role(%L, %L, 'admin') $$, pg_temp.v('BX'), pg_temp.v('I1.agent'))),
563
+ '42501:grant_board_role cannot place or raise a hosted delegate; add_delegate_to_board is its only way onto a board',
564
+ 'role writers: grant_board_role refuses a delegate target, so nothing raises a delegate above member');
565
+ -- 53
566
+ select is(pg_temp.state(pg_temp.u('BX'), pg_temp.u('I1.agent')), 'joined/member', '...and the delegate is still a member');
567
+ -- 54 -- placement by an invite offer
568
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
569
+ pg_temp.try(format($$ select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid) $$, pg_temp.v('BX'), pg_temp.v('I4.agent'), pg_temp.v('K'))),
570
+ '42501:a hosted delegate joins a board only through add_delegate_to_board',
571
+ 'placement: invite_board_member refuses a delegate target (an offer it could never answer)');
572
+ -- 55 -- the owner itself, as BW's owner, cannot place it by an invite (the flag is add_delegate_to_board's)
573
+ select pg_temp.authn(pg_temp.u('OA.auth'));
574
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
575
+ pg_temp.try(format($$ select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid) $$, pg_temp.v('BW'), pg_temp.v('I4.agent'), pg_temp.v('OA'))),
576
+ '42501:a hosted delegate joins a board only through add_delegate_to_board',
577
+ 'D15: even its OWNER cannot place it through another entrypoint -- the sink needs the admission flag');
578
+ -- 56 -- the admin's admission path
579
+ select pg_temp.authn(pg_temp.u('K.auth'));
580
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
581
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'joined') $$, pg_temp.v('BY'), pg_temp.v('I1.agent'))),
582
+ 'P0002:board membership not found',
583
+ 'placement: an admin cannot seat a delegate that has no membership row');
584
+ -- 57 -- a pre-existing raised row cannot be re-affirmed (the sink's post-write member assertion)
585
+ -- (through try, so a mutation that broke I4's exchange fails the assertion below, not the file)
586
+ select pg_temp.try(format($$ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
587
+ values (%L, %L, 'admin', 'joined', %L) $$, pg_temp.v('BY'), pg_temp.v('I4.agent'), pg_temp.v('K')));
588
+ select pg_temp.authn(pg_temp.u('OA.auth'));
589
+ select is(
590
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I4.agent'), pg_temp.v('BY'))),
591
+ '42501:a hosted delegate''s board role is member, never higher',
592
+ 'D15: the sink asserts member after the write -- a raised delegate row (seeded directly) is refused, not re-affirmed');
593
+ delete from agent_board.board_memberships where board_id = pg_temp.u('BY') and agent_id = pg_temp.u('I4.agent');
594
+ -- 58 -- a delegate redeeming an invite: it holds no EXECUTE on redeem_invite_link
595
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
596
+ has_function_privilege('agent_board_delegate', 'agent_board.redeem_invite_link(text,jsonb)', 'EXECUTE'),
597
+ false,
598
+ 'a delegate cannot redeem any invite: no EXECUTE on redeem_invite_link');
599
+
600
+ -- ===========================================================================
601
+ -- 9. Removal (D7) and R-LINK-7; T3 -- siblings unchanged
602
+ -- ===========================================================================
603
+ -- 59 -- the owner, a plain member of BX, withdraws I2's delegate
604
+ select pg_temp.authn(pg_temp.u('OA.auth'));
605
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
606
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'left') $$, pg_temp.v('BX'), pg_temp.v('I2.agent'))),
607
+ 'ok',
608
+ 'D7: the owner removes its delegate though it is not a board admin');
609
+ -- 60
610
+ select is(
611
+ pg_temp.state(pg_temp.u('BX'), pg_temp.u('I2.agent')) || ' ' || pg_temp.last_event(pg_temp.u('BX'), pg_temp.u('I2.agent')),
612
+ 'left/member principal-authored/principal_withdraw/' || pg_temp.v('OA') || '/left',
613
+ 'D7: recorded principal-authored, kind principal_withdraw, the owner as actor');
614
+ -- 61
615
+ select is(pg_temp.state(pg_temp.u('BX'), pg_temp.u('I1.agent')), 'joined/member',
616
+ 'T3: one instance''s membership change leaves its sibling''s membership unchanged');
617
+ -- 62 -- the owner-check mutation pin: service_role setting the flag still cannot place it
618
+ select pg_temp.svc();
619
+ select set_config('agent_board.delegate_admission', pg_temp.v('I2.agent'), true);
620
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
621
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'joined') $$, pg_temp.v('BX'), pg_temp.v('I2.agent'))),
622
+ '42501:a hosted delegate joins a board only through add_delegate_to_board',
623
+ 'D15: a service_role session that sets the admission flag is still refused -- it is not the owner');
624
+ select set_config('agent_board.delegate_admission', '', true);
625
+ -- 63 -- a board admin removes a delegate
626
+ select pg_temp.authn(pg_temp.u('K.auth'));
627
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
628
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'removed') $$, pg_temp.v('BX'), pg_temp.v('I1.agent')))
629
+ || ' ' || pg_temp.last_event(pg_temp.u('BX'), pg_temp.u('I1.agent')),
630
+ 'ok board-authored/eviction/' || pg_temp.v('K') || '/removed',
631
+ 'a board admin removes a delegate through the existing path (board-authored eviction)');
632
+ -- 64 -- R-LINK-7
633
+ select pg_temp.authn(pg_temp.u('OA.auth'));
634
+ select is(
635
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I1.agent'), pg_temp.v('BX'))),
636
+ '42501:a removed delegate stays removed; a board admin must lift the removal',
637
+ 'R-LINK-7: the owner cannot re-add a delegate an admin removed');
638
+ -- 65 -- D7 never launders an eviction
639
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
640
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'left') $$, pg_temp.v('BX'), pg_temp.v('I1.agent')))
641
+ || ' ' || pg_temp.state(pg_temp.u('BX'), pg_temp.u('I1.agent')),
642
+ '42501:a principal may withdraw its delegate only from an active or pending membership removed/member',
643
+ 'D7: the owner cannot turn `removed` into `left` (the eviction is not laundered)');
644
+
645
+ -- ===========================================================================
646
+ -- 10. D12 -- a delegate leaves when its owner does
647
+ -- ===========================================================================
648
+ -- 66 -- the owner leaves BY on its own
649
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
650
+ pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'left') $$, pg_temp.v('BY'), pg_temp.v('OA'))),
651
+ 'ok',
652
+ 'D12 setup: the owner leaves BY');
653
+ -- 67
654
+ select is(
655
+ pg_temp.state(pg_temp.u('BY'), pg_temp.u('I3.agent')) || ' ' || pg_temp.last_event(pg_temp.u('BY'), pg_temp.u('I3.agent')),
656
+ 'left/member principal-authored/principal_withdraw/' || pg_temp.v('OA') || '/left',
657
+ 'D12: its delegate on BY moved to left in the same transaction, recorded as the principal''s withdrawal');
658
+ -- 68 -- the owner's return does not restore it
659
+ select pg_temp.svc();
660
+ select pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'joined') $$, pg_temp.v('BY'), pg_temp.v('OA')));
661
+ select is(pg_temp.state(pg_temp.u('BY'), pg_temp.u('OA')) || ' ' || pg_temp.state(pg_temp.u('BY'), pg_temp.u('I3.agent')),
662
+ 'joined/member left/member',
663
+ 'D12: the owner''s return does not bring its delegate back');
664
+ -- 69, 70 -- an admin removing the owner evicts its delegates too
665
+ select pg_temp.exch(pg_temp.claim('inst_bbbbbbbbbbbbbbbb_06', 'grant_bbbbbbbbbbbbbb_06_1', 1, 'hosted-owner-b', 'client-codex'), 'I6');
666
+ select pg_temp.authn(pg_temp.u('OB.auth'));
667
+ select is(
668
+ pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I6.agent'), pg_temp.v('BX'))),
669
+ 'ok',
670
+ 'D12 setup: owner B adds its delegate to BX');
671
+ select pg_temp.authn(pg_temp.u('K.auth'));
672
+ select pg_temp.try(format($$ select agent_board.update_board_membership(%L, %L, 'removed') $$, pg_temp.v('BX'), pg_temp.v('OB')));
673
+ select is(
674
+ pg_temp.state(pg_temp.u('BX'), pg_temp.u('I6.agent')) || ' ' || pg_temp.last_event(pg_temp.u('BX'), pg_temp.u('I6.agent')),
675
+ 'left/member board-authored/eviction/' || pg_temp.v('K') || '/left',
676
+ 'D12: when an admin removes the owner, its delegate moves to left, recorded as the admin''s eviction');
677
+
678
+ -- ===========================================================================
679
+ -- 11. Tombstones (IF-0-HOSTED-8); T3 -- revocation is per instance
680
+ -- ===========================================================================
681
+ select pg_temp.authn(pg_temp.u('OA.auth'));
682
+ select pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I4.agent'), pg_temp.v('BX')));
683
+ -- 71
684
+ select is(pg_temp.tomb('inst_aaaaaaaaaaaaaaaa_04', 'hosted-owner-a'), 'first=true',
685
+ 'a Portal revocation tombstones an instance');
686
+ -- 72
687
+ select is(
688
+ (select (di.tombstoned_at is not null)::text || '/' || di.tombstone_reason from agent_board.delegate_instances di where di.instance_id = 'inst_aaaaaaaaaaaaaaaa_04')
689
+ || ' ' || (select status from agent_board.agents where id = pg_temp.u('I4.agent'))
690
+ || ' ' || (select count(*) from agent_board.agent_credentials where agent_id = pg_temp.u('I4.agent') and revoked_at is null)::text,
691
+ 'true/owner_revoked revoked 0',
692
+ 'the tombstone, in one transaction: recorded, the agent revoked, every credential revoked');
693
+ -- 73
694
+ select is(
695
+ pg_temp.state(pg_temp.u('BX'), pg_temp.u('I4.agent')) || ' ' || pg_temp.last_event(pg_temp.u('BX'), pg_temp.u('I4.agent')),
696
+ 'left/member operator-designated/operator_removal/null/left',
697
+ 'the tombstone ends memberships through update_board_membership(..., ''left''): an operator_removal event');
698
+ -- 74
699
+ select pg_temp.deleg(pg_temp.u('I4.auth'), pg_temp.u('I4.cred'));
700
+ select is(agent_board.current_agent_id(), null, 'T3: the tombstoned instance''s credential no longer resolves');
701
+ -- 75 -- siblings unchanged
702
+ select is(
703
+ (select string_agg(a.status || '/' || (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.revoked_at is null)::text, ' ' order by di.instance_id)
704
+ from agent_board.delegate_instances di join agent_board.agents a on a.id = di.agent_id
705
+ where di.instance_id in ('inst_aaaaaaaaaaaaaaaa_01', 'inst_aaaaaaaaaaaaaaaa_03')),
706
+ 'active/2 active/1',
707
+ 'T3: one instance''s tombstone leaves its siblings'' agents and credentials unchanged');
708
+ -- 76 -- idempotent
709
+ select is(
710
+ pg_temp.tomb('inst_aaaaaaaaaaaaaaaa_04', 'hosted-owner-a', 'portal_admin')
711
+ || ' ' || pg_temp.q($q$select tombstone_reason from agent_board.delegate_instances where instance_id = 'inst_aaaaaaaaaaaaaaaa_04'$q$),
712
+ 'first=false owner_revoked',
713
+ 'a repeat tombstone is a no-op: first=false, nothing changes');
714
+ -- 77 -- a revoked instance is never resurrected
715
+ select is(
716
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_04', 'grant_aaaaaaaaaaaaaa_04_9', 9, 'hosted-owner-a', 'client-claude-ai'), 'x'),
717
+ '42501:instance_revoked',
718
+ 'T3: a tombstoned instance id is refused at any later exchange, whatever generation');
719
+ -- 78, 79 -- a tombstone BEFORE the first exchange
720
+ select is(pg_temp.tomb('inst_aaaaaaaaaaaaaaaa_05', 'hosted-owner-a'), 'first=true',
721
+ 'a revocation before the first exchange creates the tombstoned row');
722
+ select is(
723
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_05', 'grant_aaaaaaaaaaaaaa_05_1', 1, 'hosted-owner-a', 'client-claude-ai'), 'x')
724
+ || ' ' || pg_temp.q($q$select count(*)::text from agent_board.agents where handle = agent_board._delegate_instance_slug('https://portal.hosted.test', 'inst_aaaaaaaaaaaaaaaa_05')$q$),
725
+ '42501:instance_revoked 0',
726
+ '...so the first exchange is refused and no agent is created');
727
+ -- 80 -- a revocation naming another owner
728
+ select is(
729
+ pg_temp.tomb('inst_aaaaaaaaaaaaaaaa_03', 'hosted-owner-b')
730
+ || ' ' || pg_temp.q($q$select (tombstoned_at is null)::text from agent_board.delegate_instances where instance_id = 'inst_aaaaaaaaaaaaaaaa_03'$q$),
731
+ '42501:owner_mismatch true',
732
+ 'a revocation naming an existing instance of another owner is refused and changes nothing');
733
+ -- 81 -- a token the Portal reports revoked
734
+ select is(
735
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai', null, null, 'revoked'), 'x')
736
+ || ' ' || pg_temp.q($q$select (tombstoned_at is null)::text from agent_board.delegate_instances where instance_id = 'inst_aaaaaaaaaaaaaaaa_01'$q$),
737
+ '42501:instance_revoked true',
738
+ 'a token whose instance claim reports revoked is refused, and does not tombstone (only a Portal revocation does)');
739
+
740
+ -- ===========================================================================
741
+ -- 12. Revoking the owner revokes its delegates
742
+ -- ===========================================================================
743
+ select pg_temp.svc();
744
+ select pg_temp.try(format($$ select agent_board.revoke_human_subtree(%L, %L, 'hosted test') $$, pg_temp.v('OB.auth'), pg_temp.v('K')));
745
+ -- 82
746
+ select is((select status from agent_board.agents where id = pg_temp.u('I6.agent')), 'revoked',
747
+ 'revoke_human_subtree revokes the owner''s delegates (their owner_user_id is the owner)');
748
+ -- 83
749
+ select is(
750
+ pg_temp.exch(pg_temp.claim('inst_bbbbbbbbbbbbbbbb_06', 'grant_bbbbbbbbbbbbbb_06_2', 2, 'hosted-owner-b', 'client-codex'), 'x')
751
+ || ' ' || pg_temp.q($q$select (tombstoned_at is null)::text from agent_board.delegate_instances where instance_id = 'inst_bbbbbbbbbbbbbbbb_06'$q$),
752
+ '42501:owner_inactive true',
753
+ 'N2: an exchange for an instance of a revoked owner is refused owner_inactive (the owner check precedes the instance''s own), and does not tombstone');
754
+
755
+ -- ===========================================================================
756
+ -- 13. D8 -- the resource
757
+ -- ===========================================================================
758
+ -- 84
759
+ select is(
760
+ pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_07', 'grant_cccccccccccccc_07_1', 1, 'hosted-owner-c', 'client-c', null,
761
+ 'https://mcp.hosted.test/mcp/s/slotAAAAAAAAAAAAAAAAAAAAAAA'), 'I7')
762
+ || ' ' || pg_temp.q($q$select slot_resource from agent_board.delegate_instances where instance_id = 'inst_cccccccccccccccc_07'$q$),
763
+ 'ok https://mcp.hosted.test/mcp/s/slotAAAAAAAAAAAAAAAAAAAAAAA',
764
+ 'D8: the first exchange records the slot');
765
+ -- 85
766
+ select is(
767
+ pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_07', 'grant_cccccccccccccc_07_1', 1, 'hosted-owner-c', 'client-c'), 'x')
768
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_07', 'grant_cccccccccccccc_07_2', 2, 'hosted-owner-c', 'client-c', null,
769
+ 'https://mcp.hosted.test/mcp/s/slotBBBBBBBBBBBBBBBBBBBBBBB'), 'x'),
770
+ '42501:resource_mismatch 42501:resource_mismatch',
771
+ 'D8: a slotted instance refuses the base URL and any other slot, even at a higher generation');
772
+ -- 86
773
+ select is(
774
+ pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_08', 'grant_cccccccccccccc_08_1', 1, 'hosted-owner-c', 'client-c', null,
775
+ 'https://mcp.hosted.test/mcp/s/slotAAAAAAAAAAAAAAAAAAAAAAA'), 'x')
776
+ || ' ' || pg_temp.q($q$select count(*)::text from agent_board.delegate_instances where instance_id = 'inst_cccccccccccccccc_08'$q$),
777
+ '42501:slot_in_use 0',
778
+ 'D8: a slot belongs to one instance; another instance''s exchange for it is refused and keeps nothing');
779
+ -- 87, 88 -- the one move: base URL -> slot, only at a higher generation
780
+ select pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_09', 'grant_cccccccccccccc_09_1', 1, 'hosted-owner-c', 'client-c'), 'I9');
781
+ select is(
782
+ pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_09', 'grant_cccccccccccccc_09_1', 1, 'hosted-owner-c', 'client-c', null,
783
+ 'https://mcp.hosted.test/mcp/s/slotCCCCCCCCCCCCCCCCCCCCCCC'), 'x'),
784
+ '42501:resource_mismatch',
785
+ 'D8: an instance recorded at the base URL cannot move to a slot on the same grant');
786
+ select is(
787
+ pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_09', 'grant_cccccccccccccc_09_2', 2, 'hosted-owner-c', 'client-c', null,
788
+ 'https://mcp.hosted.test/mcp/s/slotCCCCCCCCCCCCCCCCCCCCCCC'), 'x')
789
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_cccccccccccccccc_09', 'grant_cccccccccccccc_09_3', 3, 'hosted-owner-c', 'client-c'), 'x'),
790
+ 'ok 42501:resource_mismatch',
791
+ 'D8: ...it moves once, at a higher generation, and never reverts to the base URL');
792
+
793
+ -- ===========================================================================
794
+ -- 14. D13 -- at most 20 live instances per owner
795
+ -- ===========================================================================
796
+ -- Owner C already has instances 07 and 09 live; add 18 more to reach 20.
797
+ select pg_temp.put('cap', (select string_agg(pg_temp.exch(pg_temp.claim('inst_capcapcapcapcap_' || lpad(n::text, 2, '0'),
798
+ 'grant_capcapcapcapca_' || lpad(n::text, 2, '0'), 1, 'hosted-owner-c', 'client-c'), 'cap' || n), ',')
799
+ from generate_series(1, 18) n));
800
+ -- 89
801
+ select is(
802
+ (select count(*)::int from agent_board.delegate_instances di where di.owner_user_id = pg_temp.u('OC.auth') and di.tombstoned_at is null),
803
+ 20,
804
+ 'D13 setup: owner C has 20 live instances');
805
+ -- 90
806
+ select is(
807
+ pg_temp.exch(pg_temp.claim('inst_capcapcapcapcap_21', 'grant_capcapcapcapca_21', 1, 'hosted-owner-c', 'client-c'), 'x'),
808
+ '42501:instance_cap',
809
+ 'D13: a first exchange that would give an owner a 21st live instance is refused');
810
+ -- 91
811
+ select pg_temp.tomb('inst_capcapcapcapcap_01', 'hosted-owner-c');
812
+ select is(
813
+ pg_temp.exch(pg_temp.claim('inst_capcapcapcapcap_21', 'grant_capcapcapcapca_21', 1, 'hosted-owner-c', 'client-c'), 'x'),
814
+ 'ok',
815
+ 'D13: a tombstoned instance does not count toward the cap');
816
+
817
+ -- ===========================================================================
818
+ -- 15. D16 -- nothing is addressed to a delegate except through a board it is active on
819
+ -- ===========================================================================
820
+ select pg_temp.authn(pg_temp.u('K.auth'));
821
+ select pg_temp.put('TK', (select thread_id::text from agent_board.create_or_find_thread(pg_temp.u('K'), 'keyed tasks', 'hosted-key-tasks')));
822
+ select pg_temp.put('cnt0', (select (select count(*) from agent_board.tasks)::text || '/' || (select count(*) from agent_board.escalations)::text
823
+ || '/' || (select count(*) from agent_board.delegations)::text));
824
+ -- 92
825
+ select is(
826
+ pg_temp.try(format($$ select agent_board.create_task(%L, null, 'for a delegate', null, %L) $$, pg_temp.v('TK'), pg_temp.v('I3.agent'))),
827
+ '42501:a hosted delegate is addressed only through a board it is active on (task assignment)',
828
+ 'D16: a task assigned to a delegate is refused');
829
+ -- 93
830
+ select is(
831
+ pg_temp.try(format($$ select agent_board.raise_escalation(%L, null, 'to a delegate', 'other', 'medium', null, %L) $$, pg_temp.v('TK'), pg_temp.v('I3.agent'))),
832
+ '42501:a hosted delegate is addressed only through a board it is active on (escalation assignment)',
833
+ 'D16: an escalation assigned to a delegate is refused');
834
+ -- 94
835
+ select pg_temp.put('PT', (select task_id::text from agent_board.create_task(pg_temp.u('TK'), null, 'parent')));
836
+ select is(
837
+ pg_temp.try(format($$ select agent_board.delegate_task(%L, null, 'child', null, %L) $$, pg_temp.v('PT'), pg_temp.v('I3.agent'))),
838
+ '42501:a hosted delegate is addressed only through a board it is active on (task assignment)',
839
+ 'D16: task delegation to a delegate is refused');
840
+ -- 95
841
+ select is(
842
+ (select (select count(*) from agent_board.tasks)::text || '/' || (select count(*) from agent_board.escalations)::text
843
+ || '/' || (select count(*) from agent_board.delegations)::text),
844
+ (select split_part(pg_temp.v('cnt0'), '/', 1)::int + 1)::text || '/' || split_part(pg_temp.v('cnt0'), '/', 2) || '/' || split_part(pg_temp.v('cnt0'), '/', 3),
845
+ 'D16: the refused assignments wrote no task, escalation or delegation (only the parent task exists)');
846
+ -- 96, 97 -- a multicast naming a member and a delegate refuses the WHOLE call
847
+ select pg_temp.put('cnt1', (select count(*) from agent_board.delivery_recipients)::text || '/' || (select count(*) from agent_board.inbox_items)::text);
848
+ select is(
849
+ pg_temp.try(format($$ select agent_board.send_multicast_message(%L, null, %L::jsonb, 'addressing_v1', 's', 'to a member and a delegate', 'comment', 'medium', null, '{}'::jsonb) $$,
850
+ pg_temp.v('TK'), jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v('K2')),
851
+ jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v('I3.agent')))::text)),
852
+ '42501:a hosted delegate is reached only through a board it is on',
853
+ 'D16/NEW-4: a multicast naming a member AND a delegate refuses the whole call');
854
+ select is(
855
+ (select count(*) from agent_board.delivery_recipients)::text || '/' || (select count(*) from agent_board.inbox_items)::text,
856
+ pg_temp.v('cnt1'),
857
+ 'D16/NEW-4: ...and wrote no delivery and no inbox item, for anyone');
858
+ -- 98 -- the raw-uuid overload has no resolver: the delivery writer refuses it instead
859
+ select is(
860
+ pg_temp.try(format($$ select agent_board.send_multicast_message(%L, null, array[%L, %L]::uuid[], 's', 'raw ids') $$,
861
+ pg_temp.v('TK'), pg_temp.v('K2'), pg_temp.v('I3.agent'))),
862
+ '42501:a hosted delegate is addressed only through a board it is active on (delivery)',
863
+ 'D16: the raw-id multicast naming a delegate is refused by the delivery writer');
864
+ -- 99 -- the dry run shows the denial
865
+ select is(
866
+ (select r."deniedRecipients" -> 0 ->> 'reason'
867
+ from agent_board.expand_recipients_dry_run(null,
868
+ jsonb_build_array(jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v('I3.agent'))), null) r),
869
+ 'recipient_is_hosted_delegate',
870
+ 'D16/NEW-4: the dry run reports recipient_is_hosted_delegate for an explicitly named delegate');
871
+ -- 100
872
+ select is(
873
+ (select r."deniedRecipients" -> 0 ->> 'reason'
874
+ from agent_board.expand_recipients_dry_run(null,
875
+ jsonb_build_array(jsonb_build_object('kind', 'agent_handle', 'handle',
876
+ (select handle from agent_board.agents where id = pg_temp.u('I3.agent')))), null) r),
877
+ 'recipient_is_hosted_delegate',
878
+ 'D16/NEW-4: ...and for one named by handle');
879
+ -- 101 -- an expansion never includes a delegate
880
+ select is(
881
+ (select (exists (select 1 from jsonb_array_elements(r."resolvedRecipients") e
882
+ where (e ->> 'agentId')::uuid in (select id from agent_board.agents where is_delegate)))::text
883
+ || '/' || (r."resolvedRecipients" @> jsonb_build_array(jsonb_build_object('agentId', pg_temp.v('K2'))))::text
884
+ || '/' || jsonb_array_length(r."deniedRecipients")
885
+ from agent_board.resolve_recipients(null, '[{"kind": "role_selector", "role": "agent"}]'::jsonb) r),
886
+ 'false/true/0',
887
+ 'D16/NEW-4: a role_selector expansion matching delegates delivers to the others and leaves the delegates out');
888
+ -- 102, 103 -- the board-post fan-out still reaches an active delegate member
889
+ select pg_temp.authn(pg_temp.u('OA.auth'));
890
+ select pg_temp.try(format($$ select agent_board.add_delegate_to_board(%L, %L) $$, pg_temp.v('I3.agent'), pg_temp.v('BW')));
891
+ select pg_temp.authn(pg_temp.u('K.auth'));
892
+ select pg_temp.put('TW', (select thread_id::text from agent_board.create_or_find_thread(pg_temp.u('K'), 'to BW', 'hosted-key-bw')));
893
+ select is(
894
+ pg_temp.try(format($$ select agent_board.post_board_message(%L, null, %L, 's', 'fan-out to BW') $$, pg_temp.v('TW'), pg_temp.v('BW'))),
895
+ 'ok',
896
+ 'D16: a board post on a board the delegate is active on succeeds');
897
+ select is(
898
+ (select count(*)::int from agent_board.inbox_items ii
899
+ join agent_board.messages m on m.id = ii.source_message_id
900
+ where ii.recipient_agent_id = pg_temp.u('I3.agent') and ii.board_id = pg_temp.u('BW') and m.body = 'fan-out to BW'),
901
+ 1,
902
+ 'D16: ...and its fan-out to members reaches the delegate (a board-scoped delivery)');
903
+
904
+ -- ===========================================================================
905
+ -- 16. The exchange's own shape checks
906
+ -- ===========================================================================
907
+ -- 104
908
+ select is(
909
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai') #- '{agent_board_delegate_instance,status}', 'x')
910
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai')
911
+ || jsonb_build_object('agent_board_delegate_instance', pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai') -> 'agent_board_delegate_instance' || '{"extra": 1}'::jsonb), 'x')
912
+ || ' ' || pg_temp.exch(jsonb_set(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai'),
913
+ '{agent_board_delegate_instance,owner_sub}', '"someone-else"'), 'x')
914
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_short', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai'), 'x')
915
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_01', 'grant_aaaaaaaaaaaaaa_01_2', 2, 'hosted-owner-a', 'client-claude-ai', null,
916
+ 'https://mcp.hosted.test/mcp/s/slotDDDDDDDDDDDDDDDDDDDDDDD?x=1'), 'x'),
917
+ '22023:claims_malformed 22023:claims_malformed 22023:claims_malformed 22023:claims_malformed 22023:claims_malformed',
918
+ 'the RPC refuses a missing field, an extra field, owner_sub <> sub, a malformed instance id and a slot with a query');
919
+
920
+ -- ===========================================================================
921
+ -- 17. First exchange with client_change, and the T2 worked example
922
+ -- ===========================================================================
923
+ -- 105
924
+ select is(
925
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_10', 'grant_aaaaaaaaaaaaaa_10_1', 1, 'hosted-owner-a', 'client-A'), 'x')
926
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_10', 'grant_aaaaaaaaaaaaaa_10_2', 2, 'hosted-owner-a', 'client-B',
927
+ '{"from_client_id": "client-A", "confirmed_at": 1790000001, "confirmed_generation": 2}'::jsonb), 'x')
928
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_10', 'grant_aaaaaaaaaaaaaa_10_3', 3, 'hosted-owner-a', 'client-B',
929
+ '{"from_client_id": "client-A", "confirmed_at": 1790000001, "confirmed_generation": 2}'::jsonb), 'x'),
930
+ 'ok ok ok',
931
+ 'T2 worked example: a confirmed cross-client reconnect, then a same-client reconnect still carrying the confirmation');
932
+ -- 106
933
+ select is(
934
+ pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_11', 'grant_aaaaaaaaaaaaaa_11_1', 1, 'hosted-owner-a', 'client-B',
935
+ '{"from_client_id": "client-A", "confirmed_at": 1790000001, "confirmed_generation": 1}'::jsonb), 'x')
936
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_aaaaaaaaaaaaaaaa_10', 'grant_aaaaaaaaaaaaaa_10_4', 4, 'hosted-owner-a', 'client-C',
937
+ '{"from_client_id": "client-B", "confirmed_at": 1790000002, "confirmed_generation": 2}'::jsonb), 'x'),
938
+ 'ok 42501:client_mismatch',
939
+ 'a first exchange accepts a client_change; a later client change whose confirmation predates the recorded binding is refused');
940
+
941
+ -- ===========================================================================
942
+ -- 18. The owner leaving through leave_board, and the cascade's exact reach
943
+ -- ===========================================================================
944
+ -- 107 -- I3's delegate is on BW (joined above); OA leaves BW through leave_board
945
+ select pg_temp.authn(pg_temp.u('OA.auth'));
946
+ select is( -- pgtap-allow-raise-checked: the RPC runs inside pg_temp.try, which returns a raise as text
947
+ pg_temp.try(format($$ select agent_board.leave_board(%L, %L) $$, pg_temp.v('BW'), pg_temp.v('OA')))
948
+ || ' ' || pg_temp.state(pg_temp.u('BW'), pg_temp.u('I3.agent')),
949
+ 'ok left/member',
950
+ 'D12: leave_board (another route to the same sink) also takes the owner''s delegates with it');
951
+ -- 108 -- the cascade touches only the owner's delegates on THAT board
952
+ select is(
953
+ (select count(*)::int from agent_board.board_memberships bm
954
+ join agent_board.agents a on a.id = bm.agent_id
955
+ where a.is_delegate and bm.membership_state in ('joined', 'subscribed')),
956
+ 0,
957
+ 'D12: no delegate is left active anywhere once each of its owners'' standing ended');
958
+ -- 109 -- the D12 cascade leaves non-delegates alone
959
+ select is(pg_temp.state(pg_temp.u('BW'), pg_temp.u('K')), 'joined/member',
960
+ 'D12: the owner leaving does not touch a non-delegate co-member');
961
+
962
+ -- ===========================================================================
963
+ -- 19. The ledger CHECK and guard for principal-authored
964
+ -- ===========================================================================
965
+ -- 110
966
+ select is(
967
+ pg_temp.try(format($$ insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
968
+ values (%L, %L, 'principal_placement', 'joined', 'principal-authored', %L) $$,
969
+ pg_temp.v('BX'), pg_temp.v('I1.agent'), pg_temp.v('K'))),
970
+ '23514:substrate CHECK refuses principal-authored actor that is not the subject''s principal',
971
+ 'the guard refuses a principal-authored event whose actor is not the subject''s principal');
972
+ -- 111
973
+ select is(
974
+ pg_temp.try(format($$ insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
975
+ values (%L, %L, 'principal_placement', 'joined', 'principal-authored', %L) $$,
976
+ pg_temp.v('BX'), pg_temp.v('K2'), pg_temp.v('K'))),
977
+ '23514:substrate CHECK refuses principal-authored actor that is not the subject''s principal',
978
+ '...and one whose subject is not a delegate at all');
979
+ -- 112 -- the CHECK arm itself, with the guard out of the way (the guard refuses first otherwise)
980
+ alter table agent_board.board_membership_events disable trigger trg_board_membership_events_provenance_guard;
981
+ select is(
982
+ pg_temp.try(format($$ insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
983
+ values (%L, %L, 'principal_placement', 'joined', 'principal-authored', %L) $$,
984
+ pg_temp.v('BX'), pg_temp.v('I1.agent'), pg_temp.v('I1.agent'))),
985
+ '23514:new row for relation "board_membership_events" violates check constraint "board_membership_events_provenance_actor"',
986
+ 'the CHECK arm refuses a principal-authored event whose actor is its subject');
987
+ alter table agent_board.board_membership_events enable trigger trg_board_membership_events_provenance_guard;
988
+ -- 113
989
+ select is(
990
+ (select agent_board._membership_consent_kind(pg_temp.u('BX'), pg_temp.u('I2.agent'))),
991
+ null,
992
+ 'a principal WITHDRAWAL is a removal: the withdrawn delegate has no consent kind');
993
+
994
+ -- ===========================================================================
995
+ -- 20. Visibility after removal, for the delegate itself (the full matrix is in
996
+ -- agent_board_delegate_role_class.test.sql)
997
+ -- ===========================================================================
998
+ -- 114 -- I1 was removed from BX: its own posts there are now invisible to it
999
+ select pg_temp.deleg(pg_temp.u('I1.auth'), pg_temp.u('I1b.cred'));
1000
+ select is(
1001
+ pg_temp.as_d_val(format($$ select count(*) from agent_board.messages where author_agent_id = %L $$, pg_temp.v('I1.agent'))),
1002
+ '0',
1003
+ 'D14: after its removal from BX the delegate no longer reads even the posts it authored there');
1004
+ -- 115
1005
+ select is(
1006
+ pg_temp.as_d_val(format($$ select count(*) from agent_board.v_board_feed where board_id = %L $$, pg_temp.v('BX'))),
1007
+ '0',
1008
+ 'D14: ...nor the board feed');
1009
+ -- 116 -- and it cannot post there any more
1010
+ select is(
1011
+ pg_temp.as_d(format($$ select agent_board.post_board_message(%L::uuid, null, %L::uuid, 's', 'after removal') $$, pg_temp.v('T1'), pg_temp.v('BX'))),
1012
+ '42501:board not accessible to current agent',
1013
+ 'a removed delegate cannot post to the board');
1014
+
1015
+ -- ===========================================================================
1016
+ -- 21. The compatibility report covers the role
1017
+ -- ===========================================================================
1018
+ -- 117
1019
+ select is(
1020
+ (agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'),
1021
+ 'true',
1022
+ 'signed_* stays operator-only for authenticated, anon AND agent_board_delegate');
1023
+ -- 118
1024
+ select ok(
1025
+ agent_board_test.stripped_body('agent_board.get_bundle_compatibility()'::regprocedure, true) ~ '''agent_board_delegate''',
1026
+ 'get_bundle_compatibility''s signed_write_enforced names the delegate role');
1027
+
1028
+ -- ===========================================================================
1029
+ -- 22. The admission flag never outlives add_delegate_to_board
1030
+ -- ===========================================================================
1031
+ -- 119
1032
+ select is(coalesce(current_setting('agent_board.delegate_admission', true), ''), '',
1033
+ 'D15: add_delegate_to_board clears the admission flag before returning');
1034
+ -- 120 -- the role is never a member of authenticated (it would inherit every authenticated grant)
1035
+ select is(pg_has_role('agent_board_delegate', 'authenticated', 'MEMBER'), false,
1036
+ 'agent_board_delegate is not a member of authenticated');
1037
+
1038
+ -- 121 -- delegate_instances: SELECT for service_role only (the operator check), no writes for anyone
1039
+ select is(
1040
+ (select string_agg(r || ':' || p || '=' || has_table_privilege(r, 'agent_board.delegate_instances', p)::text, ' ' order by r, p)
1041
+ from unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r
1042
+ cross join unnest(array['SELECT', 'INSERT', 'UPDATE', 'DELETE']) p),
1043
+ 'agent_board_delegate:DELETE=false agent_board_delegate:INSERT=false agent_board_delegate:SELECT=false agent_board_delegate:UPDATE=false anon:DELETE=false anon:INSERT=false anon:SELECT=false anon:UPDATE=false authenticated:DELETE=false authenticated:INSERT=false authenticated:SELECT=false authenticated:UPDATE=false service_role:DELETE=false service_role:INSERT=false service_role:SELECT=true service_role:UPDATE=false',
1044
+ 'delegate_instances is written only by its two RPCs; service_role may only read it');
1045
+
1046
+ -- ===========================================================================
1047
+ -- 23. N2 -- a revoked or suspended owner has no hosted path (fix lane, PR #590)
1048
+ -- ===========================================================================
1049
+ -- The exchange refuses owner_inactive when (iss, sub) is in fleet_human_revocations or the
1050
+ -- owner's human row (agents.auth_user_id = the owner uid) is not active, as
1051
+ -- enroll_federated_human does. Each refusal is checked to have written nothing.
1052
+ create function pg_temp.n2_rows(p_iid text) returns text language sql as $$
1053
+ select (select count(*) from agent_board.delegate_instances where instance_id = p_iid)::text
1054
+ || ' ' || (select count(*) from agent_board.agents
1055
+ where handle = agent_board._delegate_instance_slug('https://portal.hosted.test', p_iid))::text
1056
+ || ' ' || (select count(*) from auth.users
1057
+ where email = 'agent+' || agent_board._delegate_instance_slug('https://portal.hosted.test', p_iid) || '@agent-board.local')::text
1058
+ $$;
1059
+ -- 122 -- owner B was revoked by revoke_human_subtree (section 12): a NEW instance is refused too
1060
+ select is(
1061
+ pg_temp.exch(pg_temp.claim('inst_bbbbbbbbbbbbbbbb_n2', 'grant_bbbbbbbbbbbbbb_n2_1', 1, 'hosted-owner-b', 'client-codex'), 'x')
1062
+ || ' ' || pg_temp.n2_rows('inst_bbbbbbbbbbbbbbbb_n2'),
1063
+ '42501:owner_inactive 0 0 0',
1064
+ 'N2: a fleet-revoked human cannot bring up a new delegate instance: refused, and no instance, agent or auth user is written');
1065
+
1066
+ -- Owner D: a federated human, enrolled through the real RPC.
1067
+ do $$
1068
+ declare r record;
1069
+ begin
1070
+ perform set_config('request.jwt.claims', '', true);
1071
+ select * into r from agent_board.enroll_federated_human(
1072
+ 'https://portal.hosted.test', 'message-board', 'hosted-owner-d',
1073
+ jsonb_build_object('sub', 'hosted-owner-d'), 'Owner D');
1074
+ perform pg_temp.put('OD', r.agent_id::text);
1075
+ end $$;
1076
+ -- 123 -- control: a live owner exchanges
1077
+ select is(
1078
+ pg_temp.exch(pg_temp.claim('inst_dddddddddddddddd_01', 'grant_dddddddddddddd_01_1', 1, 'hosted-owner-d', 'client-d'), 'D1')
1079
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_dddddddddddddddd_02', 'grant_dddddddddddddd_02_1', 1, 'hosted-owner-d', 'client-d'), 'D2'),
1080
+ 'ok ok',
1081
+ 'N2 control: an active, unrevoked owner''s first exchanges succeed');
1082
+ -- 124 -- the delegate itself suspended, the owner live: still agent_inactive
1083
+ select pg_temp.svc();
1084
+ select pg_temp.try(format($$ select agent_board.revoke_agent(%L, 'hosted n2 test') $$, pg_temp.v('D2.agent')));
1085
+ select is(
1086
+ pg_temp.exch(pg_temp.claim('inst_dddddddddddddddd_02', 'grant_dddddddddddddd_02_1', 1, 'hosted-owner-d', 'client-d'), 'x')
1087
+ || ' ' || pg_temp.q($q$select (tombstoned_at is null)::text from agent_board.delegate_instances where instance_id = 'inst_dddddddddddddddd_02'$q$),
1088
+ '42501:agent_inactive true',
1089
+ 'an exchange for a suspended delegate of a live owner is refused agent_inactive, and does not tombstone');
1090
+ -- 125 -- the OWNER suspended (revoke_agent): its existing delegate stops exchanging
1091
+ select pg_temp.svc();
1092
+ select pg_temp.try(format($$ select agent_board.revoke_agent(%L, 'hosted n2 test') $$, pg_temp.v('OD')));
1093
+ select is(
1094
+ pg_temp.q(format($q$select status from agent_board.agents where id = %L$q$, pg_temp.v('OD')))
1095
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_dddddddddddddddd_01', 'grant_dddddddddddddd_01_1', 1, 'hosted-owner-d', 'client-d'), 'x')
1096
+ || ' ' || pg_temp.q($q$select (tombstoned_at is null)::text from agent_board.delegate_instances where instance_id = 'inst_dddddddddddddddd_01'$q$)
1097
+ || ' ' || pg_temp.q(format($q$select count(*)::text from agent_board.agent_credentials where agent_id = %L and revoked_at is null$q$, pg_temp.v('D1.agent'))),
1098
+ 'suspended 42501:owner_inactive true 1',
1099
+ 'N2: a suspended owner''s existing delegate is refused owner_inactive: no new credential, no tombstone');
1100
+ -- 126
1101
+ select is(
1102
+ pg_temp.exch(pg_temp.claim('inst_dddddddddddddddd_03', 'grant_dddddddddddddd_03_1', 1, 'hosted-owner-d', 'client-d'), 'x')
1103
+ || ' ' || pg_temp.n2_rows('inst_dddddddddddddddd_03'),
1104
+ '42501:owner_inactive 0 0 0',
1105
+ 'N2: a suspended owner cannot bring up a new delegate instance, and nothing is written');
1106
+
1107
+ -- Owner E: never enrolled as a human on this board (no human row), then durably revoked. The
1108
+ -- marker is SEEDED here, by hand, so that E's delegate row stays active and 128 isolates the
1109
+ -- marker arm alone (revoke_human_subtree would also revoke the delegate). The row is the shape
1110
+ -- revoke_human_subtree writes, keyed by the verified (iss, sub). The real revocation of a
1111
+ -- delegate-only owner, which now writes the marker from the instance's recorded (iss, sub)
1112
+ -- (#591 item 4, AB15), is section 24.
1113
+ -- 127 -- control: an owner with no human row is allowed, as before
1114
+ select is(
1115
+ pg_temp.exch(pg_temp.claim('inst_eeeeeeeeeeeeeeee_01', 'grant_eeeeeeeeeeeeee_01_1', 1, 'hosted-owner-e', 'client-e'), 'E1'),
1116
+ 'ok',
1117
+ 'N2 control: an owner the board has never enrolled as a human is not refused for the absent row');
1118
+ insert into agent_board.fleet_human_revocations(issuer, subject, owner_user_id, revoked_by_agent_id, revocation_reason)
1119
+ values ('https://portal.hosted.test', 'hosted-owner-e',
1120
+ (select owner_user_id from agent_board.delegate_instances where instance_id = 'inst_eeeeeeeeeeeeeeee_01'),
1121
+ pg_temp.u('K'), 'hosted n2 test');
1122
+ -- 128
1123
+ select is(
1124
+ pg_temp.exch(pg_temp.claim('inst_eeeeeeeeeeeeeeee_01', 'grant_eeeeeeeeeeeeee_01_1', 1, 'hosted-owner-e', 'client-e'), 'x')
1125
+ || ' ' || pg_temp.q(format($q$select status || '/' || (select count(*) from agent_board.agent_credentials where agent_id = %L and revoked_at is null)::text from agent_board.agents where id = %L$q$, pg_temp.v('E1.agent'), pg_temp.v('E1.agent'))),
1126
+ '42501:owner_inactive active/1',
1127
+ 'N2: the revocation marker alone refuses the exchange of an existing instance (the delegate row itself is still active)');
1128
+ -- 129
1129
+ select is(
1130
+ pg_temp.exch(pg_temp.claim('inst_eeeeeeeeeeeeeeee_02', 'grant_eeeeeeeeeeeeee_02_1', 1, 'hosted-owner-e', 'client-e'), 'x')
1131
+ || ' ' || pg_temp.n2_rows('inst_eeeeeeeeeeeeeeee_02'),
1132
+ '42501:owner_inactive 0 0 0',
1133
+ 'N2: the revocation marker alone refuses a new instance, and nothing is written');
1134
+
1135
+ -- ===========================================================================
1136
+ -- 24. #591 item 4 -- the owner's (iss, sub) on each instance, and the marker it gives
1137
+ -- revoke_human_subtree (AB15); #591 item 9 -- owner_inactive before instance_revoked
1138
+ -- ===========================================================================
1139
+ -- An owner whose only principals are hosted delegates has no enrollment audit row, so before
1140
+ -- 1.59.0 revoke_human_subtree wrote it no marker, and a fresh first exchange passed. Both RPCs now
1141
+ -- record the verified pair, and the revocation derives a marker from it.
1142
+ -- plpgsql, not sql: each read runs as its own query, so it sees what an action earlier in the
1143
+ -- same statement wrote (the reason pg_temp.q exists).
1144
+ create function pg_temp.pair(p_iid text) returns text language plpgsql as $$
1145
+ begin
1146
+ return coalesce((select coalesce(owner_issuer, '(null)') || ' ' || coalesce(owner_subject, '(null)')
1147
+ from agent_board.delegate_instances where instance_id = p_iid), '(no row)');
1148
+ end $$;
1149
+ create function pg_temp.owner_of(p_iid text) returns uuid language plpgsql as $$
1150
+ begin
1151
+ return (select owner_user_id from agent_board.delegate_instances where instance_id = p_iid);
1152
+ end $$;
1153
+ create function pg_temp.markers(p_sub text) returns text language plpgsql as $$
1154
+ begin
1155
+ return (select count(*)::text || coalesce(' ' || string_agg(r.owner_user_id::text, ','), '')
1156
+ from agent_board.fleet_human_revocations r
1157
+ where r.issuer = 'https://portal.hosted.test' and r.subject = p_sub);
1158
+ end $$;
1159
+ -- 130 -- the exchange records the pair (D1 was exchanged in 123)
1160
+ select is(pg_temp.pair('inst_dddddddddddddddd_01'), 'https://portal.hosted.test hosted-owner-d',
1161
+ 'AB15: the exchange records the owner''s verified (iss, sub) on the instance');
1162
+ -- 131 -- a revocation before the first exchange records it too (owner H has nothing else)
1163
+ select is(
1164
+ pg_temp.tomb('inst_hhhhhhhhhhhhhhhh_01', 'hosted-owner-h') || ' ' || pg_temp.pair('inst_hhhhhhhhhhhhhhhh_01'),
1165
+ 'first=true https://portal.hosted.test hosted-owner-h',
1166
+ 'AB15: a pre-exchange tombstone records the owner''s verified (iss, sub) on the row it creates');
1167
+ -- 132 -- once recorded, the pair never changes
1168
+ select is(
1169
+ pg_temp.try($q$update agent_board.delegate_instances set owner_subject = 'someone-else' where instance_id = 'inst_dddddddddddddddd_01'$q$)
1170
+ || ' ' || pg_temp.pair('inst_dddddddddddddddd_01'),
1171
+ '23514:delegate_instances owner identity never changes once recorded https://portal.hosted.test hosted-owner-d',
1172
+ 'AB15: the recorded owner pair is immutable');
1173
+
1174
+ -- Owner G: never enrolled, live; the control owner, and the legacy-row fills.
1175
+ select pg_temp.svc();
1176
+ -- 133 -- a LEGACY row (pre-1.59.0 shape: no pair) is filled by its first exchange
1177
+ select is(
1178
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_01', 'grant_gggggggggggggg_01_1', 1, 'hosted-owner-g', 'client-g'), 'G1')
1179
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id) values ('inst_gggggggggggggggg_l1', %L), ('inst_gggggggggggggggg_l2', %L)$q$,
1180
+ pg_temp.owner_of('inst_gggggggggggggggg_01'), pg_temp.owner_of('inst_gggggggggggggggg_01')))
1181
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1')
1182
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_l1', 'grant_gggggggggggggg_l1_1', 1, 'hosted-owner-g', 'client-g'), 'GL1')
1183
+ || ' ' || pg_temp.pair('inst_gggggggggggggggg_l1'),
1184
+ 'ok ok (null) (null) ok https://portal.hosted.test hosted-owner-g',
1185
+ 'AB15: a legacy instance with no recorded pair is filled by its next exchange');
1186
+ -- 134 -- ...and by a tombstone
1187
+ select is(
1188
+ pg_temp.tomb('inst_gggggggggggggggg_l2', 'hosted-owner-g') || ' ' || pg_temp.pair('inst_gggggggggggggggg_l2'),
1189
+ 'first=true https://portal.hosted.test hosted-owner-g',
1190
+ 'AB15: a legacy instance with no recorded pair is filled by its tombstone');
1191
+
1192
+ -- Owner F: never enrolled as a human; its ONLY principal is a hosted delegate.
1193
+ -- 135 -- revoke_human_subtree for real, as service_role
1194
+ select is(
1195
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_1', 1, 'hosted-owner-f', 'client-f'), 'F1')
1196
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1197
+ pg_temp.owner_of('inst_ffffffffffffffff_01'), pg_temp.v('K')))
1198
+ || ' ' || pg_temp.q(format($q$select status from agent_board.agents where id = %L$q$, pg_temp.v('F1.agent'))),
1199
+ 'ok ok revoked',
1200
+ '#591 item 4: an owner with only a hosted delegate is revoked, and its delegate with it');
1201
+ -- 136 -- the marker exists, keyed by the verified pair, naming the owner
1202
+ select is(pg_temp.markers('hosted-owner-f'), '1 ' || pg_temp.owner_of('inst_ffffffffffffffff_01')::text,
1203
+ '#591 item 4: revoking a delegate-only owner writes the durable (iss, sub) marker from the instance''s recorded pair');
1204
+ -- 137 -- so a fresh first exchange is refused, and writes nothing
1205
+ select is(
1206
+ pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_02', 'grant_ffffffffffffff_02_1', 1, 'hosted-owner-f', 'client-f'), 'x')
1207
+ || ' ' || pg_temp.n2_rows('inst_ffffffffffffffff_02'),
1208
+ '42501:owner_inactive 0 0 0',
1209
+ '#591 item 4: after the revocation a new first exchange for that owner is refused owner_inactive, and nothing is written');
1210
+ -- 138 -- control: another live owner, never enrolled, still brings up a new instance
1211
+ select is(
1212
+ pg_temp.exch(pg_temp.claim('inst_gggggggggggggggg_02', 'grant_gggggggggggggg_02_1', 1, 'hosted-owner-g', 'client-g'), 'G2')
1213
+ || ' ' || pg_temp.markers('hosted-owner-g'),
1214
+ 'ok 0',
1215
+ '#591 item 4 control: a different, unrevoked owner''s first exchange succeeds and has no marker');
1216
+ -- 139 -- #591 item 9: a revoked owner AND a tombstoned instance -> owner_inactive, not instance_revoked
1217
+ select is(
1218
+ pg_temp.tomb('inst_ffffffffffffffff_01', 'hosted-owner-f')
1219
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_ffffffffffffffff_01', 'grant_ffffffffffffff_01_2', 2, 'hosted-owner-f', 'client-f'), 'x'),
1220
+ 'first=true 42501:owner_inactive',
1221
+ 'AB14 precedence: the owner check runs before the instance''s own refusals -- a tombstoned instance of a revoked owner is refused owner_inactive, not instance_revoked');
1222
+ -- 140 -- owner H, whose only row is a pre-exchange tombstone (131): its revocation marks it too
1223
+ select is(
1224
+ pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1225
+ pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01'), pg_temp.v('K')))
1226
+ || ' ' || pg_temp.markers('hosted-owner-h')
1227
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_hhhhhhhhhhhhhhhh_02', 'grant_hhhhhhhhhhhhhh_02_1', 1, 'hosted-owner-h', 'client-h'), 'x')
1228
+ || ' ' || pg_temp.n2_rows('inst_hhhhhhhhhhhhhhhh_02'),
1229
+ 'ok 1 ' || pg_temp.owner_of('inst_hhhhhhhhhhhhhhhh_01')::text || ' 42501:owner_inactive 0 0 0',
1230
+ '#591 item 4: an owner whose only row is a pre-exchange tombstone is marked by its revocation, and a new instance is refused');
1231
+ -- 141 -- the class: every function that WRITES delegate_instances (any INSERT, UPDATE, MERGE or
1232
+ -- DELETE, from the catalog, through the shared matcher family) is one of the two RPCs that record
1233
+ -- the pair, and every row this file created carries one except the two hand-inserted legacy rows,
1234
+ -- which their exchange and tombstone filled (133, 134). A new writer fails this and names itself.
1235
+ -- Every non-system schema, not only agent_board: a writer in public or extensions bypasses the pair
1236
+ -- just as well (mutation-checked with one in public). agent_board_test is excluded because it holds
1237
+ -- this suite's matcher functions, which carry relation names as data and write nothing.
1238
+ select is(
1239
+ (select string_agg(p.proname, ',' order by p.proname)
1240
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
1241
+ where n.nspname not in ('pg_catalog', 'information_schema', 'agent_board_test')
1242
+ and n.nspname !~ '^pg_'
1243
+ and p.prokind in ('f', 'p')
1244
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.write_re('delegate_instances'))
1245
+ || ' ' || (select count(*) from agent_board.delegate_instances where owner_issuer is null)::text,
1246
+ 'exchange_delegate_credential,tombstone_delegate_instance 0',
1247
+ 'AB15: exactly the exchange and the tombstone write delegate_instances (catalog-derived), and no row is left without the owner pair');
1248
+
1249
+ -- 142 -- the pair is both-or-neither and never empty (delegate_instances_owner_identity_shape)
1250
+ select is(
1251
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer) values ('inst_gggggggggggggggg_s1', %L, 'https://portal.hosted.test')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01')))
1252
+ || ' ' || pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, owner_issuer, owner_subject) values ('inst_gggggggggggggggg_s2', %L, 'https://portal.hosted.test', '')$q$, pg_temp.owner_of('inst_gggggggggggggggg_01'))),
1253
+ '23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape" 23514:new row for relation "delegate_instances" violates check constraint "delegate_instances_owner_identity_shape"',
1254
+ 'AB15: an owner pair is recorded whole or not at all, and never empty');
1255
+
1256
+ -- 143 -- a LEGACY TOMBSTONED row (pre-1.59.0: tombstoned, no pair) is filled by a repeated
1257
+ -- revocation. Nothing else ever writes such a row again: the exchange refuses it
1258
+ -- (instance_revoked) before its UPDATE, and a tombstone used to return early. Owner J has only this
1259
+ -- row (never enrolled, never exchanged), so the pair is the only evidence revoke_human_subtree can
1260
+ -- mark it from. Then a new first exchange of J is refused owner_inactive and writes nothing.
1261
+ select pg_temp.put('J.owner', agent_board._ensure_agent_auth_user(
1262
+ agent_board._federated_identity_slug('https://portal.hosted.test', 'hosted-owner-j'))::text);
1263
+ select is(
1264
+ pg_temp.try(format($q$insert into agent_board.delegate_instances(instance_id, owner_user_id, tombstoned_at, tombstone_reason) values ('inst_jjjjjjjjjjjjjjjj_l1', %L, now(), 'owner_revoked')$q$, pg_temp.v('J.owner')))
1265
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1266
+ || ' ' || pg_temp.tomb('inst_jjjjjjjjjjjjjjjj_l1', 'hosted-owner-j')
1267
+ || ' ' || pg_temp.pair('inst_jjjjjjjjjjjjjjjj_l1')
1268
+ || ' ' || pg_temp.try(format($q$select agent_board.revoke_human_subtree(%L, %L, 'hosted 591 test')$q$,
1269
+ pg_temp.v('J.owner'), pg_temp.v('K')))
1270
+ || ' ' || pg_temp.markers('hosted-owner-j')
1271
+ || ' ' || pg_temp.exch(pg_temp.claim('inst_jjjjjjjjjjjjjjjj_02', 'grant_jjjjjjjjjjjjjj_02_1', 1, 'hosted-owner-j', 'client-j'), 'x')
1272
+ || ' ' || pg_temp.n2_rows('inst_jjjjjjjjjjjjjjjj_02'),
1273
+ 'ok (null) (null) first=false https://portal.hosted.test hosted-owner-j ok 1 ' || pg_temp.v('J.owner')
1274
+ || ' 42501:owner_inactive 0 0 0',
1275
+ 'AB15: a legacy tombstoned row is filled by a repeated revocation, so revoking its delegate-only owner marks it and a new instance is refused');
1276
+
1277
+ select * from finish();
1278
+ rollback;