@consiliency/agent-board-schema 1.10.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/fixtures/already-applied.json +136 -0
  2. package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
  3. package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
  4. package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
  5. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  6. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  7. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  8. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  9. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  10. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  11. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  12. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  13. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  14. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  15. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  16. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  17. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  18. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  19. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  20. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  21. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  22. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  23. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  24. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  25. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  26. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  27. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  28. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  29. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  30. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  31. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  32. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  33. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  34. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  35. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  36. package/package.json +1 -1
  37. package/schema-manifest.json +309 -3
  38. package/src/tooling.test.mjs +123 -2
  39. package/tests/agent_board_actorspoof.test.sql +349 -0
  40. package/tests/agent_board_agentsdir.test.sql +154 -0
  41. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  42. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  43. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  44. package/tests/agent_board_claimkey.test.sql +229 -0
  45. package/tests/agent_board_consent_backfill.test.sql +48 -0
  46. package/tests/agent_board_consent_emission.test.sql +601 -0
  47. package/tests/agent_board_consent_gating.test.sql +489 -0
  48. package/tests/agent_board_consent_substrate.test.sql +720 -0
  49. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  50. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  51. package/tests/agent_board_delegation_policies.test.sql +107 -1
  52. package/tests/agent_board_fedsession.test.sql +6 -4
  53. package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
  54. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  55. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  56. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  57. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  58. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  59. package/tests/agent_board_fleet_policy.test.sql +2 -2
  60. package/tests/agent_board_gatenull_membstate.test.sql +352 -0
  61. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  62. package/tests/agent_board_identity_providers.test.sql +9 -0
  63. package/tests/agent_board_inbox_reads.test.sql +29 -12
  64. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  65. package/tests/agent_board_invitepolicy.test.sql +205 -0
  66. package/tests/agent_board_leaveidem.test.sql +194 -0
  67. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  68. package/tests/agent_board_mint_harden.test.sql +303 -0
  69. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  70. package/tests/agent_board_obsgate.test.sql +178 -0
  71. package/tests/agent_board_obsgate2.test.sql +153 -0
  72. package/tests/agent_board_obsgate3.test.sql +170 -0
  73. package/tests/agent_board_obsgate4.test.sql +196 -0
  74. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  75. package/tests/agent_board_pubexec_class.test.sql +53 -0
  76. package/tests/agent_board_rejectsem.test.sql +271 -0
  77. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  78. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  79. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  80. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  81. package/tests/agent_board_rolereset.test.sql +351 -0
  82. package/tests/agent_board_roomdeleg.test.sql +5 -3
  83. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  84. package/tests/agent_board_roomlink.test.sql +25 -9
  85. package/tests/agent_board_rowid.test.sql +1274 -0
  86. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  87. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  88. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  89. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  90. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  91. package/tests/agent_board_signing_store.test.sql +6 -6
  92. package/tests/fixtures/isolation-substrate.sql +93 -0
  93. package/tests/fixtures/test-prereqs.sql +33 -0
  94. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  95. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  96. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  97. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  98. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  99. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  100. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  101. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  102. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  103. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  104. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  105. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  106. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  107. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  108. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  109. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -0,0 +1,349 @@
