@consiliency/agent-board-schema 1.12.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.
- package/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- 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,
|
|
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,
|