@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -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/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -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_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -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_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- 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 +45 -31
- 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_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- 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 +27 -11
- 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/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -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/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -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/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -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
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
-- MBCONSENT-A pgTAP: the consent SUBSTRATE only (interface freeze IF-0-MBCONSENT-1).
|
|
2
|
+
--
|
|
3
|
+
-- Scope is deliberately narrow. This suite asserts the ledger, the anti-forge rules, the
|
|
4
|
+
-- privilege posture, the two predicates, and the backfill. It drives NO membership route,
|
|
5
|
+
-- because this migration changes none -- emission and the force-join closure are MBCONSENT-B.
|
|
6
|
+
--
|
|
7
|
+
-- Methodology notes that are load-bearing:
|
|
8
|
+
-- * The CHECK constraint is verified with the BEFORE INSERT trigger DISABLED. With the
|
|
9
|
+
-- trigger enabled the constraint cannot be reached, so a null-permissive CHECK would pass
|
|
10
|
+
-- a naive test -- which is exactly the state a review found in an earlier draft.
|
|
11
|
+
-- * Privilege assertions use has_table_privilege / has_function_privilege, which resolve
|
|
12
|
+
-- ROLE MEMBERSHIP. A query filtering information_schema on grantee = 'authenticated' sees
|
|
13
|
+
-- only DIRECT grants and reads clean when a privilege is held through another role.
|
|
14
|
+
-- * Event ordering is asserted through seq, never created_at: now() is
|
|
15
|
+
-- transaction_timestamp() and is constant within a transaction.
|
|
16
|
+
create extension if not exists pgtap with schema extensions;
|
|
17
|
+
set search_path = extensions, public, agent_board;
|
|
18
|
+
|
|
19
|
+
begin;
|
|
20
|
+
|
|
21
|
+
select plan(91);
|
|
22
|
+
|
|
23
|
+
-- ---------------------------------------------------------------------------
|
|
24
|
+
-- Fixtures. Agents 1..6; board 1 owned by agent 1.
|
|
25
|
+
-- ---------------------------------------------------------------------------
|
|
26
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
27
|
+
select ('c0000000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
|
|
28
|
+
'agent', 'agent', 'substrate-agent-' || n, 'Substrate Agent ' || n, 'active'
|
|
29
|
+
from generate_series(1, 7) n;
|
|
30
|
+
|
|
31
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
32
|
+
values ('cb000000-0000-4000-8000-000000000001', 'substrate-b1', 'Substrate B1',
|
|
33
|
+
'c0000000-0000-4000-8000-000000000001', 'private', 'active');
|
|
34
|
+
|
|
35
|
+
-- ===========================================================================
|
|
36
|
+
-- 1. Table shape
|
|
37
|
+
-- ===========================================================================
|
|
38
|
+
select has_table('agent_board', 'board_membership_events', 'substrate: ledger table exists');
|
|
39
|
+
select has_column('agent_board', 'board_membership_events', 'seq', 'substrate: seq exists');
|
|
40
|
+
select col_is_pk('agent_board', 'board_membership_events', 'seq',
|
|
41
|
+
'substrate: seq is the PRIMARY KEY -- the total order, not created_at');
|
|
42
|
+
|
|
43
|
+
select is(
|
|
44
|
+
(select a.attidentity from pg_attribute a
|
|
45
|
+
where a.attrelid = 'agent_board.board_membership_events'::regclass and a.attname = 'seq'),
|
|
46
|
+
'a'::"char",
|
|
47
|
+
'substrate: seq is GENERATED ALWAYS AS IDENTITY (callers cannot supply it)');
|
|
48
|
+
|
|
49
|
+
select is(
|
|
50
|
+
(select count(*)::int from pg_constraint c
|
|
51
|
+
where c.conrelid = 'agent_board.board_membership_events'::regclass
|
|
52
|
+
and c.contype = 'f' and c.confdeltype = 'c'),
|
|
53
|
+
2,
|
|
54
|
+
'substrate: board_id and agent_id both cascade on delete');
|
|
55
|
+
|
|
56
|
+
-- ===========================================================================
|
|
57
|
+
-- 2. Anti-forge: CHECK constraint IN ISOLATION, then the trigger
|
|
58
|
+
-- ===========================================================================
|
|
59
|
+
-- The trigger masks the constraint, so it is disabled here on purpose. Without this the
|
|
60
|
+
-- next four assertions would pass against a CHECK that accepts a NULL actor.
|
|
61
|
+
alter table agent_board.board_membership_events
|
|
62
|
+
disable trigger trg_board_membership_events_provenance_guard;
|
|
63
|
+
|
|
64
|
+
select throws_ok($$
|
|
65
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
66
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
67
|
+
'consent_self_join', 'joined', 'target-authored', null)
|
|
68
|
+
$$, '23514', null,
|
|
69
|
+
'CHECK alone: target-authored with a NULL actor is refused');
|
|
70
|
+
|
|
71
|
+
select throws_ok($$
|
|
72
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
73
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
74
|
+
'consent_self_join', 'joined', 'target-authored', 'c0000000-0000-4000-8000-000000000003')
|
|
75
|
+
$$, '23514', null,
|
|
76
|
+
'CHECK alone: target-authored by a THIRD PARTY is refused');
|
|
77
|
+
|
|
78
|
+
select throws_ok($$
|
|
79
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
80
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
81
|
+
'operator_designation', 'joined', 'operator-designated', 'c0000000-0000-4000-8000-000000000003')
|
|
82
|
+
$$, '23514', null,
|
|
83
|
+
'CHECK alone: operator-designated with a NON-NULL actor is refused');
|
|
84
|
+
|
|
85
|
+
select throws_ok($$
|
|
86
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
87
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
88
|
+
'invite_offer', 'invited', 'board-authored', 'c0000000-0000-4000-8000-000000000002')
|
|
89
|
+
$$, '23514', null,
|
|
90
|
+
'CHECK alone: board-authored by the SUBJECT is refused');
|
|
91
|
+
|
|
92
|
+
alter table agent_board.board_membership_events
|
|
93
|
+
enable trigger trg_board_membership_events_provenance_guard;
|
|
94
|
+
|
|
95
|
+
-- Now the MIRROR IMAGE: isolate the TRIGGER by dropping the CHECK. Asserting a bare 23514
|
|
96
|
+
-- with the constraint in place would be vacuous -- the CHECK rejects these same rows, so a
|
|
97
|
+
-- trigger whose body was replaced with `return new` would still pass. Each assertion below
|
|
98
|
+
-- pins the trigger's OWN message, which only the trigger can produce.
|
|
99
|
+
-- Capture the SHIPPED definition before dropping it, so the restore below cannot drift from
|
|
100
|
+
-- the migration. Hand-copying the constraint here would mean a later change to the migration
|
|
101
|
+
-- leaves this suite asserting a stale definition -- the test would still pass, against the
|
|
102
|
+
-- wrong constraint.
|
|
103
|
+
create temporary table _saved_provenance_check on commit drop as
|
|
104
|
+
select pg_get_constraintdef(oid) as def
|
|
105
|
+
from pg_constraint where conname = 'board_membership_events_provenance_actor';
|
|
106
|
+
|
|
107
|
+
select is((select count(*)::int from _saved_provenance_check), 1,
|
|
108
|
+
'guard: the shipped provenance CHECK was captured before dropping it');
|
|
109
|
+
|
|
110
|
+
alter table agent_board.board_membership_events
|
|
111
|
+
drop constraint board_membership_events_provenance_actor;
|
|
112
|
+
|
|
113
|
+
select throws_ok($$
|
|
114
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
115
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
116
|
+
'consent_self_join', 'joined', 'target-authored', null)
|
|
117
|
+
$$, 'substrate CHECK refuses target-authored actor <> subject',
|
|
118
|
+
'TRIGGER alone (CHECK dropped) refuses target-authored with a NULL actor');
|
|
119
|
+
|
|
120
|
+
select throws_ok($$
|
|
121
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
122
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
123
|
+
'consent_self_join', 'joined', 'target-authored', 'c0000000-0000-4000-8000-000000000003')
|
|
124
|
+
$$, 'substrate CHECK refuses target-authored actor <> subject',
|
|
125
|
+
'TRIGGER alone refuses a third-party target-authored forge');
|
|
126
|
+
|
|
127
|
+
select throws_ok($$
|
|
128
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
129
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
130
|
+
'operator_designation', 'joined', 'operator-designated', 'c0000000-0000-4000-8000-000000000003')
|
|
131
|
+
$$, 'substrate CHECK refuses operator-designated non-null actor',
|
|
132
|
+
'TRIGGER alone refuses operator-designated with a non-null actor');
|
|
133
|
+
|
|
134
|
+
select throws_ok($$
|
|
135
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
136
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
137
|
+
'invite_offer', 'invited', 'board-authored', 'c0000000-0000-4000-8000-000000000002')
|
|
138
|
+
$$, 'substrate CHECK refuses board-authored actor = subject',
|
|
139
|
+
'TRIGGER alone refuses board-authored by the subject');
|
|
140
|
+
|
|
141
|
+
-- Restore the captured definition verbatim rather than a hand-written copy.
|
|
142
|
+
do $restore$
|
|
143
|
+
declare
|
|
144
|
+
v_def text;
|
|
145
|
+
begin
|
|
146
|
+
select def into strict v_def from _saved_provenance_check;
|
|
147
|
+
execute format(
|
|
148
|
+
'alter table agent_board.board_membership_events add constraint %I %s',
|
|
149
|
+
'board_membership_events_provenance_actor', v_def);
|
|
150
|
+
end
|
|
151
|
+
$restore$;
|
|
152
|
+
|
|
153
|
+
select is(
|
|
154
|
+
(select pg_get_constraintdef(oid) from pg_constraint
|
|
155
|
+
where conname = 'board_membership_events_provenance_actor'),
|
|
156
|
+
(select def from _saved_provenance_check),
|
|
157
|
+
'guard: the restored CHECK is identical to the shipped one');
|
|
158
|
+
|
|
159
|
+
-- ...and with BOTH restored, the row is still refused (defense in depth intact).
|
|
160
|
+
select throws_ok($$
|
|
161
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
162
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
163
|
+
'consent_self_join', 'joined', 'target-authored', null)
|
|
164
|
+
$$, 'substrate CHECK refuses target-authored actor <> subject',
|
|
165
|
+
'CHECK + trigger together still refuse the null-actor forge');
|
|
166
|
+
|
|
167
|
+
-- The three LEGAL shapes must all be writable.
|
|
168
|
+
select lives_ok($$
|
|
169
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
170
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
171
|
+
'consent_self_join', 'joined', 'target-authored', 'c0000000-0000-4000-8000-000000000002')
|
|
172
|
+
$$, 'legal: target-authored by the subject');
|
|
173
|
+
|
|
174
|
+
select lives_ok($$
|
|
175
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
176
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003',
|
|
177
|
+
'operator_designation', 'joined', 'operator-designated', null)
|
|
178
|
+
$$, 'legal: operator-designated with a null actor');
|
|
179
|
+
|
|
180
|
+
select lives_ok($$
|
|
181
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
182
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004',
|
|
183
|
+
'invite_offer', 'invited', 'board-authored', 'c0000000-0000-4000-8000-000000000001')
|
|
184
|
+
$$, 'legal: board-authored by a third party');
|
|
185
|
+
|
|
186
|
+
-- ===========================================================================
|
|
187
|
+
-- 3. Privilege posture
|
|
188
|
+
-- ===========================================================================
|
|
189
|
+
select ok(
|
|
190
|
+
(select relrowsecurity from pg_class
|
|
191
|
+
where oid = 'agent_board.board_membership_events'::regclass),
|
|
192
|
+
'substrate: RLS is enabled');
|
|
193
|
+
|
|
194
|
+
select is(has_table_privilege('authenticated', 'agent_board.board_membership_events', 'SELECT'),
|
|
195
|
+
true, 'substrate: authenticated may SELECT (RLS narrows it to own rows)');
|
|
196
|
+
|
|
197
|
+
select is(has_table_privilege('authenticated', 'agent_board.board_membership_events', 'INSERT'),
|
|
198
|
+
false, 'substrate: authenticated holds no INSERT by ANY route (incl. role membership)');
|
|
199
|
+
select is(has_table_privilege('authenticated', 'agent_board.board_membership_events', 'UPDATE'),
|
|
200
|
+
false, 'substrate: authenticated holds no UPDATE by any route');
|
|
201
|
+
select is(has_table_privilege('authenticated', 'agent_board.board_membership_events', 'DELETE'),
|
|
202
|
+
false, 'substrate: authenticated holds no DELETE by any route');
|
|
203
|
+
|
|
204
|
+
-- The frozen contract forbids an authenticated write POLICY, not merely the privilege. With RLS
|
|
205
|
+
-- on and no permissive policy, the default is DENY; a permissive policy backed by a raising
|
|
206
|
+
-- function would satisfy every privilege assertion above while violating the substrate shape.
|
|
207
|
+
select is(
|
|
208
|
+
(select count(*)::int from pg_policy
|
|
209
|
+
where polrelid = 'agent_board.board_membership_events'::regclass
|
|
210
|
+
and polcmd in ('a', 'w', 'd')
|
|
211
|
+
and 'authenticated' = any (select rolname from pg_roles where oid = any (polroles))),
|
|
212
|
+
0,
|
|
213
|
+
'substrate: NO authenticated INSERT/UPDATE/DELETE policy exists at all');
|
|
214
|
+
select is(has_table_privilege('anon', 'agent_board.board_membership_events', 'SELECT'),
|
|
215
|
+
false, 'substrate: anon cannot read the ledger at all');
|
|
216
|
+
|
|
217
|
+
select is(has_function_privilege('authenticated', 'agent_board._membership_consent_present(uuid,uuid)', 'EXECUTE'),
|
|
218
|
+
false, 'substrate: authenticated cannot read an arbitrary subject consent state');
|
|
219
|
+
select is(has_function_privilege('authenticated', 'agent_board._membership_consent_kind(uuid,uuid)', 'EXECUTE'),
|
|
220
|
+
false, 'substrate: authenticated cannot read an arbitrary subject consent provenance');
|
|
221
|
+
|
|
222
|
+
-- The SELECT policy must be subject-scoped, not permissive-true.
|
|
223
|
+
-- The schema qualifier is OPTIONAL in the rendering: pg_get_expr omits it when the schema is
|
|
224
|
+
-- on search_path, which this suite puts there. Pinning one spelling would fail against a
|
|
225
|
+
-- CORRECT policy -- a rendering artifact, not a policy defect.
|
|
226
|
+
select matches(
|
|
227
|
+
(select pg_get_expr(polqual, polrelid) from pg_policy
|
|
228
|
+
where polrelid = 'agent_board.board_membership_events'::regclass
|
|
229
|
+
and polname = 'agent_board_authenticated_select_board_membership_events'),
|
|
230
|
+
'^\(agent_id = (agent_board\.)?current_agent_id\(\)\)$',
|
|
231
|
+
'substrate: the authenticated SELECT policy is subject-scoped');
|
|
232
|
+
|
|
233
|
+
-- Direct-table RLS probe -- the one case that requires SET LOCAL ROLE.
|
|
234
|
+
set local role authenticated;
|
|
235
|
+
select throws_ok($$
|
|
236
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
237
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
238
|
+
'consent_self_join', 'joined', 'target-authored', 'c0000000-0000-4000-8000-000000000005')
|
|
239
|
+
$$, '42501', 'permission denied for table board_membership_events',
|
|
240
|
+
'substrate: authenticated direct INSERT is refused by PRIVILEGE, before RLS');
|
|
241
|
+
reset role;
|
|
242
|
+
|
|
243
|
+
-- ===========================================================================
|
|
244
|
+
-- 4. Predicate semantics -- _present and _kind must agree on every case
|
|
245
|
+
-- ===========================================================================
|
|
246
|
+
-- agent 2: target-authored joined (above) -> consent
|
|
247
|
+
select is(agent_board._membership_consent_present(
|
|
248
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
249
|
+
true, 'predicate: a target-authored placement is consent');
|
|
250
|
+
select is(agent_board._membership_consent_kind(
|
|
251
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
252
|
+
'target', 'predicate: ...and _kind agrees');
|
|
253
|
+
|
|
254
|
+
-- agent 3: operator-designated JOINED -> consent (Assumption 6 operator carve-out)
|
|
255
|
+
select is(agent_board._membership_consent_present(
|
|
256
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
257
|
+
true, 'predicate: an operator PLACEMENT is consent');
|
|
258
|
+
select is(agent_board._membership_consent_kind(
|
|
259
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
260
|
+
'operator', 'predicate: ...and _kind agrees');
|
|
261
|
+
|
|
262
|
+
-- agent 4: board-authored invited -> NEUTRAL
|
|
263
|
+
select is(agent_board._membership_consent_present(
|
|
264
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
265
|
+
false, 'predicate: a board-authored offer is not consent');
|
|
266
|
+
select is(agent_board._membership_consent_kind(
|
|
267
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
268
|
+
null, 'predicate: ...and _kind agrees');
|
|
269
|
+
|
|
270
|
+
-- agent 5: a PENDING operator OFFER is not consent. This is the case an earlier draft got
|
|
271
|
+
-- wrong: any operator-designated non-removal event counted, so an invitation the subject had
|
|
272
|
+
-- never seen read as consent.
|
|
273
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
274
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
275
|
+
'invite_offer', 'invited', 'operator-designated', null);
|
|
276
|
+
select is(agent_board._membership_consent_present(
|
|
277
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
278
|
+
false, 'predicate: a PENDING operator invitation is NOT consent');
|
|
279
|
+
select is(agent_board._membership_consent_kind(
|
|
280
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
281
|
+
null, 'predicate: ...and _kind agrees on the pending offer');
|
|
282
|
+
|
|
283
|
+
-- The operator boundary is joined|subscribed EXACTLY. Testing only `invited` would leave an
|
|
284
|
+
-- implementation that wrongly accepts `requested`, or wrongly rejects `subscribed`, green.
|
|
285
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
286
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
287
|
+
'join_request', 'requested', 'operator-designated', null);
|
|
288
|
+
select is(agent_board._membership_consent_present(
|
|
289
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
290
|
+
false, 'boundary: operator-designated REQUESTED is not consent');
|
|
291
|
+
select is(agent_board._membership_consent_kind(
|
|
292
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
293
|
+
null, 'boundary: ...and _kind agrees');
|
|
294
|
+
|
|
295
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
296
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
297
|
+
'operator_designation', 'subscribed', 'operator-designated', null);
|
|
298
|
+
select is(agent_board._membership_consent_present(
|
|
299
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
300
|
+
true, 'boundary: operator-designated SUBSCRIBED IS consent');
|
|
301
|
+
select is(agent_board._membership_consent_kind(
|
|
302
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
303
|
+
'operator', 'boundary: ...and _kind agrees');
|
|
304
|
+
|
|
305
|
+
-- target-authored REQUESTED is a grant. Without this, an implementation restricting target
|
|
306
|
+
-- consent to joined|subscribed stays green -- and that would break the join-request approval
|
|
307
|
+
-- path, where the subject's own request IS the consenting act.
|
|
308
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
309
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004',
|
|
310
|
+
'consent_join_request', 'requested', 'target-authored', 'c0000000-0000-4000-8000-000000000004');
|
|
311
|
+
select is(agent_board._membership_consent_present(
|
|
312
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
313
|
+
true, 'predicate: a target-authored REQUEST is consent (the approval path)');
|
|
314
|
+
select is(agent_board._membership_consent_kind(
|
|
315
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
316
|
+
'target', 'predicate: ...and _kind agrees');
|
|
317
|
+
|
|
318
|
+
-- A board-authored NEUTRAL event after a valid grant must PRESERVE the grant. An
|
|
319
|
+
-- implementation treating neutral events as revocations stays green without this.
|
|
320
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
321
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004',
|
|
322
|
+
'invite_offer', 'invited', 'board-authored', 'c0000000-0000-4000-8000-000000000001');
|
|
323
|
+
select is(agent_board._membership_consent_present(
|
|
324
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
325
|
+
true, 'predicate: a board-authored neutral event PRESERVES an existing grant');
|
|
326
|
+
-- _kind must be asserted HERE, immediately after the neutral event. Checking only _present
|
|
327
|
+
-- leaves a _kind that includes board-authored rows in its candidate set returning null while
|
|
328
|
+
-- _present says true -- the twins disagreeing, with the suite green.
|
|
329
|
+
select is(agent_board._membership_consent_kind(
|
|
330
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
331
|
+
'target', 'predicate: _kind also preserves the grant across a neutral event');
|
|
332
|
+
|
|
333
|
+
-- A pending OPERATOR event must likewise preserve an existing grant, not just read neutral on
|
|
334
|
+
-- an empty history.
|
|
335
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
336
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004',
|
|
337
|
+
'invite_offer', 'invited', 'operator-designated', null);
|
|
338
|
+
select is(agent_board._membership_consent_present(
|
|
339
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
340
|
+
true, 'predicate: a pending OPERATOR offer preserves an existing grant');
|
|
341
|
+
select is(agent_board._membership_consent_kind(
|
|
342
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
343
|
+
'target', 'predicate: ...and _kind agrees');
|
|
344
|
+
|
|
345
|
+
-- removal-first must cover REMOVED, not only LEFT.
|
|
346
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
347
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004',
|
|
348
|
+
'operator_removal', 'removed', 'operator-designated', null);
|
|
349
|
+
select is(agent_board._membership_consent_present(
|
|
350
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
351
|
+
false, 'predicate: REMOVED is removal-first, not just LEFT');
|
|
352
|
+
select is(agent_board._membership_consent_kind(
|
|
353
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000004'),
|
|
354
|
+
null, 'predicate: ...and _kind agrees on removed');
|
|
355
|
+
|
|
356
|
+
-- LEDGER-ONLY, the contract's central claim: consent is read from board_membership_events and
|
|
357
|
+
-- NEVER from board_memberships.membership_state. Agent 6 has NO membership row, so an
|
|
358
|
+
-- implementation that fell back to membership_state on an empty ledger would still read false
|
|
359
|
+
-- and pass. Agent 3 below has an ACTIVE row and a ledger that does not grant -- that is the
|
|
360
|
+
-- case which discriminates.
|
|
361
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
362
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
363
|
+
'member', 'joined', 'c0000000-0000-4000-8000-000000000001');
|
|
364
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
365
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005',
|
|
366
|
+
'self_withdraw', 'left', 'target-authored', 'c0000000-0000-4000-8000-000000000005');
|
|
367
|
+
select is(agent_board._membership_consent_present(
|
|
368
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
369
|
+
false,
|
|
370
|
+
'LEDGER-ONLY: an ACTIVE membership row does not grant consent when the ledger says left');
|
|
371
|
+
select is(agent_board._membership_consent_kind(
|
|
372
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000005'),
|
|
373
|
+
null, 'LEDGER-ONLY: ...and _kind agrees');
|
|
374
|
+
|
|
375
|
+
-- board-authored is neutral at EVERY non-removal state, not only `invited`.
|
|
376
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
377
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003',
|
|
378
|
+
'join_request', 'requested', 'board-authored', 'c0000000-0000-4000-8000-000000000001');
|
|
379
|
+
select is(agent_board._membership_consent_present(
|
|
380
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
381
|
+
true, 'board-authored REQUESTED is neutral: it neither grants nor revokes');
|
|
382
|
+
|
|
383
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
384
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003',
|
|
385
|
+
'resolution', 'joined', 'board-authored', 'c0000000-0000-4000-8000-000000000001');
|
|
386
|
+
select is(agent_board._membership_consent_present(
|
|
387
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
388
|
+
true, 'board-authored JOINED is neutral -- an owner resolution is not the subject consenting');
|
|
389
|
+
|
|
390
|
+
-- ...but a board-authored REMOVAL does revoke: removal-first applies to every provenance.
|
|
391
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
392
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003',
|
|
393
|
+
'eviction', 'removed', 'board-authored', 'c0000000-0000-4000-8000-000000000001');
|
|
394
|
+
select is(agent_board._membership_consent_present(
|
|
395
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
396
|
+
false, 'board-authored REMOVAL revokes: removal-first applies to every provenance');
|
|
397
|
+
select is(agent_board._membership_consent_kind(
|
|
398
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000003'),
|
|
399
|
+
null, 'board-authored removal: ...and _kind agrees');
|
|
400
|
+
|
|
401
|
+
-- THE discriminating case for LEDGER-ONLY. The other fixture (active row + a `left` event)
|
|
402
|
+
-- catches an implementation where membership_state replaces or overrides the ledger -- but NOT
|
|
403
|
+
-- the one actually prohibited: consult the ledger when it has events, fall back to
|
|
404
|
+
-- membership_state when it is EMPTY. That implementation passes every other assertion in this
|
|
405
|
+
-- file. Agent 7 exists solely for this and is never given an event.
|
|
406
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
407
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000007',
|
|
408
|
+
'member', 'joined', 'c0000000-0000-4000-8000-000000000001');
|
|
409
|
+
|
|
410
|
+
select is(
|
|
411
|
+
(select count(*)::int from agent_board.board_memberships
|
|
412
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000007'
|
|
413
|
+
and membership_state in ('joined','subscribed')),
|
|
414
|
+
1,
|
|
415
|
+
'LEDGER-ONLY setup: agent 7 holds an ACTIVE membership row');
|
|
416
|
+
select is(
|
|
417
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
418
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000007'),
|
|
419
|
+
0,
|
|
420
|
+
'LEDGER-ONLY setup: ...and its ledger is EMPTY');
|
|
421
|
+
|
|
422
|
+
select is(agent_board._membership_consent_present(
|
|
423
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000007'),
|
|
424
|
+
false,
|
|
425
|
+
'LEDGER-ONLY: an ACTIVE membership with an EMPTY ledger is NOT consent (no fallback)');
|
|
426
|
+
select is(agent_board._membership_consent_kind(
|
|
427
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000007'),
|
|
428
|
+
null, 'LEDGER-ONLY: ...and _kind agrees');
|
|
429
|
+
|
|
430
|
+
-- a silent ledger reads false, never "unknown"
|
|
431
|
+
select is(agent_board._membership_consent_present(
|
|
432
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000006'),
|
|
433
|
+
false, 'predicate: a silent ledger is not consent');
|
|
434
|
+
select is(agent_board._membership_consent_kind(
|
|
435
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000006'),
|
|
436
|
+
null, 'predicate: ...and _kind agrees on the silent ledger');
|
|
437
|
+
|
|
438
|
+
-- REMOVAL-FIRST and seq ordering: decline after consent -> false; re-accept -> true.
|
|
439
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
440
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
441
|
+
'consent_decline', 'left', 'target-authored', 'c0000000-0000-4000-8000-000000000002');
|
|
442
|
+
select is(agent_board._membership_consent_present(
|
|
443
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
444
|
+
false, 'predicate: a later decline revokes consent (removal-first)');
|
|
445
|
+
select is(agent_board._membership_consent_kind(
|
|
446
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
447
|
+
null, 'sequence: ...and _kind agrees after the decline');
|
|
448
|
+
|
|
449
|
+
-- A board-authored RE-INVITE after the decline must NOT launder it back to consent.
|
|
450
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
451
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
452
|
+
'invite_offer', 'invited', 'board-authored', 'c0000000-0000-4000-8000-000000000001');
|
|
453
|
+
select is(agent_board._membership_consent_present(
|
|
454
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
455
|
+
false, 'predicate: a re-invite cannot launder a prior refusal (REJECTSEM substrate)');
|
|
456
|
+
select is(agent_board._membership_consent_kind(
|
|
457
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
458
|
+
null, 'sequence: ...and _kind agrees after the re-invite');
|
|
459
|
+
|
|
460
|
+
-- ...but the subject's own later act does restore it.
|
|
461
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
462
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002',
|
|
463
|
+
'consent_self_join', 'joined', 'target-authored', 'c0000000-0000-4000-8000-000000000002');
|
|
464
|
+
select is(agent_board._membership_consent_present(
|
|
465
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
466
|
+
true, 'predicate: the subject can consent again after declining');
|
|
467
|
+
select is(agent_board._membership_consent_kind(
|
|
468
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000002'),
|
|
469
|
+
'target', 'sequence: ...and _kind agrees after re-consent (pins _kind ordering too)');
|
|
470
|
+
|
|
471
|
+
-- All four events for agent 2 landed in a strict seq order despite identical created_at.
|
|
472
|
+
select is(
|
|
473
|
+
(select count(distinct seq)::int from agent_board.board_membership_events
|
|
474
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000002'),
|
|
475
|
+
4,
|
|
476
|
+
'ordering: every event has a distinct seq');
|
|
477
|
+
select is(
|
|
478
|
+
(select count(distinct created_at)::int from agent_board.board_membership_events
|
|
479
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000002'),
|
|
480
|
+
1,
|
|
481
|
+
'ordering: ...while created_at is IDENTICAL for all of them (transaction_timestamp)');
|
|
482
|
+
|
|
483
|
+
-- ===========================================================================
|
|
484
|
+
-- 5. Function contracts
|
|
485
|
+
-- ===========================================================================
|
|
486
|
+
-- provolatile is "char", so it must be cast before concatenation ("char" || text is
|
|
487
|
+
-- ambiguous). Asserting the two properties separately is clearer than a packed string.
|
|
488
|
+
select is(
|
|
489
|
+
(select p.provolatile::text from pg_proc p
|
|
490
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
491
|
+
where n.nspname = 'agent_board' and p.proname = '_membership_consent_present'),
|
|
492
|
+
's',
|
|
493
|
+
'contract: _membership_consent_present is STABLE');
|
|
494
|
+
|
|
495
|
+
select is(
|
|
496
|
+
(select p.prosecdef from pg_proc p
|
|
497
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
498
|
+
where n.nspname = 'agent_board' and p.proname = '_membership_consent_present'),
|
|
499
|
+
true,
|
|
500
|
+
'contract: _membership_consent_present is SECURITY DEFINER');
|
|
501
|
+
|
|
502
|
+
select is(
|
|
503
|
+
(select pg_get_function_identity_arguments(p.oid) from pg_proc p
|
|
504
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
505
|
+
where n.nspname = 'agent_board' and p.proname = '_membership_consent_present'),
|
|
506
|
+
'p_board_id uuid, p_agent_id uuid',
|
|
507
|
+
'contract: the frozen signature takes NO actor -- consent is a property of the subject');
|
|
508
|
+
|
|
509
|
+
-- ===========================================================================
|
|
510
|
+
-- 6. Backfill
|
|
511
|
+
-- ===========================================================================
|
|
512
|
+
-- Duplicate non-active rows for one (board, agent) are permitted -- the partial unique index
|
|
513
|
+
-- constrains ACTIVE rows only -- and `not exists` sees the table as it stood BEFORE the
|
|
514
|
+
-- statement, so without DISTINCT ON each duplicate emits its own event.
|
|
515
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
516
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000006', 'member', 'requested', 'c0000000-0000-4000-8000-000000000001'),
|
|
517
|
+
('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000006', 'member', 'invited', 'c0000000-0000-4000-8000-000000000001');
|
|
518
|
+
|
|
519
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
520
|
+
select distinct on (bm.board_id, bm.agent_id)
|
|
521
|
+
bm.board_id, bm.agent_id, 'backfill_neutral', bm.membership_state, 'board-authored', b.created_by_agent_id
|
|
522
|
+
from agent_board.board_memberships bm
|
|
523
|
+
join agent_board.boards b on b.id = bm.board_id
|
|
524
|
+
where bm.membership_state in ('requested', 'invited')
|
|
525
|
+
and b.created_by_agent_id is not null
|
|
526
|
+
and b.created_by_agent_id <> bm.agent_id
|
|
527
|
+
and not exists (select 1 from agent_board.board_membership_events e
|
|
528
|
+
where e.board_id = bm.board_id and e.agent_id = bm.agent_id)
|
|
529
|
+
order by bm.board_id, bm.agent_id, bm.id;
|
|
530
|
+
|
|
531
|
+
select is(
|
|
532
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
533
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000006' and event_kind = 'backfill_neutral'),
|
|
534
|
+
1,
|
|
535
|
+
'backfill: duplicate pending rows yield exactly ONE event, not one per row');
|
|
536
|
+
|
|
537
|
+
select is(
|
|
538
|
+
(select provenance from agent_board.board_membership_events
|
|
539
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000006' and event_kind = 'backfill_neutral'),
|
|
540
|
+
'board-authored',
|
|
541
|
+
'backfill: annotates as board-authored');
|
|
542
|
+
|
|
543
|
+
select is(
|
|
544
|
+
agent_board._membership_consent_present(
|
|
545
|
+
'cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000006'),
|
|
546
|
+
false,
|
|
547
|
+
'backfill: a backfilled row does NOT read as consent');
|
|
548
|
+
|
|
549
|
+
-- The owner-subject exclusion (b.created_by_agent_id <> bm.agent_id) is load-bearing, not a
|
|
550
|
+
-- tidy-up: a board-authored event REQUIRES actor <> subject, so backfilling the CREATOR's own
|
|
551
|
+
-- pending row would violate the anti-forge CHECK and abort the migration on deploy. The
|
|
552
|
+
-- creator's row must be skipped entirely.
|
|
553
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
554
|
+
values ('cb000000-0000-4000-8000-000000000001', 'c0000000-0000-4000-8000-000000000001',
|
|
555
|
+
'owner', 'requested', 'c0000000-0000-4000-8000-000000000001');
|
|
556
|
+
|
|
557
|
+
select lives_ok($$
|
|
558
|
+
insert into agent_board.board_membership_events(board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
559
|
+
select distinct on (bm.board_id, bm.agent_id)
|
|
560
|
+
bm.board_id, bm.agent_id, 'backfill_neutral', bm.membership_state, 'board-authored', b.created_by_agent_id
|
|
561
|
+
from agent_board.board_memberships bm
|
|
562
|
+
join agent_board.boards b on b.id = bm.board_id
|
|
563
|
+
where bm.membership_state in ('requested', 'invited')
|
|
564
|
+
and b.created_by_agent_id is not null
|
|
565
|
+
and b.created_by_agent_id <> bm.agent_id
|
|
566
|
+
and not exists (select 1 from agent_board.board_membership_events e
|
|
567
|
+
where e.board_id = bm.board_id and e.agent_id = bm.agent_id)
|
|
568
|
+
order by bm.board_id, bm.agent_id, bm.id
|
|
569
|
+
$$, 'backfill: survives a board CREATOR holding a pending row on their own board');
|
|
570
|
+
|
|
571
|
+
select is(
|
|
572
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
573
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000001' and event_kind = 'backfill_neutral'),
|
|
574
|
+
0,
|
|
575
|
+
'backfill: the creator''s own pending row is SKIPPED (board-authored needs actor <> subject)');
|
|
576
|
+
|
|
577
|
+
-- IDEMPOTENCY asserted AFTER a second application, not before it. Checking the count before the
|
|
578
|
+
-- rerun proves nothing; a miscorrelated guard keeps every lexical assertion green and still
|
|
579
|
+
-- inserts a duplicate.
|
|
580
|
+
insert into agent_board.board_membership_events(
|
|
581
|
+
board_id, agent_id, event_kind, result_state, provenance, actor_agent_id)
|
|
582
|
+
select distinct on (bm.board_id, bm.agent_id)
|
|
583
|
+
bm.board_id, bm.agent_id, 'backfill_neutral', bm.membership_state,
|
|
584
|
+
'board-authored', b.created_by_agent_id
|
|
585
|
+
from agent_board.board_memberships bm
|
|
586
|
+
join agent_board.boards b on b.id = bm.board_id
|
|
587
|
+
where bm.membership_state in ('requested', 'invited')
|
|
588
|
+
and b.created_by_agent_id is not null
|
|
589
|
+
and b.created_by_agent_id <> bm.agent_id
|
|
590
|
+
and not exists (select 1 from agent_board.board_membership_events e
|
|
591
|
+
where e.board_id = bm.board_id and e.agent_id = bm.agent_id)
|
|
592
|
+
order by bm.board_id, bm.agent_id, bm.id;
|
|
593
|
+
|
|
594
|
+
select is(
|
|
595
|
+
(select count(*)::int from agent_board.board_membership_events
|
|
596
|
+
where agent_id = 'c0000000-0000-4000-8000-000000000006' and event_kind = 'backfill_neutral'),
|
|
597
|
+
1,
|
|
598
|
+
'backfill: IDEMPOTENT -- a second application adds nothing');
|
|
599
|
+
|
|
600
|
+
-- ===========================================================================
|
|
601
|
+
-- ===========================================================================
|
|
602
|
+
-- 6b. Join-request EXPIRY
|
|
603
|
+
-- ===========================================================================
|
|
604
|
+
-- A join request IS the subject's consenting act, but it goes stale: an owner approving a
|
|
605
|
+
-- six-month-old request re-adds someone who has moved on. Per-board TTL, 30 days by default.
|
|
606
|
+
-- An expired request is NEUTRAL, never a refusal -- so a genuine grant BENEATH it survives.
|
|
607
|
+
-- created_at is written explicitly here; it is the one column age depends on.
|
|
608
|
+
|
|
609
|
+
select is(
|
|
610
|
+
(select join_request_ttl_days from agent_board.boards
|
|
611
|
+
where id = 'cb000000-0000-4000-8000-000000000001'),
|
|
612
|
+
30,
|
|
613
|
+
'expiry: the default request TTL is 30 days');
|
|
614
|
+
|
|
615
|
+
-- agent 1 of a second board: a FRESH request still counts.
|
|
616
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
617
|
+
values ('cb000000-0000-4000-8000-000000000002', 'substrate-b2', 'Substrate B2',
|
|
618
|
+
'c0000000-0000-4000-8000-000000000001', 'private', 'active');
|
|
619
|
+
|
|
620
|
+
insert into agent_board.board_membership_events(
|
|
621
|
+
board_id, agent_id, event_kind, result_state, provenance, actor_agent_id, created_at)
|
|
622
|
+
values ('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000002',
|
|
623
|
+
'consent_join_request', 'requested', 'target-authored',
|
|
624
|
+
'c0000000-0000-4000-8000-000000000002', now() - interval '29 days');
|
|
625
|
+
select is(agent_board._membership_consent_present(
|
|
626
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000002'),
|
|
627
|
+
true, 'expiry: a request 29 days old still counts as consent');
|
|
628
|
+
select is(agent_board._membership_consent_kind(
|
|
629
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000002'),
|
|
630
|
+
'target', 'expiry: ...and _kind agrees');
|
|
631
|
+
|
|
632
|
+
-- ...and one past the TTL does not.
|
|
633
|
+
insert into agent_board.board_membership_events(
|
|
634
|
+
board_id, agent_id, event_kind, result_state, provenance, actor_agent_id, created_at)
|
|
635
|
+
values ('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000003',
|
|
636
|
+
'consent_join_request', 'requested', 'target-authored',
|
|
637
|
+
'c0000000-0000-4000-8000-000000000003', now() - interval '31 days');
|
|
638
|
+
select is(agent_board._membership_consent_present(
|
|
639
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000003'),
|
|
640
|
+
false, 'expiry: a request 31 days old is NO LONGER consent');
|
|
641
|
+
select is(agent_board._membership_consent_kind(
|
|
642
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000003'),
|
|
643
|
+
null, 'expiry: ...and _kind agrees');
|
|
644
|
+
|
|
645
|
+
-- NEUTRAL, not a refusal: a genuine self-join BENEATH an expired request survives.
|
|
646
|
+
insert into agent_board.board_membership_events(
|
|
647
|
+
board_id, agent_id, event_kind, result_state, provenance, actor_agent_id, created_at)
|
|
648
|
+
values ('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000004',
|
|
649
|
+
'consent_self_join', 'joined', 'target-authored',
|
|
650
|
+
'c0000000-0000-4000-8000-000000000004', now() - interval '90 days'),
|
|
651
|
+
('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000004',
|
|
652
|
+
'consent_join_request', 'requested', 'target-authored',
|
|
653
|
+
'c0000000-0000-4000-8000-000000000004', now() - interval '60 days');
|
|
654
|
+
select is(agent_board._membership_consent_present(
|
|
655
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000004'),
|
|
656
|
+
true,
|
|
657
|
+
'expiry: an expired request is NEUTRAL -- an earlier genuine join beneath it survives');
|
|
658
|
+
-- _kind MUST be asserted here too. `null` is what BOTH a neutral and a removal-like
|
|
659
|
+
-- implementation return for a bare expired request, so the standalone case cannot tell them
|
|
660
|
+
-- apart -- only the overlay can. A _kind that kept the expired request as the latest candidate
|
|
661
|
+
-- would return null here while _present returns true: the twins disagreeing, suite green. That
|
|
662
|
+
-- exact divergence already shipped once in an earlier draft.
|
|
663
|
+
select is(agent_board._membership_consent_kind(
|
|
664
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000004'),
|
|
665
|
+
'target',
|
|
666
|
+
'expiry: _kind agrees -- it reports the surviving grant, not the expired request');
|
|
667
|
+
|
|
668
|
+
-- ...whereas a real REMOVAL beneath nothing still revokes, so expiry has not become a blanket skip.
|
|
669
|
+
insert into agent_board.board_membership_events(
|
|
670
|
+
board_id, agent_id, event_kind, result_state, provenance, actor_agent_id, created_at)
|
|
671
|
+
values ('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000005',
|
|
672
|
+
'consent_join_request', 'requested', 'target-authored',
|
|
673
|
+
'c0000000-0000-4000-8000-000000000005', now() - interval '90 days'),
|
|
674
|
+
('cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000005',
|
|
675
|
+
'self_withdraw', 'left', 'target-authored',
|
|
676
|
+
'c0000000-0000-4000-8000-000000000005', now() - interval '80 days');
|
|
677
|
+
select is(agent_board._membership_consent_present(
|
|
678
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000005'),
|
|
679
|
+
false, 'expiry: a removal is still a removal -- expiry is not a blanket skip');
|
|
680
|
+
|
|
681
|
+
-- The TTL is PER BOARD: widening it revives the same 31-day-old request.
|
|
682
|
+
update agent_board.boards set join_request_ttl_days = 365
|
|
683
|
+
where id = 'cb000000-0000-4000-8000-000000000002';
|
|
684
|
+
select is(agent_board._membership_consent_present(
|
|
685
|
+
'cb000000-0000-4000-8000-000000000002', 'c0000000-0000-4000-8000-000000000003'),
|
|
686
|
+
true, 'expiry: the TTL is per-board -- widening it to 365 days revives that request');
|
|
687
|
+
update agent_board.boards set join_request_ttl_days = 30
|
|
688
|
+
where id = 'cb000000-0000-4000-8000-000000000002';
|
|
689
|
+
|
|
690
|
+
-- A non-positive TTL is refused.
|
|
691
|
+
select throws_ok($$
|
|
692
|
+
update agent_board.boards set join_request_ttl_days = 0
|
|
693
|
+
where id = 'cb000000-0000-4000-8000-000000000002'
|
|
694
|
+
$$, '23514', null, 'expiry: a non-positive TTL is refused by the constraint');
|
|
695
|
+
|
|
696
|
+
-- 7. This migration changes no route
|
|
697
|
+
-- ===========================================================================
|
|
698
|
+
-- SCOPE BOUNDARY, updated when gating landed. This previously read "no membership route
|
|
699
|
+
-- consults the predicate yet" and FAILED the moment MBCONSENT-B(ii) shipped -- which is what a
|
|
700
|
+
-- scope guard is for. It now names the expected consumer SET, so a route that starts consulting
|
|
701
|
+
-- the predicate without being added here fails and names itself.
|
|
702
|
+
--
|
|
703
|
+
-- resolve_board_request is the sole predicate READER. join_board and update_board_membership
|
|
704
|
+
-- gate STRUCTURALLY (the actor must be the subject; no other-agent promotion), which needs no
|
|
705
|
+
-- ledger read -- a structural refusal is stronger, because it cannot be satisfied by
|
|
706
|
+
-- manufacturing evidence.
|
|
707
|
+
select is(
|
|
708
|
+
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
709
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
710
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
711
|
+
and p.proname in ('join_board','resolve_board_request','update_board_membership','_upsert_board_membership_state')
|
|
712
|
+
and pg_get_functiondef(p.oid) ~ '_membership_consent_present'),
|
|
713
|
+
'resolve_board_request, update_board_membership',
|
|
714
|
+
'scope: exactly TWO membership routes read the predicate');
|
|
715
|
+
|
|
716
|
+
select is((select version from agent_board.schema_version), 8,
|
|
717
|
+
'schema_version stays 8 -- this migration is additive');
|
|
718
|
+
|
|
719
|
+
select finish();
|
|
720
|
+
rollback;
|