@consiliency/agent-board-schema 1.63.0 → 1.68.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/README.md +25 -0
- package/fixtures/already-applied.json +20 -0
- package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
- package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
- package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
- package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
- package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
- package/package.json +1 -1
- package/schema-manifest.json +48 -3
- package/src/deployment-drift-main.test.mjs +156 -0
- package/src/deployment-drift-script.test.mjs +14 -1
- package/src/policy-comment.test.mjs +17 -0
- package/src/tooling.test.mjs +10 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +3 -1
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
- package/tests/agent_board_consent_gating.test.sql +4 -4
- package/tests/agent_board_fk_lock_edges.test.sql +3 -2
- package/tests/agent_board_gatenull_membstate.test.sql +7 -4
- package/tests/agent_board_grant_target_role.test.sql +4 -4
- package/tests/agent_board_hosted_delegates.test.sql +5 -3
- package/tests/agent_board_invite_no_downgrade.test.sql +5 -3
- package/tests/agent_board_ledger_authority.test.sql +7 -0
- package/tests/agent_board_policy_correlation.test.sql +189 -0
- package/tests/agent_board_request_removed.test.sql +96 -0
- package/tests/agent_board_resolution_authority.test.sql +190 -0
- package/tests/agent_board_role_guard.test.sql +319 -0
- package/tests/agent_board_rolereset.test.sql +17 -27
- package/tests/agent_board_roomdeleg.test.sql +8 -0
- package/tests/agent_board_roomown.test.sql +3 -1
- package/tests/agent_board_rowid.test.sql +4 -1
- package/tests/agent_board_session_provenance.test.sql +106 -0
- package/tests/catalog/function_execute_privileges.json +11 -2
- package/tests/catalog/p_metadata_parameters.json +4 -0
- package/tests/isolation/expected/consent_approval_lock.out +1 -1
- package/tests/isolation/expected/last_owner_departure.out +562 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
- package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
- package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
- package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
- package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
- package/tests/isolation/specs/consent_approval_lock.spec +13 -8
- package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
- package/tests/isolation/specs/grant_target_role_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
- package/tests/isolation/specs/last_owner_departure.spec +137 -0
- package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
- package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
- package/tests/isolation/specs/session_identity_links.spec +3 -1
- package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +0 -1
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const comment = (file) => {
|
|
5
|
+
const sql = readFileSync(new URL('../migrations/' + file, import.meta.url), 'utf8');
|
|
6
|
+
const statement = sql.match(/^comment on function agent_board\._current_agent_can_access_thread\(uuid\) is\s*((?:'(?:''|[^'])*'\s*)+);/im);
|
|
7
|
+
if (!statement) throw new Error('thread-access comment missing in ' + file);
|
|
8
|
+
return [...statement[1].matchAll(/'((?:''|[^'])*)'/g)].map((part) => part[1].replaceAll("''", "'")).join('');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
it('RLS-comment-ruling: authenticated grants preserve the complete ratified thread-access comment', () => {
|
|
12
|
+
const ruling = comment('20260914000100_agent_board_claim_subject_gate.sql');
|
|
13
|
+
expect(ruling).toContain('message-board#291');
|
|
14
|
+
expect(ruling).toContain('REVERSING A RULING');
|
|
15
|
+
expect(comment('20261015000300_agent_board_rls_correlation.sql')).toBe(
|
|
16
|
+
ruling + ' Callable by authenticated for its RLS policies and by service_role.');
|
|
17
|
+
});
|
package/src/tooling.test.mjs
CHANGED
|
@@ -24,6 +24,9 @@ const historyExact = () => ({ applied: ordered.map((m) => ({ id: m.id, sha256: m
|
|
|
24
24
|
const union = () => ordered.filter((m) => m.origin !== 'message-board-forward');
|
|
25
25
|
|
|
26
26
|
describe('manifest integrity', () => {
|
|
27
|
+
it('pins the exact owner-authorized A3/A10.3 test amendment for Consiliency/message-board#675', () => {
|
|
28
|
+
expect(sha256(readFileSync(new URL('../tests/agent_board_grant_target_role.test.sql', import.meta.url)))).toBe('5c0a857d8c138f368235fa345a18c76fafa773cfe8eb57185b232d466d222f93');
|
|
29
|
+
});
|
|
27
30
|
it('aggregate digest matches recomputation over ordered per-file digests', () => {
|
|
28
31
|
const agg = sha256(Buffer.from(ordered.map((e) => `${e.order} ${e.id} ${e.sha256}`).join('\n')));
|
|
29
32
|
expect(agg).toBe(manifest.aggregateSha256);
|
|
@@ -38,9 +41,9 @@ describe('manifest integrity', () => {
|
|
|
38
41
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
42
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
43
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
44
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) + one LASTOWNER (Consiliency/message-board#675) + one SESSIONPROVENANCE (Consiliency/message-board#665) + one REQUESTREMOVED (Consiliency/message-board#680) + one RLSCORRELATION (Consiliency/message-board#589) + one RESOLUTIONAUTHORITY (Consiliency/message-board#668) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 166)', () => {
|
|
42
45
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
46
|
+
expect(manifest.migrationCount).toBe(166);
|
|
44
47
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
48
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
49
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -360,6 +363,11 @@ describe('manifest integrity', () => {
|
|
|
360
363
|
'20261012000100_agent_board_sidowner.sql',
|
|
361
364
|
// #674: grant_board_role checks the target's current role (C-A + C-B).
|
|
362
365
|
'20261014000100_agent_board_grant_target_role.sql',
|
|
366
|
+
'20261014000200_agent_board_last_owner.sql',
|
|
367
|
+
'20261015000100_agent_board_session_provenance.sql',
|
|
368
|
+
'20261015000200_agent_board_request_preserve_removed.sql',
|
|
369
|
+
'20261015000300_agent_board_rls_correlation.sql',
|
|
370
|
+
'20261015000400_agent_board_resolution_authority.sql',
|
|
363
371
|
]) {
|
|
364
372
|
expect(names.has(f)).toBe(true);
|
|
365
373
|
}
|
|
@@ -700,6 +700,7 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
|
|
|
700
700
|
-- than naming the table itself. Internal — it reads rows for an arbitrary agent id with no
|
|
701
701
|
-- authorization of its own, so service_role only, like every other helper here.
|
|
702
702
|
'agent_board._actor_fleet_revoked(uuid) [definer] => service_role',
|
|
703
|
+
'agent_board._board_memberships_last_owner_guard() [definer] => (none)',
|
|
703
704
|
'agent_board._lock_board_authority_ceiling(uuid) [invoker] => service_role',
|
|
704
705
|
'agent_board._redeem_fleet_invite_for_agent(text,uuid) [definer] => service_role',
|
|
705
706
|
'agent_board._redeem_invite_for_agent(text,uuid) [definer] => service_role',
|
|
@@ -736,7 +737,8 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
|
|
|
736
737
|
'agent_board.signed_grant_board_role(jsonb,jsonb) [definer] => service_role',
|
|
737
738
|
-- SIDSCHEMA integrity repair: the verified Portal withdrawal now takes board mutexes
|
|
738
739
|
-- before the instance/family rows. Its service-only transport is unchanged.
|
|
739
|
-
'agent_board.tombstone_delegate_instance(text,text,text,text) [definer] => service_role'
|
|
740
|
+
'agent_board.tombstone_delegate_instance(text,text,text,text) [definer] => service_role',
|
|
741
|
+
'agent_board.update_board_membership(uuid,uuid,text,jsonb) [definer] => authenticated,service_role'
|
|
740
742
|
]::text[],
|
|
741
743
|
'every function on this migration set''s surface is declared with its definer flag and exact EXECUTE grantees'
|
|
742
744
|
);
|
|
@@ -837,6 +837,13 @@ select is(
|
|
|
837
837
|
'0/0', 'generic-invite rejection leaves uses and redemption counts unchanged'
|
|
838
838
|
);
|
|
839
839
|
|
|
840
|
+
-- Preserve a second owner while exercising the invite creator's lost membership.
|
|
841
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
842
|
+
values ('16210000-0000-0000-0000-000000000002', 'agent', 'atomic-runtime-coowner', 'Atomic Runtime Coowner', 'active');
|
|
843
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
|
|
844
|
+
select board_id, '16210000-0000-0000-0000-000000000002', 'joined', 'owner' from _atomic_cfg;
|
|
845
|
+
select agent_board._lock_board_authority_ceiling(board_id) from _atomic_cfg;
|
|
846
|
+
|
|
840
847
|
update agent_board.board_memberships
|
|
841
848
|
set membership_state='removed', left_at=now(), updated_at=now()
|
|
842
849
|
where board_id=(select board_id from _atomic_cfg)
|
|
@@ -202,7 +202,7 @@ select is(pg_temp.state('c2000000-0000-4000-8000-000000000001','c1000000-0000-40
|
|
|
202
202
|
'joined', 'EC-2 join_board: re-entry after leaving still works');
|
|
203
203
|
|
|
204
204
|
-- ===========================================================================
|
|
205
|
-
-- EC-3 OPERATOR --
|
|
205
|
+
-- EC-3 OPERATOR -- direct placement remains; resolution requires owner/admin identity
|
|
206
206
|
-- ===========================================================================
|
|
207
207
|
select pg_temp.service();
|
|
208
208
|
select lives_ok($$
|
|
@@ -215,13 +215,13 @@ select is(
|
|
|
215
215
|
(select provenance from agent_board.board_membership_events
|
|
216
216
|
where agent_id = 'c1000000-0000-4000-8000-000000000006' order by seq desc limit 1),
|
|
217
217
|
'operator-designated', 'EC-3: ...recorded as an operator placement, never as consent');
|
|
218
|
-
select
|
|
218
|
+
select throws_ok($$
|
|
219
219
|
select agent_board.resolve_board_request(
|
|
220
220
|
'c2000000-0000-4000-8000-000000000001'::uuid,'c1000000-0000-4000-8000-000000000003'::uuid,
|
|
221
221
|
'joined'::text, null::uuid, '{}'::jsonb) $$,
|
|
222
|
-
'EC-3: service_role
|
|
222
|
+
'42501', null, 'EC-3: unresolved service_role cannot resolve pending membership');
|
|
223
223
|
select is(pg_temp.state('c2000000-0000-4000-8000-000000000001','c1000000-0000-4000-8000-000000000003'),
|
|
224
|
-
'
|
|
224
|
+
'requested', 'EC-3: refused service_role resolution preserves the pending row');
|
|
225
225
|
|
|
226
226
|
-- ===========================================================================
|
|
227
227
|
-- EC-6 / EC-7 -- consent is EXERCISABLE (#108), and the widening admits nobody else
|
|
@@ -297,7 +297,8 @@ select is(
|
|
|
297
297
|
'grant_board_role: agents(share) then memberships(update) [advisory]',
|
|
298
298
|
'invite_board_member: memberships(update) then agents(key share) [unserialised]',
|
|
299
299
|
'join_board: memberships(update) then agents(key share) [unserialised]',
|
|
300
|
-
|
|
300
|
+
-- ROLEGUARD: the departure RPC serializes on the board key before its sink.
|
|
301
|
+
'leave_board: memberships(update) then agents(key share) [advisory]',
|
|
301
302
|
-- SIDSCHEMA role writers use the existing consent sink and board mutex.
|
|
302
303
|
-- Their installed lock order is asserted, not exempted from the class.
|
|
303
304
|
'link_session_alias: agents(update) then memberships(update) [advisory]',
|
|
@@ -319,7 +320,7 @@ select is(
|
|
|
319
320
|
-- SIDSCHEMA integrity repair: board mutexes precede owner/instance and family row locks,
|
|
320
321
|
-- matching add_delegate_to_board's board-before-instance order. No withdrawal exemption.
|
|
321
322
|
'tombstone_delegate_instance: agents(update) then memberships(update) [advisory]',
|
|
322
|
-
'update_board_membership: memberships(update) then agents(key share) [
|
|
323
|
+
'update_board_membership: memberships(update) then agents(key share) [advisory]'
|
|
323
324
|
]::text[],
|
|
324
325
|
'FKL-9: the agents+board_memberships lock class, FK edges included (section 9''s text scan sees 3 of these)');
|
|
325
326
|
|
|
@@ -23,7 +23,7 @@ set search_path = extensions, public, agent_board;
|
|
|
23
23
|
|
|
24
24
|
begin;
|
|
25
25
|
|
|
26
|
-
select plan(
|
|
26
|
+
select plan(28);
|
|
27
27
|
|
|
28
28
|
-- ---------------------------------------------------------------------------
|
|
29
29
|
-- Phase 0: exactly one pg_proc row per name (a changed arg list would OVERLOAD, not replace)
|
|
@@ -340,13 +340,16 @@ select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
|
340
340
|
select throws_ok(
|
|
341
341
|
$$ select agent_board.resolve_board_request((select board from _f), (select nopend from _f3), 'joined', (select owner_agent from _f)) $$,
|
|
342
342
|
'P0002', null, 'NULLFIX: cannot resolve an agent with no pending request straight to joined');
|
|
343
|
-
--
|
|
343
|
+
-- Consiliency/message-board#668: resolution requires a resolved owner/admin identity.
|
|
344
344
|
reset role;
|
|
345
345
|
select set_config('request.jwt.claim.sub', '', true);
|
|
346
346
|
select set_config('request.jwt.claim.role', '', true);
|
|
347
|
-
select
|
|
347
|
+
select throws_ok(
|
|
348
348
|
$$ select agent_board.resolve_board_request((select board from _f), (select victim from _f), 'joined', (select owner_agent from _f)) $$,
|
|
349
|
-
'
|
|
349
|
+
'42501', null, 'unresolved service_role cannot resolve a pending membership');
|
|
350
|
+
select is((select membership_state from agent_board.board_memberships
|
|
351
|
+
where board_id=(select board from _f) and agent_id=(select victim from _f)),
|
|
352
|
+
'invited', 'service-role refusal preserves the pending membership');
|
|
350
353
|
|
|
351
354
|
select * from finish();
|
|
352
355
|
rollback;
|
|
@@ -611,9 +611,9 @@ select results_eq(format('select label, membership_state, role from pg_temp.g674
|
|
|
611
611
|
$$values ('G', 'joined', 'owner')$$,
|
|
612
612
|
'A3.pre');
|
|
613
613
|
select pg_temp.g674_as('A3', 'G');
|
|
614
|
-
select
|
|
614
|
+
select throws_ok(format('select * from agent_board.grant_board_role(%L::uuid, %L::uuid, %L, null)', pg_temp.g674_b('A3'), pg_temp.g674_a('A3', 'G'), 'admin'), '23514', 'last_active_board_owner: cannot remove the last active owner', 'A3.call');
|
|
615
615
|
select pg_temp.g674_service();
|
|
616
|
-
select is(pg_temp.g674_post('A3', 'G'), 'G[joined/
|
|
616
|
+
select is(pg_temp.g674_post('A3', 'G'), 'G[joined/owner/pinned] owners=1', 'A3.post');
|
|
617
617
|
|
|
618
618
|
-- A4: admin -> member, admin: ceiling 3<=3, then 3>2
|
|
619
619
|
insert into _g674_case values ('A4', pg_temp.g674_board('{"O": "owner", "G": "admin", "T": "member", "_creator": "O"}'::jsonb));
|
|
@@ -711,9 +711,9 @@ select results_eq(format('select label, membership_state, role from pg_temp.g674
|
|
|
711
711
|
$$values ('G', 'joined', 'owner')$$,
|
|
712
712
|
'A10.3.pre');
|
|
713
713
|
select pg_temp.g674_as('A10.3', 'G');
|
|
714
|
-
select
|
|
714
|
+
select throws_ok(format('select * from agent_board.grant_board_role(%L::uuid, %L::uuid, %L, null)', pg_temp.g674_b('A10.3'), pg_temp.g674_a('A10.3', 'G'), 'admin'), '23514', 'last_active_board_owner: cannot remove the last active owner', 'A10.3.call');
|
|
715
715
|
select pg_temp.g674_service();
|
|
716
|
-
select is(pg_temp.g674_post('A10.3', 'G'), 'G[joined/
|
|
716
|
+
select is(pg_temp.g674_post('A10.3', 'G'), 'G[joined/owner/pinned] owners=1', 'A10.3.post');
|
|
717
717
|
|
|
718
718
|
-- A10.4: A4 with the role gate on
|
|
719
719
|
insert into _g674_case values ('A10.4', pg_temp.g674_board('{"O": "owner", "G": "admin", "T": "member", "_creator": "O", "_gate": true}'::jsonb));
|
|
@@ -165,16 +165,17 @@ begin
|
|
|
165
165
|
end loop;
|
|
166
166
|
end $$;
|
|
167
167
|
|
|
168
|
-
-- Keyed agents: K (board owner/admin everywhere), K2 (member), Z1 (only on BZ)
|
|
168
|
+
-- Keyed agents: K (board owner/admin everywhere), K2 (member), Z1 (only on BZ),
|
|
169
|
+
-- and a coowner of BW so its principal-departure cascade leaves an active owner.
|
|
169
170
|
insert into auth.users(id, instance_id, aud, role, email)
|
|
170
171
|
select ('5e000000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
171
172
|
'00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'hosted-k' || n || '@t.test'
|
|
172
|
-
from generate_series(1,
|
|
173
|
+
from generate_series(1, 4) n;
|
|
173
174
|
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
174
175
|
select ('5e100000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, 'agent', 'agent',
|
|
175
176
|
'hosted-keyed-' || n, 'Hosted Keyed ' || n, 'active',
|
|
176
177
|
('5e000000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
|
|
177
|
-
from generate_series(1,
|
|
178
|
+
from generate_series(1, 4) n;
|
|
178
179
|
select pg_temp.put('K', '5e100000-0000-4000-8000-000000000001'),
|
|
179
180
|
pg_temp.put('K.auth', '5e000000-0000-4000-8000-000000000001'),
|
|
180
181
|
pg_temp.put('K2', '5e100000-0000-4000-8000-000000000002'),
|
|
@@ -200,6 +201,7 @@ values (pg_temp.u('BX'), pg_temp.u('K'), 'owner', 'joined', pg_temp.u('K')),
|
|
|
200
201
|
(pg_temp.u('BZ'), pg_temp.u('K'), 'owner', 'joined', pg_temp.u('K')),
|
|
201
202
|
(pg_temp.u('BZ'), pg_temp.u('Z1'), 'member', 'joined', pg_temp.u('K')),
|
|
202
203
|
(pg_temp.u('BW'), pg_temp.u('OA'), 'owner', 'joined', pg_temp.u('OA')),
|
|
204
|
+
(pg_temp.u('BW'), '5e100000-0000-4000-8000-000000000004', 'owner', 'joined', pg_temp.u('OA')),
|
|
203
205
|
(pg_temp.u('BW'), pg_temp.u('K'), 'member', 'joined', pg_temp.u('OA'));
|
|
204
206
|
|
|
205
207
|
-- ===========================================================================
|
|
@@ -513,14 +513,16 @@ select is(
|
|
|
513
513
|
-- A new view over this table is then a loud event that forces its author to decide whether it
|
|
514
514
|
-- is writable, which is the decision that would otherwise be made silently.
|
|
515
515
|
select is(
|
|
516
|
-
(select coalesce(array_agg(t.tgname::text
|
|
516
|
+
(select coalesce(array_agg(t.tgname::text || ':' || t.tgtype::text || ':' || p.proname::text
|
|
517
|
+
order by t.tgname::text), array[]::text[])
|
|
517
518
|
from pg_trigger t
|
|
518
519
|
join pg_class c on c.oid = t.tgrelid
|
|
519
520
|
join pg_namespace n on n.oid = c.relnamespace
|
|
521
|
+
join pg_proc p on p.oid = t.tgfoid
|
|
520
522
|
where n.nspname = 'agent_board' and c.relname = 'board_memberships'
|
|
521
523
|
and not t.tgisinternal),
|
|
522
|
-
array[]::text[],
|
|
523
|
-
'#317:
|
|
524
|
+
array['trg_board_memberships_last_owner_guard:27:_board_memberships_last_owner_guard']::text[],
|
|
525
|
+
'#317: only the exact UPDATE/DELETE last-owner guard is installed; new state-rewriting triggers name themselves (preserved-state controls are in agent_board_role_guard)');
|
|
524
526
|
|
|
525
527
|
select is(
|
|
526
528
|
(select coalesce(array_agg(r.rulename::text order by r.rulename::text), array[]::text[])
|
|
@@ -652,6 +652,13 @@ select is(
|
|
|
652
652
|
|
|
653
653
|
select pg_temp.service();
|
|
654
654
|
|
|
655
|
+
-- Preserve a coowner while testing the returning owner's role clamp.
|
|
656
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
657
|
+
values ('27610000-0000-4000-8000-000000000007','agent','agent','k276-coowner','K276 Coowner','active');
|
|
658
|
+
insert into agent_board.board_memberships(board_id,agent_id,role,membership_state,created_by_agent_id)
|
|
659
|
+
values ('27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000007','owner','joined','27610000-0000-4000-8000-000000000003');
|
|
660
|
+
select agent_board._lock_board_authority_ceiling('27620000-0000-4000-8000-000000000002');
|
|
661
|
+
|
|
655
662
|
select lives_ok(
|
|
656
663
|
$$create temp table _cl as select agent_board._upsert_board_membership_state(
|
|
657
664
|
'27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000003',
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
create extension if not exists pgtap with schema extensions;
|
|
2
|
+
set search_path = extensions, public, agent_board;
|
|
3
|
+
begin;
|
|
4
|
+
select plan(41);
|
|
5
|
+
create function pg_temp.a(n integer) returns uuid language sql immutable as $$
|
|
6
|
+
select ('58900000-0000-4000-8000-'||lpad(n::text,12,'0'))::uuid
|
|
7
|
+
$$;
|
|
8
|
+
create function pg_temp.claim(n integer) returns void language plpgsql as $$
|
|
9
|
+
begin perform set_config('request.jwt.claims',json_build_object('role','authenticated','sub',pg_temp.a(n))::text,true); end $$;
|
|
10
|
+
insert into auth.users(id,instance_id,aud,role,email)
|
|
11
|
+
select pg_temp.a(n),'00000000-0000-0000-0000-000000000000','authenticated','authenticated','rls589-'||n||'@test.invalid' from generate_series(1,9)n;
|
|
12
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id)
|
|
13
|
+
select pg_temp.a(n),'agent','rls589-'||n,'RLS '||n,'active',pg_temp.a(n) from generate_series(1,9)n;
|
|
14
|
+
insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility) values(pg_temp.a(200),'rls589','RLS',pg_temp.a(1),'private');
|
|
15
|
+
insert into agent_board.board_memberships(id,board_id,agent_id,membership_state,role,created_by_agent_id)
|
|
16
|
+
values(pg_temp.a(201),pg_temp.a(200),pg_temp.a(1),'joined','owner',pg_temp.a(1)),
|
|
17
|
+
(pg_temp.a(202),pg_temp.a(200),pg_temp.a(3),'joined','member',pg_temp.a(3));
|
|
18
|
+
insert into agent_board.threads(id,created_by_agent_id,subject)
|
|
19
|
+
select pg_temp.a(n),pg_temp.a(1),'RLS '||n from generate_series(100,106)n;
|
|
20
|
+
insert into agent_board.messages(id,thread_id,author_agent_id,message_sequence,body)
|
|
21
|
+
values(pg_temp.a(300),pg_temp.a(100),pg_temp.a(2),1,'Board-visible message'),
|
|
22
|
+
(pg_temp.a(301),pg_temp.a(103),pg_temp.a(1),1,'Unrelated message');
|
|
23
|
+
insert into agent_board.tasks(id,thread_id,created_by_agent_id,assigned_agent_id,title,parent_task_id,root_task_id)
|
|
24
|
+
values(pg_temp.a(400),pg_temp.a(101),pg_temp.a(1),pg_temp.a(5),'Assigned task',null,null),
|
|
25
|
+
(pg_temp.a(401),pg_temp.a(102),pg_temp.a(6),pg_temp.a(6),'Parent',null,null),
|
|
26
|
+
(pg_temp.a(402),pg_temp.a(102),pg_temp.a(7),pg_temp.a(7),'Child',pg_temp.a(401),pg_temp.a(401)),
|
|
27
|
+
(pg_temp.a(403),pg_temp.a(103),pg_temp.a(1),pg_temp.a(1),'Unrelated parent',null,null),
|
|
28
|
+
(pg_temp.a(404),pg_temp.a(103),pg_temp.a(1),pg_temp.a(1),'Unrelated child',pg_temp.a(403),pg_temp.a(403)),
|
|
29
|
+
(pg_temp.a(405),pg_temp.a(105),pg_temp.a(1),pg_temp.a(1),'Board-delivered task',null,null);
|
|
30
|
+
insert into agent_board.delegations(id,parent_task_id,child_task_id,delegated_by_agent_id,delegated_to_agent_id)
|
|
31
|
+
values(pg_temp.a(500),pg_temp.a(401),pg_temp.a(402),pg_temp.a(8),pg_temp.a(9)),
|
|
32
|
+
(pg_temp.a(501),pg_temp.a(403),pg_temp.a(404),pg_temp.a(1),pg_temp.a(1));
|
|
33
|
+
insert into agent_board.delivery_recipients(delivery_mode,recipient_agent_id,source_kind,source_message_id,source_task_id,board_id,board_membership_id)
|
|
34
|
+
values('board',pg_temp.a(1),'message',pg_temp.a(300),null,pg_temp.a(200),pg_temp.a(201)),
|
|
35
|
+
('direct',pg_temp.a(9),'message',pg_temp.a(300),null,null,null),
|
|
36
|
+
('tasking',pg_temp.a(1),'task',null,pg_temp.a(405),pg_temp.a(200),pg_temp.a(201));
|
|
37
|
+
insert into agent_board.board_authority_ceilings(board_id) values(pg_temp.a(200));
|
|
38
|
+
insert into agent_board.thread_memberships(id,thread_id,agent_id,granted_by_agent_id)
|
|
39
|
+
values(pg_temp.a(600),pg_temp.a(100),pg_temp.a(2),pg_temp.a(1));
|
|
40
|
+
insert into agent_board.read_receipts(thread_id,agent_id) values(pg_temp.a(104),pg_temp.a(3));
|
|
41
|
+
create temp table captured(label text,id uuid);
|
|
42
|
+
grant select,insert on captured to authenticated;
|
|
43
|
+
|
|
44
|
+
-- Compare all policies, deriving FK/key column pairs from the constraint catalog.
|
|
45
|
+
create temp table policy_sites as
|
|
46
|
+
select p.oid,p.polname,c.relname,p.polroles,
|
|
47
|
+
agent_board_test.strip_sql(coalesce(pg_get_expr(p.polqual,p.polrelid),'')||' '||coalesce(pg_get_expr(p.polwithcheck,p.polrelid),'')) as expr
|
|
48
|
+
from pg_policy p join pg_class c on c.oid=p.polrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='agent_board';
|
|
49
|
+
with pairs as (
|
|
50
|
+
select distinct a.attname::text as fk,b.attname::text as key
|
|
51
|
+
from pg_constraint c join pg_class r on r.oid=c.conrelid join pg_namespace n on n.oid=r.relnamespace
|
|
52
|
+
cross join lateral unnest(c.conkey,c.confkey) k(src,dst)
|
|
53
|
+
join pg_attribute a on a.attrelid=c.conrelid and a.attnum=k.src
|
|
54
|
+
join pg_attribute b on b.attrelid=c.confrelid and b.attnum=k.dst
|
|
55
|
+
where c.contype='f' and n.nspname='agent_board'
|
|
56
|
+
), refs as (
|
|
57
|
+
select s.relname||'.'||s.polname||': '||m[1]||'.'||m[2]||' = '||m[1]||'.'||m[3] as site,trim(both '"' from m[2]) as lhs,trim(both '"' from m[3]) as rhs
|
|
58
|
+
from policy_sites s cross join lateral regexp_matches(s.expr,agent_board_test.re('('||agent_board_test.ident_any_frag()||')\s*\.\s*('||agent_board_test.ident_any_frag()||')\s*=\s*\1\s*\.\s*('||agent_board_test.ident_any_frag()||')'),'g')m
|
|
59
|
+
)
|
|
60
|
+
select is((select coalesce(string_agg(distinct site,', ' order by site),'') from refs where lhs=rhs or exists(select 1 from pairs where (lhs=fk and rhs=key) or (rhs=fk and lhs=key))),'','RLS-1 no policy self-comparison or same-alias foreign-key/key comparison; offenders named');
|
|
61
|
+
create temp table policy_functions as
|
|
62
|
+
select distinct s.relname,s.polname,f.oid,f.oid::regprocedure::text as signature,f.prosecdef
|
|
63
|
+
from policy_sites s join pg_depend d on d.classid='pg_policy'::regclass and d.objid=s.oid and d.refclassid='pg_proc'::regclass
|
|
64
|
+
join pg_proc f on f.oid=d.refobjid join pg_namespace n on n.oid=f.pronamespace
|
|
65
|
+
where n.nspname='agent_board' and exists(select 1 from unnest(s.polroles) r(role_id)
|
|
66
|
+
where case when role_id=0 then true else pg_has_role('authenticated',role_id,'USAGE') end);
|
|
67
|
+
select is((select coalesce(string_agg(relname||'.'||polname||': '||signature,', ' order by relname,polname,signature),'') from policy_functions where not has_function_privilege('authenticated',oid,'EXECUTE')),'','RLS-2 every authenticated policy function is executable; offenders named');
|
|
68
|
+
select cmp_ok((select count(*) from policy_sites),'>',0::bigint,'RLS-3 catalog policy class is nonempty');
|
|
69
|
+
select is((select coalesce(string_agg(distinct signature,', ' order by signature),'') from policy_functions where prosecdef and has_function_privilege('anon',oid,'EXECUTE')),'','RLS-4 authenticated policy definers are not anonymous-callable');
|
|
70
|
+
|
|
71
|
+
select pg_temp.claim(2);
|
|
72
|
+
set local role authenticated;
|
|
73
|
+
select lives_ok($$insert into captured select 'message-author',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-5 message-author SELECT executes');
|
|
74
|
+
reset role;
|
|
75
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='message-author'),array[pg_temp.a(100)]::uuid[],'RLS-6 message-author visible rows are scoped');
|
|
76
|
+
|
|
77
|
+
select pg_temp.claim(3);
|
|
78
|
+
set local role authenticated;
|
|
79
|
+
select lives_ok($$insert into captured select 'board-member',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-7 board-member SELECT executes');
|
|
80
|
+
reset role;
|
|
81
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='board-member'),array[pg_temp.a(100)]::uuid[],'RLS-8 board-member visible rows are scoped');
|
|
82
|
+
|
|
83
|
+
select pg_temp.claim(5);
|
|
84
|
+
set local role authenticated;
|
|
85
|
+
select lives_ok($$insert into captured select 'task-assignee',id from agent_board.threads where id in (pg_temp.a(101),pg_temp.a(103))$$,'RLS-9 task-assignee SELECT executes');
|
|
86
|
+
reset role;
|
|
87
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='task-assignee'),array[pg_temp.a(101)]::uuid[],'RLS-10 task-assignee visible rows are scoped');
|
|
88
|
+
|
|
89
|
+
select pg_temp.claim(6);
|
|
90
|
+
set local role authenticated;
|
|
91
|
+
select lives_ok($$insert into captured select 'parent-task-owner',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-11 parent-task-owner SELECT executes');
|
|
92
|
+
reset role;
|
|
93
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='parent-task-owner'),array[pg_temp.a(500)]::uuid[],'RLS-12 parent-task-owner visible rows are scoped');
|
|
94
|
+
|
|
95
|
+
select pg_temp.claim(7);
|
|
96
|
+
set local role authenticated;
|
|
97
|
+
select lives_ok($$insert into captured select 'child-task-owner',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-13 child-task-owner SELECT executes');
|
|
98
|
+
reset role;
|
|
99
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='child-task-owner'),array[pg_temp.a(500)]::uuid[],'RLS-14 child-task-owner visible rows are scoped');
|
|
100
|
+
|
|
101
|
+
select pg_temp.claim(8);
|
|
102
|
+
set local role authenticated;
|
|
103
|
+
select lives_ok($$insert into captured select 'delegator',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-15 delegator SELECT executes');
|
|
104
|
+
reset role;
|
|
105
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='delegator'),array[pg_temp.a(500)]::uuid[],'RLS-16 delegator visible rows are scoped');
|
|
106
|
+
|
|
107
|
+
select pg_temp.claim(9);
|
|
108
|
+
set local role authenticated;
|
|
109
|
+
select lives_ok($$insert into captured select 'delegate-recipient',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-17 delegate-recipient SELECT executes');
|
|
110
|
+
reset role;
|
|
111
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='delegate-recipient'),array[pg_temp.a(500)]::uuid[],'RLS-18 delegate-recipient visible rows are scoped');
|
|
112
|
+
|
|
113
|
+
select pg_temp.claim(1);
|
|
114
|
+
set local role authenticated;
|
|
115
|
+
select lives_ok($$insert into captured select 'thread-creator',id from agent_board.threads where id=pg_temp.a(106)$$,'RLS-19 thread-creator SELECT executes');
|
|
116
|
+
reset role;
|
|
117
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='thread-creator'),array[pg_temp.a(106)]::uuid[],'RLS-20 thread-creator visible rows are scoped');
|
|
118
|
+
|
|
119
|
+
select pg_temp.claim(4);
|
|
120
|
+
set local role authenticated;
|
|
121
|
+
select lives_ok($$insert into captured select 'outsider-threads',id from agent_board.threads where id between pg_temp.a(100) and pg_temp.a(105)$$,'RLS-21 outsider-threads SELECT executes');
|
|
122
|
+
reset role;
|
|
123
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-threads'),array[]::uuid[],'RLS-22 outsider-threads visible rows are scoped');
|
|
124
|
+
|
|
125
|
+
select pg_temp.claim(4);
|
|
126
|
+
set local role authenticated;
|
|
127
|
+
select lives_ok($$insert into captured select 'outsider-delegations',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-23 outsider-delegations SELECT executes');
|
|
128
|
+
reset role;
|
|
129
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-delegations'),array[]::uuid[],'RLS-24 outsider-delegations visible rows are scoped');
|
|
130
|
+
|
|
131
|
+
select pg_temp.claim(3);
|
|
132
|
+
set local role authenticated;
|
|
133
|
+
select lives_ok($$insert into captured select 'member-ceiling',board_id from agent_board.board_authority_ceilings where board_id=pg_temp.a(200)$$,'RLS-25 member-ceiling SELECT executes');
|
|
134
|
+
reset role;
|
|
135
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-ceiling'),array[pg_temp.a(200)]::uuid[],'RLS-26 member-ceiling visible rows are scoped');
|
|
136
|
+
|
|
137
|
+
select pg_temp.claim(4);
|
|
138
|
+
set local role authenticated;
|
|
139
|
+
select lives_ok($$insert into captured select 'outsider-ceiling',board_id from agent_board.board_authority_ceilings where board_id=pg_temp.a(200)$$,'RLS-27 outsider-ceiling SELECT executes');
|
|
140
|
+
reset role;
|
|
141
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-ceiling'),array[]::uuid[],'RLS-28 outsider-ceiling visible rows are scoped');
|
|
142
|
+
|
|
143
|
+
select pg_temp.claim(3);
|
|
144
|
+
set local role authenticated;
|
|
145
|
+
select lives_ok($$insert into captured select 'member-thread-acl',id from agent_board.thread_memberships where id=pg_temp.a(600)$$,'RLS-29 member-thread-acl SELECT executes');
|
|
146
|
+
reset role;
|
|
147
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-thread-acl'),array[pg_temp.a(600)]::uuid[],'RLS-30 member-thread-acl visible rows are scoped');
|
|
148
|
+
|
|
149
|
+
select pg_temp.claim(4);
|
|
150
|
+
set local role authenticated;
|
|
151
|
+
select lives_ok($$insert into captured select 'outsider-thread-acl',id from agent_board.thread_memberships where id=pg_temp.a(600)$$,'RLS-31 outsider-thread-acl SELECT executes');
|
|
152
|
+
reset role;
|
|
153
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-thread-acl'),array[]::uuid[],'RLS-32 outsider-thread-acl visible rows are scoped');
|
|
154
|
+
|
|
155
|
+
select pg_temp.claim(3);
|
|
156
|
+
set local role authenticated;
|
|
157
|
+
select lives_ok($$insert into captured select 'receipt-reader',id from agent_board.threads where id=pg_temp.a(104)$$,'RLS-33 receipt-reader SELECT executes');
|
|
158
|
+
reset role;
|
|
159
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='receipt-reader'),array[pg_temp.a(104)]::uuid[],'RLS-34 receipt-reader visible rows are scoped');
|
|
160
|
+
|
|
161
|
+
select pg_temp.claim(3);
|
|
162
|
+
set local role authenticated;
|
|
163
|
+
select lives_ok($$insert into captured select 'member-unrelated-delegation',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-35 member-unrelated-delegation SELECT executes');
|
|
164
|
+
reset role;
|
|
165
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-unrelated-delegation'),array[]::uuid[],'RLS-36 member-unrelated-delegation visible rows are scoped');
|
|
166
|
+
|
|
167
|
+
select pg_temp.claim(3);
|
|
168
|
+
set local role authenticated;
|
|
169
|
+
select lives_ok($$insert into captured select 'board-task-delivery',id from agent_board.threads where id in (pg_temp.a(105),pg_temp.a(103))$$,'RLS-37 board-task-delivery SELECT executes');
|
|
170
|
+
reset role;
|
|
171
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='board-task-delivery'),array[pg_temp.a(105)]::uuid[],'RLS-38 board-task-delivery visible rows are scoped');
|
|
172
|
+
|
|
173
|
+
select pg_temp.claim(9);
|
|
174
|
+
set local role authenticated;
|
|
175
|
+
select lives_ok($$insert into captured select 'direct-message-recipient',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-39 direct-message-recipient SELECT executes');
|
|
176
|
+
reset role;
|
|
177
|
+
select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='direct-message-recipient'),array[pg_temp.a(100)]::uuid[],'RLS-40 direct-message-recipient visible rows are scoped');
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
-- Catalog provenance is part of the read contract: grants must not erase the ruling.
|
|
181
|
+
select ok(coalesce(
|
|
182
|
+
obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%message-board#291%'
|
|
183
|
+
and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%REVERSING A RULING%'
|
|
184
|
+
and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%restricted%is UNREACHABLE%'
|
|
185
|
+
and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%Callable by authenticated for its RLS policies and by service_role.%',
|
|
186
|
+
false), 'RLS-41 thread access comment preserves the ratified claims ruling and dormant ACL');
|
|
187
|
+
|
|
188
|
+
select * from finish();
|
|
189
|
+
rollback;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
create extension if not exists pgtap with schema extensions;
|
|
2
|
+
set search_path = extensions, public, agent_board;
|
|
3
|
+
begin;
|
|
4
|
+
select plan(33);
|
|
5
|
+
create function pg_temp.a(n integer) returns uuid language sql immutable as $$
|
|
6
|
+
select ('68000000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid
|
|
7
|
+
$$;
|
|
8
|
+
create function pg_temp.claim(n integer) returns void language plpgsql as $$
|
|
9
|
+
begin
|
|
10
|
+
perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',pg_temp.a(n))::text,true);
|
|
11
|
+
end $$;
|
|
12
|
+
insert into auth.users(id, instance_id, aud, role, email)
|
|
13
|
+
select pg_temp.a(n),'00000000-0000-0000-0000-000000000000','authenticated','authenticated','removed680-'||n||'@test.invalid'
|
|
14
|
+
from generate_series(1,9) n;
|
|
15
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id)
|
|
16
|
+
select pg_temp.a(n),'agent','removed680-'||n,'Removed '||n,'active',pg_temp.a(n)
|
|
17
|
+
from generate_series(1,9) n;
|
|
18
|
+
insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility)
|
|
19
|
+
values (pg_temp.a(100),'removed680-open','Open',pg_temp.a(1),'open'),
|
|
20
|
+
(pg_temp.a(101),'removed680-link','Link',pg_temp.a(1),'invite_only'),
|
|
21
|
+
(pg_temp.a(102),'removed680-private','Private',pg_temp.a(1),'private');
|
|
22
|
+
insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id)
|
|
23
|
+
select pg_temp.a(n),pg_temp.a(1),'joined','owner',pg_temp.a(1) from generate_series(100,102) n;
|
|
24
|
+
insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id,left_at,metadata)
|
|
25
|
+
values (pg_temp.a(100),pg_temp.a(2),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
|
|
26
|
+
(pg_temp.a(101),pg_temp.a(2),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
|
|
27
|
+
(pg_temp.a(100),pg_temp.a(3),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
|
|
28
|
+
(pg_temp.a(100),pg_temp.a(3),'left','member',pg_temp.a(1),now(),'{"left":true}'),
|
|
29
|
+
(pg_temp.a(100),pg_temp.a(5),'left','member',pg_temp.a(5),now(),'{}'),
|
|
30
|
+
(pg_temp.a(100),pg_temp.a(6),'requested','member',pg_temp.a(6),null,'{}'),
|
|
31
|
+
(pg_temp.a(100),pg_temp.a(7),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
|
|
32
|
+
(pg_temp.a(100),pg_temp.a(7),'joined','member',pg_temp.a(7),null,'{"active":true}'),
|
|
33
|
+
(pg_temp.a(100),pg_temp.a(8),'subscribed','member',pg_temp.a(8),null,'{"active":true}');
|
|
34
|
+
update agent_board.board_memberships set created_at=now()-interval '1 day'
|
|
35
|
+
where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3) and membership_state='removed';
|
|
36
|
+
select pg_temp.claim(1);
|
|
37
|
+
create temp table live_link as select * from agent_board.create_invite_link(pg_temp.a(101),'member','joined',null,null,pg_temp.a(1));
|
|
38
|
+
|
|
39
|
+
create temp table request_sites as
|
|
40
|
+
select p.oid,p.proname::text as name,agent_board_test.stripped_body(p.oid) as body,
|
|
41
|
+
agent_board_test.stripped_body(p.oid,true) as literals
|
|
42
|
+
from pg_proc p join pg_namespace n on n.oid=p.pronamespace
|
|
43
|
+
where n.nspname='agent_board' and has_function_privilege('authenticated',p.oid,'EXECUTE')
|
|
44
|
+
and agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.re(
|
|
45
|
+
agent_board_test.call_frag('_upsert_board_membership_state') || '[^,;]*,[^,;]*,\s*''requested''\s*,');
|
|
46
|
+
select is((select string_agg(name,',' order by name) from request_sites),'request_join_board','REM-1 literal-request sink callers enumerated from catalog');
|
|
47
|
+
select is((select coalesce(string_agg(name,','),'') from request_sites where body !~ agent_board_test.row_lock_re('board_memberships')),'','REM-2 every request writer locks membership rows; offenders named');
|
|
48
|
+
select is((select coalesce(string_agg(name,','),'') from request_sites
|
|
49
|
+
where regexp_instr(body,agent_board_test.row_lock_re('board_memberships'))=0
|
|
50
|
+
or regexp_instr(body,agent_board_test.row_lock_re('board_memberships')) >= regexp_instr(body,agent_board_test.call_re('_upsert_board_membership_state'))
|
|
51
|
+
or regexp_instr(body,agent_board_test.row_lock_re('board_memberships')) >= regexp_instr(body,agent_board_test.re(agent_board_test.ci_re('if') || '\s+' || agent_board_test.ci_re('exists') || '\s*\('))
|
|
52
|
+
or literals !~ 'board join request refused for removed standing'),
|
|
53
|
+
'','REM-3 every request writer checks removed standing before its sink');
|
|
54
|
+
select is((select coalesce(string_agg(name,','),'') from request_sites where has_function_privilege('anon',oid,'EXECUTE') or not has_function_privilege('service_role',oid,'EXECUTE') or obj_description(oid,'pg_proc') is null),'','REM-4 request writer ACL and comment');
|
|
55
|
+
|
|
56
|
+
select pg_temp.claim(2);
|
|
57
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(2),'{"overwrite":true}')$$,'42501',null,'REM-5 removed open-board request refused');
|
|
58
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-6 eviction persists after request');
|
|
59
|
+
select is((select count(*)::integer from agent_board.board_membership_events where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),0,'REM-7 refused request emits no membership event');
|
|
60
|
+
select throws_ok($$select agent_board.join_board(pg_temp.a(100),pg_temp.a(2))$$,'P0002',null,'REM-8 request cannot unlock open self-join');
|
|
61
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-9 self-join leaves eviction intact');
|
|
62
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(101),pg_temp.a(2))$$,'42501',null,'REM-10 removed invite-only request refused');
|
|
63
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(2)),'removed','REM-11 invite-only eviction persists');
|
|
64
|
+
select throws_ok($$select agent_board.redeem_invite_link((select code from live_link))$$,'42501',null,'REM-12 request cannot unlock live-link redemption');
|
|
65
|
+
select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(2)),'removed','REM-13 live link leaves eviction intact');
|
|
66
|
+
select is((select uses_count from agent_board.board_invite_tokens where id=(select invite_id from live_link)),0,'REM-14 refused redemption consumes no use');
|
|
67
|
+
|
|
68
|
+
select pg_temp.claim(3);
|
|
69
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(3),'{"overwrite":true}')$$,'42501',null,'REM-15 removed plus left standing refused');
|
|
70
|
+
select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3)),'left,removed','REM-16 whole row set preserved');
|
|
71
|
+
select is((select metadata from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3) and membership_state='removed'),'{"eviction":true}'::jsonb,'REM-17 denied request preserves eviction metadata');
|
|
72
|
+
select pg_temp.claim(4);
|
|
73
|
+
select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(4))$$,'REM-18 first request succeeds');
|
|
74
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(4)),'requested','REM-19 first request stored');
|
|
75
|
+
select pg_temp.claim(5);
|
|
76
|
+
select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(5))$$,'REM-20 voluntary leaver can request');
|
|
77
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(5)),'requested','REM-21 voluntary request stored');
|
|
78
|
+
select pg_temp.claim(6);
|
|
79
|
+
select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(6))$$,'REM-22 repeated pending request succeeds');
|
|
80
|
+
select is((select count(*)::integer from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(6) and membership_state='requested'),1,'REM-23 repeat preserves one pending row');
|
|
81
|
+
select pg_temp.claim(7);
|
|
82
|
+
select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(7),'{"overwrite":true}')$$,'REM-24 active plus removed remains a no-op');
|
|
83
|
+
select is((select metadata from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(7) and membership_state='joined'),'{"active":true}'::jsonb,'REM-25 active row remains unchanged');
|
|
84
|
+
select pg_temp.claim(8);
|
|
85
|
+
select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(8))$$,'REM-26 subscribed request remains a no-op');
|
|
86
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(8)),'subscribed','REM-27 subscription preserved');
|
|
87
|
+
select set_config('request.jwt.claims','{"role":"service_role"}',true);
|
|
88
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(2))$$,'42501',null,'REM-28 service request cannot erase removal');
|
|
89
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-29 service refusal preserves eviction');
|
|
90
|
+
select pg_temp.claim(9);
|
|
91
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(102),pg_temp.a(9))$$,'P0002',null,'REM-30 private board request still hidden');
|
|
92
|
+
select is((select count(*)::integer from agent_board.board_memberships where board_id=pg_temp.a(102) and agent_id=pg_temp.a(9)),0,'REM-31 hidden request writes nothing');
|
|
93
|
+
select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(4))$$,'42501',null,'REM-32 request remains self-only');
|
|
94
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(4)),'requested','REM-33 actor mismatch preserves target');
|
|
95
|
+
select * from finish();
|
|
96
|
+
rollback;
|