1
+ -- MBHYGIENE ACTORSPOOF pgTAP: bind addressing actors to the resolved principal.
2
+ --
3
+ -- These are SECURITY DEFINER RPC probes. Authenticated posture is therefore
4
+ -- driven by JWT claims only; SET LOCAL ROLE would not affect the RPC execution
5
+ -- and would make the required _is_service_role() control unavailable.
6
+ create extension if not exists pgtap with schema extensions;
7
+ set search_path = extensions, public, agent_board;
8
+
9
+ begin;
10
+
11
+ select plan(13);
12
+
13
+ create temp table _actorspoof_fixture (
14
+ actor_user uuid,
15
+ actor_agent uuid,
16
+ foreign_agent uuid,
17
+ recipient_agent uuid,
18
+ thread_key text
19
+ ) on commit drop;
20
+
21
+ do $$
22
+ declare
23
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
24
+ v_actor_user uuid;
25
+ v_actor_agent uuid := gen_random_uuid();
26
+ v_foreign_agent uuid := gen_random_uuid();
27
+ v_recipient_agent uuid := gen_random_uuid();
28
+ v_thread_key text := 'actorspoof-thread-' || v_suffix;
29
+ begin
30
+ v_actor_user := public.ensure_github_user(
31
+ 'actorspoof-actor-' || v_suffix,
32
+ 'actorspoof-actor-' || v_suffix || '@example.test',
33
+ 'actorspoof-actor-' || v_suffix,
34
+ 'ACTORSPOOF Actor'
35
+ );
36
+
37
+ insert into agent_board.agents(
38
+ id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id
39
+ )
40
+ values
41
+ (
42
+ v_actor_agent,
43
+ 'agent',
44
+ 'agent',
45
+ 'actorspoof-actor-' || v_suffix,
46
+ 'ACTORSPOOF Actor',
47
+ 'active',
48
+ v_actor_user,
49
+ v_actor_user
50
+ ),
51
+ (
52
+ v_foreign_agent,
53
+ 'agent',
54
+ 'agent',
55
+ 'actorspoof-foreign-' || v_suffix,
56
+ 'ACTORSPOOF Foreign',
57
+ 'active',
58
+ null,
59
+ null
60
+ ),
61
+ (
62
+ v_recipient_agent,
63
+ 'agent',
64
+ 'agent',
65
+ 'actorspoof-recipient-' || v_suffix,
66
+ 'ACTORSPOOF Recipient',
67
+ 'active',
68
+ null,
69
+ null
70
+ );
71
+
72
+ insert into agent_board.threads(
73
+ id, thread_type, created_by_agent_id, subject, metadata
74
+ )
75
+ values (
76
+ gen_random_uuid(),
77
+ 'coordination',
78
+ v_foreign_agent,
79
+ 'ACTORSPOOF foreign reusable thread',
80
+ jsonb_build_object('addressingThreadKey', v_thread_key)
81
+ );
82
+
83
+ insert into _actorspoof_fixture(
84
+ actor_user, actor_agent, foreign_agent, recipient_agent, thread_key
85
+ )
86
+ values (
87
+ v_actor_user, v_actor_agent, v_foreign_agent, v_recipient_agent, v_thread_key
88
+ );
89
+ end
90
+ $$;
91
+
92
+ -- 1. Service/operator control.
93
+ select set_config('request.jwt.claim.role', '', true);
94
+ select set_config('request.jwt.claim.sub', '', true);
95
+ select is(
96
+ agent_board._is_service_role(),
97
+ true,
98
+ 'control: service-role addressing block really runs as service_role');
99
+
100
+ -- 2-5. ALLOW: service_role retains explicit-actor operation and its NULL contract.
101
+ select is(
102
+ (
103
+ select jsonb_build_object(
104
+ 'count', jsonb_array_length(r."resolvedRecipients"),
105
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}'
106
+ )
107
+ from agent_board.resolve_recipients(
108
+ (select foreign_agent from _actorspoof_fixture),
109
+ jsonb_build_array(
110
+ jsonb_build_object(
111
+ 'kind', 'agent_id',
112
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
113
+ )
114
+ )
115
+ ) r
116
+ ),
117
+ (
118
+ select jsonb_build_object(
119
+ 'count', 1,
120
+ 'recipient', recipient_agent::text
121
+ )
122
+ from _actorspoof_fixture
123
+ ),
124
+ 'service_role may resolve recipients with an explicit actor');
125
+
126
+ select is(
127
+ (
128
+ select jsonb_build_object(
129
+ 'count', jsonb_array_length(r."resolvedRecipients"),
130
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}',
131
+ 'reuse', r."idempotentThreadReuse"
132
+ )
133
+ from agent_board.expand_recipients_dry_run(
134
+ (select foreign_agent from _actorspoof_fixture),
135
+ jsonb_build_array(
136
+ jsonb_build_object(
137
+ 'kind', 'agent_id',
138
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
139
+ )
140
+ ),
141
+ (select thread_key from _actorspoof_fixture)
142
+ ) r
143
+ ),
144
+ (
145
+ select jsonb_build_object(
146
+ 'count', 1,
147
+ 'recipient', recipient_agent::text,
148
+ 'reuse', true
149
+ )
150
+ from _actorspoof_fixture
151
+ ),
152
+ 'service_role explicit actor retains the idempotent-thread reuse probe');
153
+
154
+ select throws_ok(
155
+ $$
156
+ select *
157
+ from agent_board.resolve_recipients(
158
+ null,
159
+ jsonb_build_array(
160
+ jsonb_build_object(
161
+ 'kind', 'agent_id',
162
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
163
+ )
164
+ )
165
+ )
166
+ $$,
167
+ '22023',
168
+ null,
169
+ 'service_role resolve_recipients still requires a non-NULL actor');
170
+
171
+ select throws_ok(
172
+ $$
173
+ select *
174
+ from agent_board.expand_recipients_dry_run(
175
+ null,
176
+ jsonb_build_array(
177
+ jsonb_build_object(
178
+ 'kind', 'agent_id',
179
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
180
+ )
181
+ ),
182
+ (select thread_key from _actorspoof_fixture)
183
+ )
184
+ $$,
185
+ '22023',
186
+ null,
187
+ 'service_role expand_recipients_dry_run still requires a non-NULL actor');
188
+
189
+ -- Authenticated SECURITY DEFINER probes: JWT claims only, never SET LOCAL ROLE.
190
+ select set_config('request.jwt.claim.role', 'authenticated', true);
191
+ select set_config(
192
+ 'request.jwt.claim.sub',
193
+ (select actor_user::text from _actorspoof_fixture),
194
+ true
195
+ );
196
+
197
+ -- 6-7. Required non-vacuity controls.
198
+ select is(
199
+ agent_board._is_service_role(),
200
+ false,
201
+ 'control: authenticated ACTORSPOOF block is not service_role');
202
+ select is(
203
+ agent_board.current_agent_id(),
204
+ (select actor_agent from _actorspoof_fixture),
205
+ 'control: authenticated ACTORSPOOF block resolves the expected actor');
206
+
207
+ -- 8-9. DENY: a caller-supplied foreign actor is rejected at both public RPCs.
208
+ select throws_ok(
209
+ $$
210
+ select *
211
+ from agent_board.resolve_recipients(
212
+ (select foreign_agent from _actorspoof_fixture),
213
+ jsonb_build_array(
214
+ jsonb_build_object(
215
+ 'kind', 'agent_id',
216
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
217
+ )
218
+ )
219
+ )
220
+ $$,
221
+ '42501',
222
+ null,
223
+ 'authenticated resolve_recipients rejects a foreign p_actor_agent_id');
224
+
225
+ select throws_ok(
226
+ $$
227
+ select *
228
+ from agent_board.expand_recipients_dry_run(
229
+ (select foreign_agent from _actorspoof_fixture),
230
+ jsonb_build_array(
231
+ jsonb_build_object(
232
+ 'kind', 'agent_id',
233
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
234
+ )
235
+ ),
236
+ (select thread_key from _actorspoof_fixture)
237
+ )
238
+ $$,
239
+ '42501',
240
+ null,
241
+ 'authenticated expand_recipients_dry_run rejects a foreign p_actor_agent_id');
242
+
243
+ -- 10-13. ALLOW: self and omitted actors preserve the resolved recipient set.
244
+ select is(
245
+ (
246
+ select jsonb_build_object(
247
+ 'count', jsonb_array_length(r."resolvedRecipients"),
248
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}'
249
+ )
250
+ from agent_board.resolve_recipients(
251
+ (select actor_agent from _actorspoof_fixture),
252
+ jsonb_build_array(
253
+ jsonb_build_object(
254
+ 'kind', 'agent_id',
255
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
256
+ )
257
+ )
258
+ ) r
259
+ ),
260
+ (
261
+ select jsonb_build_object('count', 1, 'recipient', recipient_agent::text)
262
+ from _actorspoof_fixture
263
+ ),
264
+ 'authenticated resolve_recipients accepts the caller self id');
265
+
266
+ select is(
267
+ (
268
+ select jsonb_build_object(
269
+ 'count', jsonb_array_length(r."resolvedRecipients"),
270
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}'
271
+ )
272
+ from agent_board.resolve_recipients(
273
+ null,
274
+ jsonb_build_array(
275
+ jsonb_build_object(
276
+ 'kind', 'agent_id',
277
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
278
+ )
279
+ )
280
+ ) r
281
+ ),
282
+ (
283
+ select jsonb_build_object('count', 1, 'recipient', recipient_agent::text)
284
+ from _actorspoof_fixture
285
+ ),
286
+ 'authenticated resolve_recipients binds an omitted actor to the caller');
287
+
288
+ select is(
289
+ (
290
+ select jsonb_build_object(
291
+ 'count', jsonb_array_length(r."resolvedRecipients"),
292
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}',
293
+ 'reuse', r."idempotentThreadReuse"
294
+ )
295
+ from agent_board.expand_recipients_dry_run(
296
+ (select actor_agent from _actorspoof_fixture),
297
+ jsonb_build_array(
298
+ jsonb_build_object(
299
+ 'kind', 'agent_id',
300
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
301
+ )
302
+ ),
303
+ (select thread_key from _actorspoof_fixture)
304
+ ) r
305
+ ),
306
+ (
307
+ select jsonb_build_object(
308
+ 'count', 1,
309
+ 'recipient', recipient_agent::text,
310
+ 'reuse', false
311
+ )
312
+ from _actorspoof_fixture
313
+ ),
314
+ 'authenticated dry-run accepts self without leaking a foreign reuse probe');
315
+
316
+ select is(
317
+ (
318
+ select jsonb_build_object(
319
+ 'count', jsonb_array_length(r."resolvedRecipients"),
320
+ 'recipient', r."resolvedRecipients" #>> '{0,agentId}',
321
+ 'reuse', r."idempotentThreadReuse"
322
+ )
323
+ from agent_board.expand_recipients_dry_run(
324
+ null,
325
+ jsonb_build_array(
326
+ jsonb_build_object(
327
+ 'kind', 'agent_id',
328
+ 'agentId', (select recipient_agent::text from _actorspoof_fixture)
329
+ )
330
+ ),
331
+ (select thread_key from _actorspoof_fixture)
332
+ ) r
333
+ ),
334
+ (
335
+ select jsonb_build_object(
336
+ 'count', 1,
337
+ 'recipient', recipient_agent::text,
338
+ 'reuse', false
339
+ )
340
+ from _actorspoof_fixture
341
+ ),
342
+ 'authenticated dry-run binds an omitted actor without leaking foreign reuse metadata');
343
+
344
+ select set_config('request.jwt.claim.role', '', true);
345
+ select set_config('request.jwt.claim.sub', '', true);
346
+
347
+ select finish();
348
+
349
+ rollback;
@@ -0,0 +1,154 @@
1
+ -- AGENTSDIR (#80) pgTAP — agent_board.agents column read posture for `authenticated`.
2
+ --
3
+ -- Proves: (a) the SHAPE of the grant (so deleting the migration turns CI red — no test anywhere
4
+ -- referenced this grant/policy before, meaning the exposure could be reintroduced silently);
5
+ -- (b) directory columns stay readable; (c) every identity-linkage column is denied; (d) NO product
6
+ -- regression — v_board_feed / v_board_directory / v_agent_dashboard return the same rows as before
7
+ -- and invite-by-handle still resolves a stranger.
8
+ --
9
+ -- HAND-COUNTED plan: 3 shape + 1 allow + 5 deny + 4 no-regression = 13 assertions.
10
+ --
11
+ -- METHODOLOGY (load-bearing — this exact mistake was made and caught during the #80 audit):
12
+ -- column grants and RLS are enforced against the DATABASE ROLE, not JWT claims. Setting
13
+ -- request.jwt.claim.* while still connected as `postgres` measures an RLS/grant-EXEMPT session and
14
+ -- silently reports "no leak" (or a false "no change"). Every read below is therefore captured
15
+ -- inside `set local role authenticated`, stashed in a temp table, and asserted after `reset role`
16
+ -- (pgTAP's own functions are not assumed executable by `authenticated`).
17
+ -- Teardown clears BOTH jwt GUCs: `reset role` alone leaves request.jwt.claim.role set, which would
18
+ -- poison later assertions in the same file (precedent: agent_board_fedsession.test.sql).
19
+
20
+ create extension if not exists pgtap with schema extensions;
21
+ set search_path = extensions, public, agent_board;
22
+
23
+ begin;
24
+
25
+ select plan(13);
26
+
27
+ -- ---------------------------------------------------------------------------
28
+ -- Phase 0 — GRANT SHAPE. Asserted from the catalog, so removing the migration fails CI.
29
+ -- ---------------------------------------------------------------------------
30
+ -- 1
31
+ select is(
32
+ (select coalesce(array_agg(column_name::text order by column_name), '{}')
33
+ from information_schema.column_privileges
34
+ where table_schema = 'agent_board' and table_name = 'agents'
35
+ and grantee = 'authenticated' and privilege_type = 'SELECT'),
36
+ array['agent_type','created_at','display_name','handle','id','kind','status','updated_at'],
37
+ 'authenticated may SELECT exactly the eight directory columns of agents');
38
+
39
+ -- 2 — the identity-linkage columns must be absent from that grant.
40
+ select is(
41
+ (select count(*)::int
42
+ from information_schema.column_privileges
43
+ where table_schema = 'agent_board' and table_name = 'agents'
44
+ and grantee = 'authenticated' and privilege_type = 'SELECT'
45
+ and column_name in ('auth_user_id','owner_user_id','parent_agent_id','root_agent_id','metadata')),
46
+ 0, 'authenticated holds NO grant on auth_user_id/owner_user_id/lineage/metadata');
47
+
48
+ -- 3 — anon must hold nothing at all on this table.
49
+ select is(
50
+ (select count(*)::int from information_schema.column_privileges
51
+ where table_schema = 'agent_board' and table_name = 'agents' and grantee = 'anon'),
52
+ 0, 'anon holds no privileges on agent_board.agents');
53
+
54
+ -- ---------------------------------------------------------------------------
55
+ -- Fixtures + behavioural capture as a REAL authenticated principal
56
+ -- ---------------------------------------------------------------------------
57
+ create temp table _ad (k text primary key, v text) on commit drop;
58
+ grant all on table _ad to authenticated, service_role;
59
+
60
+ do $$
61
+ declare
62
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
63
+ v_owner uuid; v_board uuid; v_reader uuid; v_uid uuid; v_th uuid;
64
+ begin
65
+ -- an owner + a board + a message, and a separate reader who is a co-member
66
+ insert into agent_board.agents(agent_type, kind, handle, display_name, status)
67
+ values ('agent', 'agent'::agent_board.agent_kind, 'ad-own-' || s, 'AD Owner', 'active')
68
+ returning id into v_owner;
69
+ insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
70
+ values ('ad-b-' || s, 'AD Board', v_owner, 'invite_only', 'active') returning id into v_board;
71
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
72
+ values (v_board, v_owner, 'owner', 'joined', v_owner);
73
+
74
+ v_uid := gen_random_uuid();
75
+ insert into auth.users(id, instance_id, aud, role, email, created_at, updated_at)
76
+ values (v_uid, '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
77
+ 'ad-' || s || '@example.test', now(), now());
78
+ insert into agent_board.agents(agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
79
+ values ('agent', 'agent'::agent_board.agent_kind, 'ad-rd-' || s, 'AD Reader', 'active', v_uid, v_uid)
80
+ returning id into v_reader;
81
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
82
+ values (v_board, v_reader, 'member', 'joined', v_owner);
83
+
84
+ select thread_id into v_th from agent_board.create_thread(v_owner, 'ad thread');
85
+ perform agent_board.post_board_message(v_th, v_owner, v_board, 'seed', 'body');
86
+
87
+ insert into _ad values ('reader_uid', v_uid::text), ('owner_handle', 'ad-own-' || s);
88
+ end
89
+ $$;
90
+
91
+ select set_config('request.jwt.claim.sub', (select v from _ad where k = 'reader_uid'), true);
92
+ select set_config('request.jwt.claim.role', 'authenticated', true);
93
+
94
+ set local role authenticated;
95
+ do $$
96
+ declare n int;
97
+ begin
98
+ -- ALLOW: directory columns
99
+ begin
100
+ select count(*) into n from (select id, handle, display_name, kind, agent_type, status
101
+ from agent_board.agents) q;
102
+ insert into _ad values ('directory_readable', 'yes:' || n);
103
+ exception when others then insert into _ad values ('directory_readable', 'ERR ' || sqlstate); end;
104
+ -- DENY: each identity-linkage column, probed individually
105
+ begin select count(auth_user_id) into n from agent_board.agents; insert into _ad values ('auth_user_id','READABLE');
106
+ exception when others then insert into _ad values ('auth_user_id', sqlstate); end;
107
+ begin select count(owner_user_id) into n from agent_board.agents; insert into _ad values ('owner_user_id','READABLE');
108
+ exception when others then insert into _ad values ('owner_user_id', sqlstate); end;
109
+ begin select count(parent_agent_id) into n from agent_board.agents; insert into _ad values ('parent_agent_id','READABLE');
110
+ exception when others then insert into _ad values ('parent_agent_id', sqlstate); end;
111
+ begin select count(root_agent_id) into n from agent_board.agents; insert into _ad values ('root_agent_id','READABLE');
112
+ exception when others then insert into _ad values ('root_agent_id', sqlstate); end;
113
+ begin select count(*) into n from agent_board.agents where metadata is not null; insert into _ad values ('metadata','READABLE');
114
+ exception when others then insert into _ad values ('metadata', sqlstate); end;
115
+ -- NO REGRESSION: the product read surfaces
116
+ begin select count(*) into n from agent_board.v_board_feed; insert into _ad values ('v_board_feed', n::text);
117
+ exception when others then insert into _ad values ('v_board_feed', 'ERR ' || sqlstate); end;
118
+ begin select count(*) into n from agent_board.v_board_directory; insert into _ad values ('v_board_directory', n::text);
119
+ exception when others then insert into _ad values ('v_board_directory', 'ERR ' || sqlstate); end;
120
+ begin select count(*) into n from agent_board.v_agent_dashboard; insert into _ad values ('v_agent_dashboard', n::text);
121
+ exception when others then insert into _ad values ('v_agent_dashboard', 'ERR ' || sqlstate); end;
122
+ -- NO REGRESSION: invite-by-handle resolves an agent the reader does NOT own (row-scoping killed this)
123
+ begin
124
+ select count(*) into n from agent_board.resolve_agent_handle((select v from _ad where k = 'owner_handle'));
125
+ insert into _ad values ('invite_by_handle', n::text);
126
+ exception when others then insert into _ad values ('invite_by_handle', 'ERR ' || sqlstate); end;
127
+ end
128
+ $$;
129
+ reset role;
130
+ select set_config('request.jwt.claim.sub', '', true);
131
+ select set_config('request.jwt.claim.role', '', true);
132
+
133
+ -- ---------------------------------------------------------------------------
134
+ -- Assertions (as postgres, over what the authenticated session actually observed)
135
+ -- ---------------------------------------------------------------------------
136
+ -- 4 — ALLOW control. Without this, a fail-closed bug would look like a successful lockdown.
137
+ select alike(( select v from _ad where k = 'directory_readable'), 'yes:%',
138
+ 'ALLOW CONTROL: authenticated can still read the directory columns');
139
+ -- 5..9 — DENY, one per identity-linkage column (42501 = insufficient_privilege)
140
+ select is((select v from _ad where k = 'auth_user_id'), '42501', 'auth_user_id is DENIED to authenticated');
141
+ select is((select v from _ad where k = 'owner_user_id'), '42501', 'owner_user_id is DENIED to authenticated');
142
+ select is((select v from _ad where k = 'parent_agent_id'), '42501', 'parent_agent_id (lineage) is DENIED to authenticated');
143
+ select is((select v from _ad where k = 'root_agent_id'), '42501', 'root_agent_id (lineage) is DENIED to authenticated');
144
+ select is((select v from _ad where k = 'metadata'), '42501', 'metadata is DENIED to authenticated');
145
+ -- 10..12 — NO REGRESSION on the product read surfaces (row-scoping would have emptied these)
146
+ select is((select v from _ad where k = 'v_board_feed'), '1', 'v_board_feed still returns the board message');
147
+ select is((select v from _ad where k = 'v_board_directory'), '1', 'v_board_directory still returns the board');
148
+ select isnt((select v from _ad where k = 'v_agent_dashboard'), '0', 'v_agent_dashboard is not emptied');
149
+ -- 13 — invite-by-handle still resolves a stranger (the feature row-scoping would have killed)
150
+ select is((select v from _ad where k = 'invite_by_handle'), '1',
151
+ 'resolve_agent_handle still resolves an agent the caller does not own (invite-by-handle intact)');
152
+
153
+ select * from finish();
154
+ rollback;
@@ -42,7 +42,7 @@ begin
42
42
  values (v_credential, v_agent, extensions.digest('authbound-refresh-' || v_suffix, 'sha256'), 1, 'authbound', now() + interval '1 day');
43
43
 
44
44
  insert into agent_board.agent_signing_keys(agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
45
- values (v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url', 'sha256:' || repeat('A', 43), 'active', 'proven')
45
+ values (v_agent, 'primary', 1, public.test_pubkey('authbound-' || v_suffix), 'raw_ed25519_base64url', public.test_key_fingerprint('authbound-' || v_suffix), 'active', 'proven')
46
46
  returning id into v_key;
47
47
 
48
48
  insert into _agent_board_authbound_fixture(user_id, agent_id, credential_id, signing_key_id)
@@ -98,6 +98,14 @@ $$;
98
98
 
99
99
  select pass('current_agent_id honors agent_credential_id revocation before JWT expiry');
100
100
 
101
+ -- #118 (MBHARDEN): _signed_write_actor_id is now revoked from authenticated — it is an
102
+ -- internal-only helper whose only callers are SECURITY DEFINER signed_* RPCs that run as the
103
+ -- owner. Its fail-closed LOGIC (raise actor_mismatch on a missing/mismatched gateway actor)
104
+ -- is role-independent, so exercise it as the owner rather than as authenticated (which would
105
+ -- now hit permission-denied before reaching the logic under test). The remaining assertions
106
+ -- below are role-agnostic catalog reads.
107
+ reset role;
108
+
101
109
  select throws_like(
102
110
  $$select agent_board._signed_write_actor_id(
103
111
  '{}'::jsonb,