@consiliency/agent-board-schema 1.64.0 → 1.74.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 +19 -0
- package/fixtures/already-applied.json +44 -4
- 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/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
- package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
- package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
- package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
- package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
- package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
- package/package.json +1 -1
- package/schema-manifest.json +93 -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 +12 -2
- package/tests/agent_board_661_reply_delivery.test.sql +185 -0
- package/tests/agent_board_667_boundary.test.sql +162 -0
- package/tests/agent_board_667_conversion_branches.test.sql +91 -0
- package/tests/agent_board_667_conversion_profile.test.sql +112 -0
- package/tests/agent_board_667_history_class.test.sql +785 -0
- package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
- package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
- package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
- package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
- package/tests/agent_board_667_retention.test.sql +201 -0
- package/tests/agent_board_667_routing.test.sql +294 -0
- package/tests/agent_board_667_sender_effects.test.sql +100 -0
- package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
- package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
- package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
- package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
- package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
- package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
- package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
- package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
- package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
- package/tests/agent_board_667_writers.test.sql +281 -0
- package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
- package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
- package/tests/agent_board_addressing.test.sql +8 -5
- package/tests/agent_board_claim_subject_gate.test.sql +7 -3
- package/tests/agent_board_claimpolicy_race.test.sql +39 -11
- package/tests/agent_board_consent_gating.test.sql +4 -4
- package/tests/agent_board_delegate_role_class.test.sql +33 -15
- package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
- package/tests/agent_board_fk_lock_edges.test.sql +3 -0
- package/tests/agent_board_gatenull_membstate.test.sql +7 -4
- package/tests/agent_board_hosted_delegates.test.sql +5 -2
- package/tests/agent_board_inbox_rpcs.test.sql +5 -3
- package/tests/agent_board_obsgate.test.sql +8 -4
- package/tests/agent_board_obsgate4.test.sql +2 -2
- package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
- package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
- 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_session_identity_behavior_classes.test.sql +2 -2
- package/tests/agent_board_session_identity_limits.test.sql +1 -1
- package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
- package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
- package/tests/agent_board_session_provenance.test.sql +106 -0
- package/tests/agent_board_sidserver_authority.test.sql +69 -0
- package/tests/agent_board_sidserver_catalog.test.sql +47 -0
- package/tests/agent_board_signed_write_gateway.test.sql +4 -2
- package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
- package/tests/agent_board_v2_invariants.test.sql +5 -2
- package/tests/agent_board_write_enforcement_class.test.sql +3 -3
- package/tests/catalog/function_execute_privileges.json +43 -7
- package/tests/catalog/p_metadata_parameters.json +16 -0
- package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
- package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
- package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
- package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
- package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
- package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
- package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
- package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
- package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
- package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
- package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
- package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
- package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
- package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
- package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
- package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
- package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
- package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
- package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
- package/tests/isolation/expected/association_board_vs_board.out +71 -0
- package/tests/isolation/expected/association_post_wait_authority.out +77 -0
- package/tests/isolation/expected/association_strong_history_writers.out +203 -0
- package/tests/isolation/expected/association_strong_isolation.out +185 -0
- package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
- package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
- package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
- package/tests/isolation/expected/association_vs_private_message.out +67 -0
- package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
- package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
- package/tests/isolation/expected/consent_approval_lock.out +1 -1
- package/tests/isolation/expected/message_replay_scope.out +209 -0
- package/tests/isolation/expected/tasking_replay_scope.out +125 -0
- package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
- package/tests/isolation/specs/association_board_vs_board.spec +37 -0
- package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
- package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
- package/tests/isolation/specs/association_strong_isolation.spec +50 -0
- package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
- package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
- package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
- package/tests/isolation/specs/association_vs_private_message.spec +33 -0
- package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
- package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
- package/tests/isolation/specs/consent_approval_lock.spec +13 -7
- package/tests/isolation/specs/message_replay_scope.spec +77 -0
- package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
- package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
- package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
- package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
- package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
- package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
- package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
- package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
- package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
- package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
- package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
- package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
- package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
- package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
- package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
- package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
- package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
- package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
- package/tests/mutations/717-f1/run.sh +17 -0
- package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
- package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
- package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
- package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
- package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
- package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
- package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
- package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
- package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
- package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
- package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
- package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
- package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
- package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
- package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
- package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
- package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
- package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
- package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
- package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
- package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
- package/tests/mutations/717-f2b/run.sh +82 -0
package/README.md
CHANGED
|
@@ -7,6 +7,25 @@ and materialize it — they do not author `agent_board` DDL.
|
|
|
7
7
|
|
|
8
8
|
## What's in the bundle
|
|
9
9
|
|
|
10
|
+
Schema 1.68.0 requires a resolved active owner/admin principal to resolve pending
|
|
11
|
+
memberships, independently of the board role gate or creator identity. Like the
|
|
12
|
+
pending-list reader, it refuses unresolved service-role sessions. It keeps 1.67.0's
|
|
13
|
+
session-derived attribution: the resolution write and redundant-request retirement
|
|
14
|
+
are attributed to the resolving session agent, never to `p_created_by_agent_id`.
|
|
15
|
+
|
|
16
|
+
Schema 1.67.0 derives invitation, request-resolution (including redundant-request
|
|
17
|
+
retirement), and self-join attribution from the session agent. The compatibility
|
|
18
|
+
`p_created_by_agent_id` argument is ignored. An unresolved service operator supplies
|
|
19
|
+
no agent attribution; existing row provenance is retained on NULL updates.
|
|
20
|
+
|
|
21
|
+
Schema 1.66.0 refuses join requests while removed standing remains. The request
|
|
22
|
+
RPC locks the subject's existing rows before deciding standing, preserving
|
|
23
|
+
eviction evidence and the existing active-member no-op.
|
|
24
|
+
|
|
25
|
+
Schema 1.65.0 repairs thread and delegation RLS correlation to the protected
|
|
26
|
+
row and grants authenticated execution of the existing read predicates used
|
|
27
|
+
by ceiling and thread-membership policies. Unrelated rows remain hidden.
|
|
28
|
+
|
|
10
29
|
Schema 1.64.0 adds `20261014000200_agent_board_last_owner.sql`: a refusal-only
|
|
11
30
|
table-level last-active-owner invariant, preserving the 1.63.0 role-grant policy.
|
|
12
31
|
Concurrent departures serialize per board; recovery remains service_role-only.
|
|
@@ -577,6 +577,10 @@
|
|
|
577
577
|
"id": "20260926000100_agent_board_ownerless_quota_census",
|
|
578
578
|
"sha256": "bbfabcb2d45cbcebd1e6e121032a1045e8406ea8763a784762a92d210e1e1043"
|
|
579
579
|
},
|
|
580
|
+
{
|
|
581
|
+
"id": "20260927000100_agent_board_fleet_domain_no_downgrade",
|
|
582
|
+
"sha256": "ec5c4d89aff2d6d77d44cb3d00c1096b39010c6064c23d72eb3c5c354f8a5e74"
|
|
583
|
+
},
|
|
580
584
|
{
|
|
581
585
|
"id": "20260928000100_agent_board_ownerless_quota_renew_convergence",
|
|
582
586
|
"sha256": "9982efff25378703a744187b15081b5cd53ab38fa6ac7d2950092a471780169e"
|
|
@@ -589,10 +593,6 @@
|
|
|
589
593
|
"id": "20260930000100_agent_board_claim_policy_race",
|
|
590
594
|
"sha256": "1893b57ccb643a8293c0dc801d2d66d20fe1d71a2b7dc95cd93f64ced39b472d"
|
|
591
595
|
},
|
|
592
|
-
{
|
|
593
|
-
"id": "20260927000100_agent_board_fleet_domain_no_downgrade",
|
|
594
|
-
"sha256": "ec5c4d89aff2d6d77d44cb3d00c1096b39010c6064c23d72eb3c5c354f8a5e74"
|
|
595
|
-
},
|
|
596
596
|
{
|
|
597
597
|
"id": "20261001000100_agent_board_claimpolicy_lock_grant_revoke",
|
|
598
598
|
"sha256": "db73a9941d4601dd6db0c767fdc718534fe2856598a305f4747496cadc722afd"
|
|
@@ -648,6 +648,46 @@
|
|
|
648
648
|
{
|
|
649
649
|
"id": "20261014000200_agent_board_last_owner",
|
|
650
650
|
"sha256": "2fed8fc5a2c7c0ba9acbb12872b329db3f0902d96597c52cc50e21e4682c2fff"
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
"id": "20261015000100_agent_board_session_provenance",
|
|
654
|
+
"sha256": "b5e3c7fc859dff7c3a0ccd4c0a7fb22d6ada1df68cb028be138035b586e1462a"
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
"id": "20261015000200_agent_board_request_preserve_removed",
|
|
658
|
+
"sha256": "9bad7438909e9c02cc034b59af67fa962e7251bedb53d691e762e651ba1c343c"
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"id": "20261015000300_agent_board_rls_correlation",
|
|
662
|
+
"sha256": "aafe4a4b6eb1b6a5e3f0b5fbd8b4e4d395399de079c6a7b2eb2535ef4514bdc4"
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
"id": "20261015000400_agent_board_resolution_authority",
|
|
666
|
+
"sha256": "bfeb7bda1eb41bc431416502db69c33156ff72acfd79cefdfd556d933f79d809"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"id": "20261026000100_agent_board_thread_association",
|
|
670
|
+
"sha256": "93c3ffc694f5dbd3a5d34d5ef392462707f959119e8c0ff52677c74720dd2c6d"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"id": "20261101000100_agent_board_session_measured_mint",
|
|
674
|
+
"sha256": "b18323397aaacfb7be138f5befedb8cb9b6a0247b868bc22a2ed3481b5f17c06"
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"id": "20261104065658_agent_board_reply_delivery",
|
|
678
|
+
"sha256": "0f785d8c2b6dd389fec88851d88370095c2de5d07764b7c8ced277388a471660"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"id": "20261104170000_agent_board_tasking_replay_scope",
|
|
682
|
+
"sha256": "dc77bd6b017e067ab20aa5ecea7d31377420d8593d33e2731059c0cbba52e4c0"
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
"id": "20261104180000_agent_board_delivery_writer_execute",
|
|
686
|
+
"sha256": "732cd0b61706f1f901eeb88f3e3a30e91a34723b8df4cc972218ebca0859e07a"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
"id": "20261104190000_agent_board_message_replay_scope",
|
|
690
|
+
"sha256": "dbfd60300e8ce8ed78df8ae851f3990f58bcb43a7f9d53c2e25499a22e775a67"
|
|
651
691
|
}
|
|
652
692
|
]
|
|
653
693
|
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
-- Consiliency/message-board#665: membership attribution comes from the session.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
create or replace function agent_board.invite_board_member(
|
|
5
|
+
p_board_id uuid,
|
|
6
|
+
p_agent_id uuid,
|
|
7
|
+
p_created_by_agent_id uuid,
|
|
8
|
+
p_metadata jsonb default '{}'::jsonb
|
|
9
|
+
)
|
|
10
|
+
returns table(
|
|
11
|
+
membership_id uuid,
|
|
12
|
+
board_id uuid,
|
|
13
|
+
agent_id uuid,
|
|
14
|
+
membership_state text,
|
|
15
|
+
created_at timestamptz,
|
|
16
|
+
updated_at timestamptz
|
|
17
|
+
)
|
|
18
|
+
language plpgsql
|
|
19
|
+
security definer
|
|
20
|
+
set search_path = 'agent_board', 'public'
|
|
21
|
+
as $function$
|
|
22
|
+
declare
|
|
23
|
+
v_board agent_board.boards%rowtype;
|
|
24
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
25
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
26
|
+
begin
|
|
27
|
+
-- GATENULL (#58): only service_role may proceed with an unresolved actor.
|
|
28
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
29
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
30
|
+
end if;
|
|
31
|
+
|
|
32
|
+
select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
|
|
33
|
+
if v_board.id is null then
|
|
34
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
35
|
+
end if;
|
|
36
|
+
if v_actor is not null then
|
|
37
|
+
if v_board.role_gate_enforced then
|
|
38
|
+
-- Gate ON: NO creator exemption. Every actor (including a demoted creator)
|
|
39
|
+
-- must hold an active owner/admin membership.
|
|
40
|
+
if not exists (
|
|
41
|
+
select 1
|
|
42
|
+
from agent_board.board_memberships bm
|
|
43
|
+
where bm.board_id = p_board_id
|
|
44
|
+
and bm.agent_id = v_actor
|
|
45
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
46
|
+
and bm.role in ('owner', 'admin')
|
|
47
|
+
) then
|
|
48
|
+
raise exception 'board invite requires owner or admin role' using errcode = '42501';
|
|
49
|
+
end if;
|
|
50
|
+
elsif v_actor <> v_board.created_by_agent_id then
|
|
51
|
+
-- Gate OFF: creator bypasses; any other actor needs an active membership.
|
|
52
|
+
if not exists (
|
|
53
|
+
select 1
|
|
54
|
+
from agent_board.board_memberships bm
|
|
55
|
+
where bm.board_id = p_board_id
|
|
56
|
+
and bm.agent_id = v_actor
|
|
57
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
58
|
+
) then
|
|
59
|
+
raise exception 'board invite requires active membership' using errcode = '42501';
|
|
60
|
+
end if;
|
|
61
|
+
end if;
|
|
62
|
+
end if;
|
|
63
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
64
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
65
|
+
end if;
|
|
66
|
+
|
|
67
|
+
-- INVITEPOLICY (#90): a target may block a specific inviter on a specific board.
|
|
68
|
+
-- Gate on the SESSION actor (the real inviter), never p_created_by_agent_id (an
|
|
69
|
+
-- assertion). A null actor is the service_role operator carve-out (permissive).
|
|
70
|
+
if v_actor is not null
|
|
71
|
+
and not agent_board._invite_permitted(p_board_id, p_agent_id, v_actor) then
|
|
72
|
+
raise exception 'target has blocked invitations from this inviter on this board'
|
|
73
|
+
using errcode = '42501';
|
|
74
|
+
end if;
|
|
75
|
+
|
|
76
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
77
|
+
p_board_id,
|
|
78
|
+
p_agent_id,
|
|
79
|
+
'invited',
|
|
80
|
+
v_actor,
|
|
81
|
+
p_metadata
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return query
|
|
85
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
|
|
86
|
+
end;
|
|
87
|
+
$function$;
|
|
88
|
+
|
|
89
|
+
revoke all on function agent_board.invite_board_member(uuid,uuid,uuid,jsonb) from public, anon;
|
|
90
|
+
|
|
91
|
+
grant execute on function agent_board.invite_board_member(uuid,uuid,uuid,jsonb) to authenticated, service_role;
|
|
92
|
+
|
|
93
|
+
comment on function agent_board.invite_board_member(uuid,uuid,uuid,jsonb) is 'Membership attribution is derived from current_agent_id(); p_created_by_agent_id is retained for signature compatibility only. Unresolved service-role writes supply no agent attribution.';
|
|
94
|
+
|
|
95
|
+
create or replace function agent_board.resolve_board_request(
|
|
96
|
+
p_board_id uuid,
|
|
97
|
+
p_agent_id uuid,
|
|
98
|
+
p_membership_state text,
|
|
99
|
+
p_created_by_agent_id uuid,
|
|
100
|
+
p_metadata jsonb default '{}'::jsonb
|
|
101
|
+
)
|
|
102
|
+
returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamp with time zone, updated_at timestamp with time zone)
|
|
103
|
+
language plpgsql
|
|
104
|
+
security definer
|
|
105
|
+
set search_path to 'agent_board', 'public'
|
|
106
|
+
as $function$
|
|
107
|
+
declare
|
|
108
|
+
v_effective_state text;
|
|
109
|
+
v_board agent_board.boards%rowtype;
|
|
110
|
+
v_membership_id uuid;
|
|
111
|
+
v_active_id uuid;
|
|
112
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
113
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
114
|
+
begin
|
|
115
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
116
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
117
|
+
end if;
|
|
118
|
+
|
|
119
|
+
if p_membership_state not in ('joined', 'subscribed', 'removed') then
|
|
120
|
+
raise exception 'resolve_board_request only accepts joined, subscribed, or removed' using errcode = '22023';
|
|
121
|
+
end if;
|
|
122
|
+
select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
|
|
123
|
+
if v_board.id is null then
|
|
124
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
125
|
+
end if;
|
|
126
|
+
if v_actor is not null then
|
|
127
|
+
if v_board.role_gate_enforced then
|
|
128
|
+
if not exists (
|
|
129
|
+
select 1
|
|
130
|
+
from agent_board.board_memberships bm
|
|
131
|
+
where bm.board_id = p_board_id
|
|
132
|
+
and bm.agent_id = v_actor
|
|
133
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
134
|
+
and bm.role in ('owner', 'admin')
|
|
135
|
+
) then
|
|
136
|
+
raise exception 'board request resolution requires owner or admin role' using errcode = '42501';
|
|
137
|
+
end if;
|
|
138
|
+
elsif v_actor <> v_board.created_by_agent_id then
|
|
139
|
+
if not exists (
|
|
140
|
+
select 1
|
|
141
|
+
from agent_board.board_memberships bm
|
|
142
|
+
where bm.board_id = p_board_id
|
|
143
|
+
and bm.agent_id = v_actor
|
|
144
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
145
|
+
) then
|
|
146
|
+
raise exception 'board request resolution requires active membership' using errcode = '42501';
|
|
147
|
+
end if;
|
|
148
|
+
end if;
|
|
149
|
+
end if;
|
|
150
|
+
|
|
151
|
+
-- Lock the WHOLE (board, agent) set first so a concurrent self-withdrawal on ANY physical row
|
|
152
|
+
-- serialises behind this resolution (#109's race is per-(board,agent), not per-row).
|
|
153
|
+
perform 1
|
|
154
|
+
from agent_board.board_memberships bm
|
|
155
|
+
where bm.board_id = p_board_id
|
|
156
|
+
and bm.agent_id = p_agent_id
|
|
157
|
+
order by bm.id
|
|
158
|
+
for update;
|
|
159
|
+
|
|
160
|
+
-- Select the PENDING row explicitly (not the total-order winner). B2 (#116): with TWO pending
|
|
161
|
+
-- rows (a requested AND an invited) for one subject this resolves the NEWEST; the older dangles.
|
|
162
|
+
-- Inherent to the multi-row model — resolution is one act on one row.
|
|
163
|
+
select bm.id
|
|
164
|
+
into v_membership_id
|
|
165
|
+
from agent_board.board_memberships bm
|
|
166
|
+
where bm.board_id = p_board_id
|
|
167
|
+
and bm.agent_id = p_agent_id
|
|
168
|
+
and bm.membership_state in ('requested', 'invited')
|
|
169
|
+
order by bm.created_at desc, bm.id desc
|
|
170
|
+
limit 1;
|
|
171
|
+
|
|
172
|
+
select bm.id
|
|
173
|
+
into v_active_id
|
|
174
|
+
from agent_board.board_memberships bm
|
|
175
|
+
where bm.board_id = p_board_id
|
|
176
|
+
and bm.agent_id = p_agent_id
|
|
177
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
178
|
+
order by bm.created_at desc, bm.id desc
|
|
179
|
+
limit 1;
|
|
180
|
+
|
|
181
|
+
if v_membership_id is null then
|
|
182
|
+
raise exception 'pending board membership not found' using errcode = 'P0002';
|
|
183
|
+
end if;
|
|
184
|
+
|
|
185
|
+
-- OPTION 1 FALLBACK (#109 A3): approving a subject who is ALREADY active is a no-op success,
|
|
186
|
+
-- not a promotion of the pending row into collision with the active one. Return the active row
|
|
187
|
+
-- untouched. Rejection still falls through to act on the pending row.
|
|
188
|
+
--
|
|
189
|
+
-- B4 (#116): the no-op path intentionally IGNORES the requested target state and p_metadata —
|
|
190
|
+
-- approving with target 'subscribed' a 'joined' active row returns 'joined' (a self-toggle
|
|
191
|
+
-- belongs to update_board_membership, which is where the old path coupled the metadata write to
|
|
192
|
+
-- a demotion). Nothing legitimate is lost.
|
|
193
|
+
if p_membership_state in ('joined', 'subscribed') and v_active_id is not null then
|
|
194
|
+
-- B1 (#116): retire the redundant pending row so it stops showing in pending listings. The
|
|
195
|
+
-- subject is already active via v_active_id (untouched); route the retire through the sink so
|
|
196
|
+
-- the write EMITS (the emission-completeness invariant requires every membership write to hit
|
|
197
|
+
-- the ledger) — a board-authored 'left' event, which is NEUTRAL for consent (not the subject's
|
|
198
|
+
-- own act) and does not affect the subject's consent-present status carried by the active row.
|
|
199
|
+
-- 'left' is recoverable. Previously only a reject cleared this, leaving an owner-visible
|
|
200
|
+
-- dangling request after a no-op approve.
|
|
201
|
+
perform agent_board._upsert_board_membership_state(
|
|
202
|
+
p_board_id, p_agent_id, 'left', v_actor, p_metadata, v_membership_id, 'noop_superseded'
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return query
|
|
206
|
+
select bm.id, bm.board_id, bm.agent_id, bm.membership_state, bm.left_at, bm.updated_at
|
|
207
|
+
from agent_board.board_memberships bm
|
|
208
|
+
where bm.id = v_active_id;
|
|
209
|
+
return;
|
|
210
|
+
end if;
|
|
211
|
+
|
|
212
|
+
-- REJECTSEM (#89): a rejection is NOT an eviction — resolve only acts on a PENDING row, so a
|
|
213
|
+
-- refusal lands on 'left' (recoverable), never 'removed' (which bars self-re-entry). Applied to
|
|
214
|
+
-- both requested and invited origins.
|
|
215
|
+
v_effective_state := case when p_membership_state = 'removed' then 'left'
|
|
216
|
+
else p_membership_state end;
|
|
217
|
+
|
|
218
|
+
-- #82: promotion into an active state requires the subject's own consent act (the sole
|
|
219
|
+
-- consent-predicate read site). Authenticated only; service_role keeps the operator carve-out.
|
|
220
|
+
if v_actor is not null
|
|
221
|
+
and v_effective_state in ('joined', 'subscribed')
|
|
222
|
+
and not agent_board._membership_consent_present(p_board_id, p_agent_id) then
|
|
223
|
+
raise exception 'admitting % requires an act by that agent', p_agent_id
|
|
224
|
+
using errcode = '42501';
|
|
225
|
+
end if;
|
|
226
|
+
|
|
227
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
228
|
+
p_board_id,
|
|
229
|
+
p_agent_id,
|
|
230
|
+
v_effective_state,
|
|
231
|
+
v_actor,
|
|
232
|
+
p_metadata,
|
|
233
|
+
v_membership_id,
|
|
234
|
+
case when p_membership_state = 'removed' then 'reject_withdrawn' else null end
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return query
|
|
238
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
|
|
239
|
+
end;
|
|
240
|
+
$function$;
|
|
241
|
+
|
|
242
|
+
revoke all on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) from public, anon;
|
|
243
|
+
|
|
244
|
+
grant execute on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) to authenticated, service_role;
|
|
245
|
+
|
|
246
|
+
comment on function agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb) is 'Membership attribution is derived from current_agent_id(); p_created_by_agent_id is retained for signature compatibility only. Unresolved service-role writes supply no agent attribution.';
|
|
247
|
+
|
|
248
|
+
create or replace function agent_board.join_board(p_board_id uuid, p_agent_id uuid, p_membership_state text DEFAULT 'joined'::text, p_created_by_agent_id uuid DEFAULT NULL::uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
249
|
+
RETURNS TABLE(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
250
|
+
LANGUAGE plpgsql
|
|
251
|
+
SECURITY DEFINER
|
|
252
|
+
SET search_path TO 'agent_board', 'public'
|
|
253
|
+
as $$
|
|
254
|
+
declare
|
|
255
|
+
v_board agent_board.boards%rowtype;
|
|
256
|
+
v_existing_state text;
|
|
257
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
258
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
259
|
+
begin
|
|
260
|
+
-- GATENULL (#58): only service_role may proceed with an unresolved actor.
|
|
261
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
262
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
263
|
+
end if;
|
|
264
|
+
|
|
265
|
+
if p_membership_state not in ('joined', 'subscribed') then
|
|
266
|
+
raise exception 'join_board only accepts joined or subscribed' using errcode = '22023';
|
|
267
|
+
end if;
|
|
268
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
269
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
270
|
+
end if;
|
|
271
|
+
|
|
272
|
+
select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
|
|
273
|
+
if v_board.id is null then
|
|
274
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
275
|
+
end if;
|
|
276
|
+
|
|
277
|
+
-- MBROWID (3a): prefer ACTIVE standing for joinability / idempotent-join. A bare
|
|
278
|
+
-- total-order can surface a high-id REMOVED row while a low-id JOINED row exists,
|
|
279
|
+
-- locking an active member out of their own private / invite_only board (P0002).
|
|
280
|
+
select bm.membership_state
|
|
281
|
+
into v_existing_state
|
|
282
|
+
from agent_board.board_memberships bm
|
|
283
|
+
where bm.board_id = p_board_id
|
|
284
|
+
and bm.agent_id = p_agent_id
|
|
285
|
+
order by
|
|
286
|
+
case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end,
|
|
287
|
+
bm.created_at desc,
|
|
288
|
+
bm.id desc
|
|
289
|
+
limit 1;
|
|
290
|
+
|
|
291
|
+
-- #82: the CREATOR EXEMPTION IS REMOVED. Previously a board's creator could join ANY agent
|
|
292
|
+
-- to their board -- `v_actor <> v_board.created_by_agent_id` let them past this guard, and
|
|
293
|
+
-- the joinability check below only runs for self-joins, so nothing else stopped them. That
|
|
294
|
+
-- is the force-join hole. Creating a board does not entitle you to place other people in it.
|
|
295
|
+
--
|
|
296
|
+
-- Only the subject may join the subject. service_role (v_actor null) keeps the operator
|
|
297
|
+
-- carve-out, which is deliberate and maintainer-confirmed.
|
|
298
|
+
if v_actor is not null and v_actor <> p_agent_id then
|
|
299
|
+
raise exception 'board membership actor mismatch' using errcode = '42501';
|
|
300
|
+
end if;
|
|
301
|
+
|
|
302
|
+
-- NULLFIX (CR): v_existing_state is NULL when the caller has NO membership row, and the original
|
|
303
|
+
-- guard then evaluated to NULL — `not (false or false or NULL)` is NULL, not TRUE — so `if NULL
|
|
304
|
+
-- then` never fired and ANY resolvable actor self-joined ANY board, private ones included. That
|
|
305
|
+
-- (not the null-actor bypass) was the true root cause of the live "joined a private invite_only
|
|
306
|
+
-- board with no invite" finding, and it is reachable by an ACTIVE agent, which the GATENULL guard
|
|
307
|
+
-- above cannot touch. coalesce() makes the absent-row case FALSE so the raise fires.
|
|
308
|
+
--
|
|
309
|
+
-- MBROWID (3b): a 'removed' agent is excluded from self-joining even an OPEN board via the
|
|
310
|
+
-- R-LINK-7 SET predicate (exists removed AND no active), not a single latest-row state.
|
|
311
|
+
-- Total-ordering the latest-row read alone is insufficient: a removed+left pair with equal
|
|
312
|
+
-- created_at can surface 'left' as the total-order winner while standing is still removed
|
|
313
|
+
-- (mutator + grant_board_role already use set-based rules). The three defenses must agree.
|
|
314
|
+
if v_actor is not null
|
|
315
|
+
and v_actor = p_agent_id
|
|
316
|
+
and v_board.created_by_agent_id <> v_actor
|
|
317
|
+
and not (
|
|
318
|
+
not (
|
|
319
|
+
exists (
|
|
320
|
+
select 1 from agent_board.board_memberships bm
|
|
321
|
+
where bm.board_id = p_board_id
|
|
322
|
+
and bm.agent_id = p_agent_id
|
|
323
|
+
and bm.membership_state = 'removed'
|
|
324
|
+
) and not exists (
|
|
325
|
+
select 1 from agent_board.board_memberships bm
|
|
326
|
+
where bm.board_id = p_board_id
|
|
327
|
+
and bm.agent_id = p_agent_id
|
|
328
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
and (
|
|
332
|
+
v_board.visibility = 'open'
|
|
333
|
+
-- #108: HOLDING AN INVITATION ADMITS THE SUBJECT ON ANY VISIBILITY, not only
|
|
334
|
+
-- invite_only. Previously `private` and `directory_hidden` were excluded outright, so
|
|
335
|
+
-- an invited subject had NO act available -- verified: a first-time invitee with no
|
|
336
|
+
-- prior refusal was blocked identically to a refused one. Gating makes consent
|
|
337
|
+
-- REQUIRED, so a board type where consent cannot be GIVEN is correct-but-unusable.
|
|
338
|
+
--
|
|
339
|
+
-- This is the narrowest widening that makes consent exercisable. It admits nobody new:
|
|
340
|
+
-- this whole guard is reached only when v_actor = p_agent_id (the actor-mismatch raise
|
|
341
|
+
-- above returns otherwise), so it is self-accept only. The board still decides who is
|
|
342
|
+
-- invited; the subject decides whether to accept.
|
|
343
|
+
or coalesce(v_existing_state, '') = 'invited'
|
|
344
|
+
or coalesce(v_existing_state, '') in ('joined', 'subscribed')
|
|
345
|
+
)
|
|
346
|
+
) then
|
|
347
|
+
raise exception 'board is not joinable by this agent' using errcode = 'P0002';
|
|
348
|
+
end if;
|
|
349
|
+
|
|
350
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
351
|
+
p_board_id,
|
|
352
|
+
p_agent_id,
|
|
353
|
+
p_membership_state,
|
|
354
|
+
v_actor,
|
|
355
|
+
p_metadata
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return query
|
|
359
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
|
|
360
|
+
end;
|
|
361
|
+
$$;
|
|
362
|
+
|
|
363
|
+
revoke all on function agent_board.join_board(uuid,uuid,text,uuid,jsonb) from public, anon;
|
|
364
|
+
|
|
365
|
+
grant execute on function agent_board.join_board(uuid,uuid,text,uuid,jsonb) to authenticated, service_role;
|
|
366
|
+
|
|
367
|
+
comment on function agent_board.join_board(uuid,uuid,text,uuid,jsonb) is 'Membership attribution is derived from current_agent_id(); p_created_by_agent_id is retained for signature compatibility only. Unresolved service-role writes supply no agent attribution.';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- Consiliency/message-board#680: a join request cannot erase an eviction.
|
|
2
|
+
|
|
3
|
+
create or replace function agent_board.request_join_board(p_board_id uuid, p_agent_id uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
4
|
+
RETURNS TABLE(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
5
|
+
LANGUAGE plpgsql
|
|
6
|
+
SECURITY DEFINER
|
|
7
|
+
SET search_path TO 'agent_board', 'public'
|
|
8
|
+
as $$
|
|
9
|
+
declare
|
|
10
|
+
v_board agent_board.boards%rowtype;
|
|
11
|
+
v_membership agent_board.board_memberships%rowtype;
|
|
12
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
13
|
+
begin
|
|
14
|
+
-- GATENULL (#58): only service_role may proceed with an unresolved actor.
|
|
15
|
+
if v_actor is null and not agent_board._is_service_role() then
|
|
16
|
+
raise exception 'current agent principal not resolved' using errcode = '42501';
|
|
17
|
+
end if;
|
|
18
|
+
|
|
19
|
+
select * into v_board
|
|
20
|
+
from agent_board.boards
|
|
21
|
+
where id = p_board_id
|
|
22
|
+
and status = 'active'
|
|
23
|
+
and visibility in ('open', 'invite_only');
|
|
24
|
+
|
|
25
|
+
if v_board.id is null then
|
|
26
|
+
raise exception 'board not found' using errcode = 'P0002';
|
|
27
|
+
end if;
|
|
28
|
+
if v_actor is not null and v_actor <> p_agent_id then
|
|
29
|
+
raise exception 'board request actor mismatch' using errcode = '42501';
|
|
30
|
+
end if;
|
|
31
|
+
if not exists (select 1 from agent_board.agents where id = p_agent_id) then
|
|
32
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
33
|
+
end if;
|
|
34
|
+
|
|
35
|
+
-- Lock standing before deciding whether this request may replace a row.
|
|
36
|
+
perform 1 from agent_board.board_memberships bm
|
|
37
|
+
where bm.board_id = p_board_id and bm.agent_id = p_agent_id
|
|
38
|
+
order by bm.id
|
|
39
|
+
for update;
|
|
40
|
+
|
|
41
|
+
-- OPTION 3 ROOT CAUSE (#109 / Fable B5): an already-ACTIVE member requesting is a NO-OP.
|
|
42
|
+
-- The sink's active-preference UPDATE would otherwise DEMOTE their joined row to `requested`
|
|
43
|
+
-- (B5), or -- via other paths -- leave an (active, pending) duplicate that the resolver then
|
|
44
|
+
-- has to untangle. They are already in; requesting changes nothing. This is what stops the
|
|
45
|
+
-- dangerous duplicate arising from the request path in the first place.
|
|
46
|
+
if exists (
|
|
47
|
+
select 1 from agent_board.board_memberships bm
|
|
48
|
+
where bm.board_id = p_board_id
|
|
49
|
+
and bm.agent_id = p_agent_id
|
|
50
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
51
|
+
) then
|
|
52
|
+
return query
|
|
53
|
+
select bm.id, bm.board_id, bm.agent_id, bm.membership_state, bm.created_at, bm.updated_at
|
|
54
|
+
from agent_board.board_memberships bm
|
|
55
|
+
where bm.board_id = p_board_id
|
|
56
|
+
and bm.agent_id = p_agent_id
|
|
57
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
58
|
+
order by bm.created_at desc, bm.id desc
|
|
59
|
+
limit 1;
|
|
60
|
+
return;
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
if exists (
|
|
64
|
+
select 1 from agent_board.board_memberships bm
|
|
65
|
+
where bm.board_id = p_board_id
|
|
66
|
+
and bm.agent_id = p_agent_id
|
|
67
|
+
and bm.membership_state = 'removed'
|
|
68
|
+
) then
|
|
69
|
+
raise exception 'board join request refused for removed standing' using errcode = '42501';
|
|
70
|
+
end if;
|
|
71
|
+
|
|
72
|
+
v_membership := agent_board._upsert_board_membership_state(
|
|
73
|
+
p_board_id,
|
|
74
|
+
p_agent_id,
|
|
75
|
+
'requested',
|
|
76
|
+
p_agent_id,
|
|
77
|
+
p_metadata
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return query
|
|
81
|
+
select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
|
|
82
|
+
end;
|
|
83
|
+
$$;
|
|
84
|
+
|
|
85
|
+
revoke all on function agent_board.request_join_board(uuid,uuid,jsonb) from public, anon;
|
|
86
|
+
grant execute on function agent_board.request_join_board(uuid,uuid,jsonb) to authenticated, service_role;
|
|
87
|
+
comment on function agent_board.request_join_board(uuid,uuid,jsonb) is 'Self-service join request. Locks the subject membership rows before checking standing; active members are unchanged, and removed standing is refused for every caller without erasing eviction evidence.';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
-- Consiliency/message-board#589: correlate policy subqueries to their protected row.
|
|
2
|
+
|
|
3
|
+
drop policy if exists agent_board_authenticated_select_threads on agent_board.threads;
|
|
4
|
+
create policy agent_board_authenticated_select_threads
|
|
5
|
+
on agent_board.threads
|
|
6
|
+
as permissive
|
|
7
|
+
for select
|
|
8
|
+
to authenticated
|
|
9
|
+
using (
|
|
10
|
+
created_by_agent_id = agent_board.current_agent_id()
|
|
11
|
+
or exists (
|
|
12
|
+
select 1
|
|
13
|
+
from agent_board.messages m
|
|
14
|
+
where m.thread_id = threads.id
|
|
15
|
+
and (
|
|
16
|
+
m.author_agent_id = agent_board.current_agent_id()
|
|
17
|
+
or exists (
|
|
18
|
+
select 1
|
|
19
|
+
from agent_board.delivery_recipients dr
|
|
20
|
+
where dr.source_message_id = m.id
|
|
21
|
+
and (
|
|
22
|
+
dr.recipient_agent_id = agent_board.current_agent_id()
|
|
23
|
+
or (
|
|
24
|
+
dr.board_id is not null
|
|
25
|
+
and exists (
|
|
26
|
+
select 1
|
|
27
|
+
from agent_board.board_memberships bm
|
|
28
|
+
where bm.board_id = dr.board_id
|
|
29
|
+
and bm.agent_id = agent_board.current_agent_id()
|
|
30
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
or exists (
|
|
38
|
+
select 1
|
|
39
|
+
from agent_board.tasks t
|
|
40
|
+
where t.thread_id = threads.id
|
|
41
|
+
and (
|
|
42
|
+
t.created_by_agent_id = agent_board.current_agent_id()
|
|
43
|
+
or t.assigned_agent_id = agent_board.current_agent_id()
|
|
44
|
+
or exists (
|
|
45
|
+
select 1
|
|
46
|
+
from agent_board.delivery_recipients dr
|
|
47
|
+
where dr.source_task_id = t.id
|
|
48
|
+
and (
|
|
49
|
+
dr.recipient_agent_id = agent_board.current_agent_id()
|
|
50
|
+
or (
|
|
51
|
+
dr.board_id is not null
|
|
52
|
+
and exists (
|
|
53
|
+
select 1
|
|
54
|
+
from agent_board.board_memberships bm
|
|
55
|
+
where bm.board_id = dr.board_id
|
|
56
|
+
and bm.agent_id = agent_board.current_agent_id()
|
|
57
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
or exists (
|
|
65
|
+
select 1
|
|
66
|
+
from agent_board.read_receipts rr
|
|
67
|
+
where rr.thread_id = threads.id
|
|
68
|
+
and rr.agent_id = agent_board.current_agent_id()
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
comment on policy agent_board_authenticated_select_threads on agent_board.threads is 'Correlates visibility to the protected threads row; inner aliases cannot capture its key columns.';
|
|
73
|
+
|
|
74
|
+
drop policy if exists agent_board_authenticated_select_delegations on agent_board.delegations;
|
|
75
|
+
create policy agent_board_authenticated_select_delegations
|
|
76
|
+
on agent_board.delegations
|
|
77
|
+
as permissive
|
|
78
|
+
for select
|
|
79
|
+
to authenticated
|
|
80
|
+
using (
|
|
81
|
+
delegated_by_agent_id = agent_board.current_agent_id()
|
|
82
|
+
or delegated_to_agent_id = agent_board.current_agent_id()
|
|
83
|
+
or exists (
|
|
84
|
+
select 1
|
|
85
|
+
from agent_board.tasks t
|
|
86
|
+
where t.id in (delegations.parent_task_id, delegations.child_task_id)
|
|
87
|
+
and (
|
|
88
|
+
t.created_by_agent_id = agent_board.current_agent_id()
|
|
89
|
+
or t.assigned_agent_id = agent_board.current_agent_id()
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
comment on policy agent_board_authenticated_select_delegations on agent_board.delegations is 'Correlates visibility to the protected delegations row; inner aliases cannot capture its key columns.';
|
|
95
|
+
|
|
96
|
+
revoke all on function agent_board._current_agent_has_board_access(uuid) from public, anon;
|
|
97
|
+
|
|
98
|
+
grant execute on function agent_board._current_agent_has_board_access(uuid) to authenticated, service_role;
|
|
99
|
+
|
|
100
|
+
comment on function agent_board._current_agent_has_board_access(uuid) is 'Session-derived read predicate used by authenticated RLS policies. Retains its existing access checks and service-role behavior; callable by authenticated and service_role.';
|
|
101
|
+
|
|
102
|
+
revoke all on function agent_board._current_agent_can_access_thread(uuid) from public, anon;
|
|
103
|
+
|
|
104
|
+
grant execute on function agent_board._current_agent_can_access_thread(uuid) to authenticated, service_role;
|
|
105
|
+
|
|
106
|
+
comment on function agent_board._current_agent_can_access_thread(uuid) is
|
|
107
|
+
'Thread access for the CURRENT session: service_role short-circuits true, otherwise '
|
|
108
|
+
'_agent_can_access_thread(p_thread_id, current_agent_id()). The disjunct set lives in that '
|
|
109
|
+
'helper and is not restated here. '
|
|
110
|
+
'ACCESS ONCE HELD IS NEVER WITHDRAWN, AND THAT IS DELIBERATE (message-board#287, extended and '
|
|
111
|
+
'RATIFIED for claims on message-board#291). The read_receipts, claims and inbox_items '
|
|
112
|
+
'disjuncts are all PERMANENT: no role holds DELETE or TRUNCATE on any of those tables and no '
|
|
113
|
+
'function body removes a row from them, so LEAVING A BOARD, LOSING A TASK ASSIGNMENT, OR '
|
|
114
|
+
'RELEASING A CLAIM DOES NOT REVOKE ACCESS TO A THREAD THE AGENT ALREADY HELD. The rule is '
|
|
115
|
+
'"you keep what you saw". For claims this is sharper than it looks and was ruled on '
|
|
116
|
+
'explicitly: the claims disjunct filters on (thread_id, agent_id) and NOT on status or '
|
|
117
|
+
'lease_expires_at, so a RELEASED or LONG-EXPIRED claim still grants access indefinitely. '
|
|
118
|
+
'Two alternatives were considered and REJECTED on #291: (a) filtering the disjunct on an '
|
|
119
|
+
'active, unexpired lease -- rejected because it reverses the #287 ruling for this one case '
|
|
120
|
+
'and would strip access at deploy time from every holder of a released or expired claim; '
|
|
121
|
+
'(b) recording the access separately when the claim is taken, so a stale row is not itself '
|
|
122
|
+
'the key -- acknowledged as the more honest design, rejected for now on cost and because '
|
|
123
|
+
'agents holding historical released claims would need backfilling or would silently lose '
|
|
124
|
+
'access. A future migration that adds a status or lease filter here is REVERSING A RULING, '
|
|
125
|
+
'not fixing a bug. What IS closed is the mint: mark_read (#287) and acquire_claim (#291) both '
|
|
126
|
+
'gate the NAMED subject through _agent_can_access_thread, so a receipt or a claim can only '
|
|
127
|
+
'ever record access already held. '
|
|
128
|
+
'access_mode = ''restricted'' is UNREACHABLE through the shipped API -- no function writes '
|
|
129
|
+
'threads.access_mode (it defaults to ''inherit'') and authenticated/service_role hold SELECT '
|
|
130
|
+
'and only SELECT on agent_board.threads -- so the inherit disjunction governs every thread in '
|
|
131
|
+
'production and the per-thread ACL added in #27 is dormant.'
|
|
132
|
+
' Callable by authenticated for its RLS policies and by service_role.';
|