@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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
-- DELIVERYWRITEREXEC pgTAP (#717 F1, migration 20261104180000).
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT. `_upsert_delivery_recipient` was EXECUTE-able by service_role (through the 2026-05
|
|
4
|
+
-- schema-wide `grant execute on all functions in schema agent_board to service_role`). Called
|
|
5
|
+
-- directly, nothing in front of it checks authorship or addressing, so a service_role caller could
|
|
6
|
+
-- deliver an existing private DM to a board member ('board' mode) or to any agent ('direct' mode),
|
|
7
|
+
-- and the DM kept its original author. Four sibling sinks had the same grant.
|
|
8
|
+
--
|
|
9
|
+
-- THE CLASS COMES FROM THE CATALOG (AGENTS.md rule 1), not from that list:
|
|
10
|
+
-- * the SINK RELATIONS are agent_board.messages plus every relation that reaches it by foreign
|
|
11
|
+
-- key (DWX-1 pins that closure by name, so a new referencing relation fails and names itself);
|
|
12
|
+
-- three closure members are NOT message/delivery/inbox sinks and are excluded, each for a
|
|
13
|
+
-- stated reason: artifacts (the artifact domain; add_artifact is its own public route),
|
|
14
|
+
-- signature_evidence and signature_replay_state (the signed-write evidence ledger, whose
|
|
15
|
+
-- writers are classed by agent_board_write_enforcement_class / signed_* tests);
|
|
16
|
+
-- * plus the two thread columns a message write moves: threads.last_message_sequence and
|
|
17
|
+
-- threads.board_id;
|
|
18
|
+
-- * a CLASS MEMBER is any agent_board routine whose body writes a sink, or calls a member,
|
|
19
|
+
-- transitively. Name-based (`call_re`), so an overload of a member's name is a member too;
|
|
20
|
+
-- * the API ROLES are the roles `authenticator` (PostgREST) can switch to, read from
|
|
21
|
+
-- pg_auth_members, unioned with the four this schema grants to; DWX-0 asserts the set is not
|
|
22
|
+
-- vacuous.
|
|
23
|
+
-- DWX-2: the API-executable members are EXACTLY the named exemptions. A new route, a new overload,
|
|
24
|
+
-- or a re-grant of an internal helper fails it and appears in `have`.
|
|
25
|
+
-- Each exemption is asserted POSITIVELY, by kind:
|
|
26
|
+
-- session_send DWX-S1..S5 an authenticated caller naming another author is refused (42501)
|
|
27
|
+
-- wrapper DWX-4a writes no sink itself and calls its same-named overload
|
|
28
|
+
-- signed DWX-4b service-role only, actor from _signed_write_actor_id, no direct
|
|
29
|
+
-- sink write, calls its unsigned twin
|
|
30
|
+
-- callback_worker DWX-4c service-role only; transitively moves outbox events by UPDATE only (its own
|
|
31
|
+
-- callback_delivery_attempts ledger is the one sink it inserts
|
|
32
|
+
-- into) and never writes messages/delivery_recipients/inbox_items
|
|
33
|
+
-- recipient_state DWX-4d writes only inbox_items, only by UPDATE, and none of its
|
|
34
|
+
-- addressing, visibility or content columns, including reachable helpers
|
|
35
|
+
-- Behaviour: service_role direct calls of the five helpers are refused (DWX-B1..B6) and the DM
|
|
36
|
+
-- stays private (DWX-B7..B9); the public routes keep working for service_role and authenticated
|
|
37
|
+
-- callers, with the author and metadata intact and private recipients private (DWX-P*).
|
|
38
|
+
--
|
|
39
|
+
-- Mutation controls: see plans/evidence/717-f1/mutations.md (each run counts all 39 assertions).
|
|
40
|
+
--
|
|
41
|
+
-- HAND-COUNTED plan: DWX-0, 1, 2, 3a-e, 4a-d (12) + P1-3 (3) + B1-10 (10) + P4-12 (9) + S1-5 (5) = 39.
|
|
42
|
+
create extension if not exists pgtap with schema extensions;
|
|
43
|
+
set search_path = extensions, public, agent_board;
|
|
44
|
+
|
|
45
|
+
begin;
|
|
46
|
+
|
|
47
|
+
select plan(39);
|
|
48
|
+
|
|
49
|
+
-- ---------------------------------------------------------------------------
|
|
50
|
+
-- The class.
|
|
51
|
+
-- ---------------------------------------------------------------------------
|
|
52
|
+
create temp table dwx_api_roles on commit drop as
|
|
53
|
+
select r.rolname::text as rolname
|
|
54
|
+
from pg_auth_members a
|
|
55
|
+
join pg_roles r on r.oid = a.roleid
|
|
56
|
+
join pg_roles m on m.oid = a.member
|
|
57
|
+
where m.rolname = 'authenticator'
|
|
58
|
+
union
|
|
59
|
+
select rolname::text from pg_roles
|
|
60
|
+
where rolname in ('anon', 'authenticated', 'service_role', 'agent_board_delegate');
|
|
61
|
+
|
|
62
|
+
select ok(
|
|
63
|
+
(select count(*) filter (where rolname in ('service_role', 'authenticated')) = 2 from dwx_api_roles),
|
|
64
|
+
'DWX-0: the API-role set includes service_role and authenticated');
|
|
65
|
+
|
|
66
|
+
create temp table dwx_closure on commit drop as
|
|
67
|
+
with recursive rel(oid) as (
|
|
68
|
+
select 'agent_board.messages'::regclass::oid
|
|
69
|
+
union
|
|
70
|
+
select c.conrelid from pg_constraint c join rel on c.confrelid = rel.oid where c.contype = 'f')
|
|
71
|
+
select oid::regclass::text as rel, c.relname::text as relname
|
|
72
|
+
from rel join pg_class c using (oid);
|
|
73
|
+
|
|
74
|
+
select is(
|
|
75
|
+
(select string_agg(rel, ', ' order by rel collate "C") from dwx_closure),
|
|
76
|
+
(select string_agg(r::regclass::text, ', ' order by r::regclass::text collate "C")
|
|
77
|
+
from unnest(array['agent_board.artifacts', 'agent_board.callback_delivery_attempts', 'agent_board.callback_outbox_events',
|
|
78
|
+
'agent_board.delivery_recipients', 'agent_board.inbox_items', 'agent_board.messages',
|
|
79
|
+
'agent_board.signature_evidence', 'agent_board.signature_replay_state']) r),
|
|
80
|
+
'DWX-1: the relations reaching agent_board.messages by foreign key are exactly the classified set');
|
|
81
|
+
|
|
82
|
+
create temp table dwx_sinks on commit drop as
|
|
83
|
+
select relname from dwx_closure
|
|
84
|
+
where relname not in ('artifacts', 'signature_evidence', 'signature_replay_state');
|
|
85
|
+
|
|
86
|
+
create temp table dwx_fns on commit drop as
|
|
87
|
+
select p.oid, p.proname::text collate "C" as n, agent_board_test.stripped_body(p.oid) as b
|
|
88
|
+
from pg_proc p
|
|
89
|
+
where p.pronamespace = 'agent_board'::regnamespace and p.prokind in ('f', 'p');
|
|
90
|
+
|
|
91
|
+
create temp table dwx_class on commit drop as
|
|
92
|
+
with recursive direct as (
|
|
93
|
+
select oid, n from dwx_fns
|
|
94
|
+
where b ~ agent_board_test.writes_any_re((select array_agg(relname) from dwx_sinks))
|
|
95
|
+
or b ~ agent_board_test.col_write_re('threads', 'last_message_sequence')
|
|
96
|
+
or b ~ agent_board_test.col_write_re('threads', 'board_id')),
|
|
97
|
+
clos(oid, n) as (
|
|
98
|
+
select oid, n from direct
|
|
99
|
+
union
|
|
100
|
+
select f.oid, f.n from dwx_fns f join clos c on f.oid <> c.oid and f.b ~ agent_board_test.call_re(c.n))
|
|
101
|
+
select distinct oid, n,
|
|
102
|
+
(select string_agg(r.rolname, ',' order by r.rolname) from dwx_api_roles r
|
|
103
|
+
where has_function_privilege(r.rolname, clos.oid, 'execute')) as api_roles
|
|
104
|
+
from clos;
|
|
105
|
+
|
|
106
|
+
create temp table dwx_exempt(sig text primary key, kind text not null) on commit drop;
|
|
107
|
+
insert into dwx_exempt values
|
|
108
|
+
('agent_board.post_message(uuid,uuid,text,text,uuid,jsonb)', 'session_send'),
|
|
109
|
+
('agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'session_send'),
|
|
110
|
+
('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'session_send'),
|
|
111
|
+
('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)', 'session_send'),
|
|
112
|
+
('agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)', 'session_send'),
|
|
113
|
+
('agent_board.post_board_message(uuid,uuid,text,text,text,text,text,text,text,jsonb)', 'wrapper'),
|
|
114
|
+
('agent_board.send_direct_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)', 'wrapper'),
|
|
115
|
+
('agent_board.send_multicast_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)', 'wrapper'),
|
|
116
|
+
('agent_board.create_tasking_delivery(uuid,uuid,text,text,jsonb,text,text,text,text,text,jsonb)', 'wrapper'),
|
|
117
|
+
('agent_board.signed_post_board_message(jsonb,jsonb)', 'signed'),
|
|
118
|
+
('agent_board.signed_send_direct_message(jsonb,jsonb)', 'signed'),
|
|
119
|
+
('agent_board.signed_send_multicast_message(jsonb,jsonb)', 'signed'),
|
|
120
|
+
('agent_board.signed_create_tasking_delivery(jsonb,jsonb)', 'signed'),
|
|
121
|
+
('agent_board.claim_callback_outbox_events(integer)', 'callback_worker'),
|
|
122
|
+
('agent_board.record_callback_delivery_success(uuid,uuid,integer,jsonb)', 'callback_worker'),
|
|
123
|
+
('agent_board.record_callback_delivery_retry(uuid,uuid,integer,text,timestamp with time zone,jsonb)', 'callback_worker'),
|
|
124
|
+
('agent_board.record_callback_delivery_terminal_failure(uuid,uuid,integer,text,jsonb)', 'callback_worker'),
|
|
125
|
+
('agent_board.disable_callback_delivery_endpoint(uuid,uuid,text,text,jsonb)', 'callback_worker'),
|
|
126
|
+
('agent_board.update_inbox_item_state(uuid,uuid,text,jsonb)', 'recipient_state');
|
|
127
|
+
|
|
128
|
+
select is(
|
|
129
|
+
(select string_agg(oid::regprocedure::text, ', ' order by oid::regprocedure::text collate "C")
|
|
130
|
+
from dwx_class where api_roles is not null),
|
|
131
|
+
(select string_agg(sig::regprocedure::text, ', ' order by sig::regprocedure::text collate "C") from dwx_exempt),
|
|
132
|
+
'DWX-2: the API-executable message/delivery/inbox writers are exactly the named exemptions');
|
|
133
|
+
|
|
134
|
+
-- The five helpers #717 F1 revoked, named one per assertion so a re-grant names itself.
|
|
135
|
+
select is((select api_roles from dwx_class where oid = 'agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)'::regprocedure),
|
|
136
|
+
null, 'DWX-3a: _upsert_delivery_recipient is a class member with no API-role EXECUTE');
|
|
137
|
+
select is((select api_roles from dwx_class where oid = 'agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)'::regprocedure),
|
|
138
|
+
null, 'DWX-3b: _upsert_inbox_item is a class member with no API-role EXECUTE');
|
|
139
|
+
select is((select api_roles from dwx_class where oid = 'agent_board._queue_inbox_callbacks(uuid,text)'::regprocedure),
|
|
140
|
+
null, 'DWX-3c: _queue_inbox_callbacks is a class member with no API-role EXECUTE');
|
|
141
|
+
select is((select api_roles from dwx_class where oid = 'agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb)'::regprocedure),
|
|
142
|
+
null, 'DWX-3d: _queue_board_post_callbacks is a class member with no API-role EXECUTE');
|
|
143
|
+
select is((select api_roles from dwx_class where oid = 'agent_board._next_thread_message_sequence(uuid)'::regprocedure),
|
|
144
|
+
null, 'DWX-3e: _next_thread_message_sequence is a class member with no API-role EXECUTE');
|
|
145
|
+
|
|
146
|
+
-- Resolve every reachable agent_board routine for restricted state-only exemptions.
|
|
147
|
+
-- UNION terminates recursive cycles; name-based edges include every overload, just as DWX-2.
|
|
148
|
+
create temp table dwx_state_reach on commit drop as
|
|
149
|
+
with recursive reach(sig, kind, oid) as (
|
|
150
|
+
select sig, kind, sig::regprocedure::oid from dwx_exempt
|
|
151
|
+
where kind in ('callback_worker', 'recipient_state')
|
|
152
|
+
union
|
|
153
|
+
select r.sig, r.kind, callee.oid from reach r
|
|
154
|
+
join dwx_fns caller on caller.oid = r.oid
|
|
155
|
+
join dwx_fns callee on caller.b ~ agent_board_test.call_re(callee.n))
|
|
156
|
+
select * from reach;
|
|
157
|
+
|
|
158
|
+
-- Exemptions, asserted positively. Each yields the violating signatures (NULL = none).
|
|
159
|
+
select is(
|
|
160
|
+
(select string_agg(e.sig, ', ' order by e.sig) from dwx_exempt e join dwx_fns f on f.oid = e.sig::regprocedure
|
|
161
|
+
where e.kind = 'wrapper'
|
|
162
|
+
and (f.b ~ agent_board_test.writes_any_re((select array_agg(relname) from dwx_sinks))
|
|
163
|
+
or not f.b ~ agent_board_test.call_re(f.n))),
|
|
164
|
+
null, 'DWX-4a: each descriptor overload writes no sink itself and calls its same-named overload');
|
|
165
|
+
|
|
166
|
+
select is(
|
|
167
|
+
(select string_agg(e.sig, ', ' order by e.sig) from dwx_exempt e join dwx_fns f on f.oid = e.sig::regprocedure
|
|
168
|
+
where e.kind = 'signed'
|
|
169
|
+
and (exists (select 1 from dwx_api_roles r where r.rolname <> 'service_role'
|
|
170
|
+
and has_function_privilege(r.rolname, f.oid, 'execute'))
|
|
171
|
+
or not f.b ~ agent_board_test.call_re('_signed_write_actor_id')
|
|
172
|
+
or f.b ~ agent_board_test.writes_any_re((select array_agg(relname) from dwx_sinks))
|
|
173
|
+
or not f.b ~ agent_board_test.call_re(substr(f.n, length('signed_') + 1)))),
|
|
174
|
+
null, 'DWX-4b: each signed route is service-role only, takes its actor from the gateway, writes no sink itself and calls its unsigned twin');
|
|
175
|
+
|
|
176
|
+
select is(
|
|
177
|
+
(select string_agg(e.sig || ' -> ' || f.oid::regprocedure::text, ', ' order by e.sig, f.oid::regprocedure::text)
|
|
178
|
+
from dwx_state_reach e join dwx_fns f on f.oid = e.oid
|
|
179
|
+
where e.kind = 'callback_worker'
|
|
180
|
+
and ((e.oid = e.sig::regprocedure and exists (select 1 from dwx_api_roles r where r.rolname <> 'service_role'
|
|
181
|
+
and has_function_privilege(r.rolname, f.oid, 'execute')))
|
|
182
|
+
or f.b ~ agent_board_test.stmt_re('insert into|merge into|delete from', 'callback_outbox_events')
|
|
183
|
+
or f.b ~ agent_board_test.writes_any_re(array['messages', 'delivery_recipients', 'inbox_items']))),
|
|
184
|
+
null, 'DWX-4c: each callback-worker RPC is service-role only and transitively never inserts or deletes an outbox event or writes a message, delivery or inbox row');
|
|
185
|
+
|
|
186
|
+
select is(
|
|
187
|
+
(select string_agg(e.sig || ' -> ' || f.oid::regprocedure::text, ', ' order by e.sig, f.oid::regprocedure::text)
|
|
188
|
+
from dwx_state_reach e join dwx_fns f on f.oid = e.oid
|
|
189
|
+
where e.kind = 'recipient_state'
|
|
190
|
+
and (f.b ~ agent_board_test.writes_any_re(array['messages', 'delivery_recipients'])
|
|
191
|
+
-- The entry itself only updates inbox state; its callback helpers may enqueue callbacks.
|
|
192
|
+
or (e.oid = e.sig::regprocedure and f.b ~ agent_board_test.writes_any_re(array['callback_outbox_events', 'callback_delivery_attempts']))
|
|
193
|
+
or f.b ~ agent_board_test.stmt_re('insert into|merge into|delete from', 'inbox_items')
|
|
194
|
+
or exists (select 1 from unnest(array['delivery_recipient_id', 'recipient_agent_id', 'board_id', 'delivery_mode',
|
|
195
|
+
'source_kind', 'source_message_id', 'source_task_id', 'source_delegation_id',
|
|
196
|
+
'subject', 'preview', 'priority', 'visible_in_inbox', 'visible_in_board_feed',
|
|
197
|
+
'fetch_descriptor']) c
|
|
198
|
+
where f.b ~ agent_board_test.col_write_re('inbox_items', c)))),
|
|
199
|
+
null, 'DWX-4d: recipient-state RPC and reachable helpers never write messages or deliveries, insert/delete inbox rows, or change inbox addressing, visibility or content');
|
|
200
|
+
|
|
201
|
+
-- ---------------------------------------------------------------------------
|
|
202
|
+
-- Fixture: A (owner), B and C (members) on a private board; X an agent outside it;
|
|
203
|
+
-- thread T already stamped to the board.
|
|
204
|
+
-- ---------------------------------------------------------------------------
|
|
205
|
+
create temp table dwx(a uuid, b uuid, c uuid, x uuid, ua uuid, ub uuid, uc uuid, ux uuid,
|
|
206
|
+
board uuid, thread uuid, c_membership uuid) on commit drop;
|
|
207
|
+
create temp table dwx_cap(tag text, id uuid) on commit drop;
|
|
208
|
+
grant select on dwx to authenticated, service_role;
|
|
209
|
+
grant select, insert on dwx_cap to authenticated, service_role;
|
|
210
|
+
|
|
211
|
+
do $$
|
|
212
|
+
declare
|
|
213
|
+
a uuid := gen_random_uuid(); b uuid := gen_random_uuid(); c uuid := gen_random_uuid(); x uuid := gen_random_uuid();
|
|
214
|
+
ua uuid; ub uuid; uc uuid; ux uuid; board uuid := gen_random_uuid(); t uuid := gen_random_uuid();
|
|
215
|
+
begin
|
|
216
|
+
ua := public.ensure_github_user('dwxa-'||a, 'dwxa-'||a||'@example.test', 'dwxa-'||a, 'A');
|
|
217
|
+
ub := public.ensure_github_user('dwxb-'||b, 'dwxb-'||b||'@example.test', 'dwxb-'||b, 'B');
|
|
218
|
+
uc := public.ensure_github_user('dwxc-'||c, 'dwxc-'||c||'@example.test', 'dwxc-'||c, 'C');
|
|
219
|
+
ux := public.ensure_github_user('dwxx-'||x, 'dwxx-'||x||'@example.test', 'dwxx-'||x, 'X');
|
|
220
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id) values
|
|
221
|
+
(a, 'agent', 'dwxa-'||a, 'A', 'active', ua, ua), (b, 'agent', 'dwxb-'||b, 'B', 'active', ub, ub),
|
|
222
|
+
(c, 'agent', 'dwxc-'||c, 'C', 'active', uc, uc), (x, 'agent', 'dwxx-'||x, 'X', 'active', ux, ux);
|
|
223
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility) values (board, 'dwx-'||board, 'dwx', a, 'private');
|
|
224
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role) values
|
|
225
|
+
(board, a, 'joined', 'owner'), (board, b, 'joined', 'member'), (board, c, 'joined', 'member');
|
|
226
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id) values (t, 'coordination', a, 'dwx', board);
|
|
227
|
+
-- Live callback subscriptions, so a forged inbox or board-post callback would leave an outbox row
|
|
228
|
+
-- for DWX-B10 to see: B on its own inbox, C on the board feed.
|
|
229
|
+
insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status) values
|
|
230
|
+
(b, b, 'https://example.test/dwx-b', 'verified'), (c, c, 'https://example.test/dwx-c', 'verified');
|
|
231
|
+
insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category) values
|
|
232
|
+
(b, b, 'own_inbox', 'inbox.item.delivered');
|
|
233
|
+
insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category, board_id, board_membership_id)
|
|
234
|
+
select c, c, 'board', 'board.post.delivered', board, bm.id from agent_board.board_memberships bm where bm.board_id = board and bm.agent_id = c;
|
|
235
|
+
insert into dwx values (a, b, c, x, ua, ub, uc, ux, board, t,
|
|
236
|
+
(select id from agent_board.board_memberships where board_id = board and agent_id = c));
|
|
237
|
+
end $$;
|
|
238
|
+
|
|
239
|
+
-- A private DM from A to B on the board thread, as an authenticated session.
|
|
240
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
241
|
+
select set_config('request.jwt.claim.sub', (select ua::text from dwx), true);
|
|
242
|
+
set local role authenticated;
|
|
243
|
+
select lives_ok($$insert into dwx_cap select 'dm', message_id from agent_board.send_direct_message(
|
|
244
|
+
(select thread from dwx), null, (select b from dwx), 'dwx dm', 'dwx secret', p_metadata => '{"dwx":"dm"}')$$,
|
|
245
|
+
'DWX-P1: an authenticated author sends a private DM on a board thread');
|
|
246
|
+
reset role;
|
|
247
|
+
|
|
248
|
+
select set_config('request.jwt.claim.sub', (select ub::text from dwx), true);
|
|
249
|
+
set local role authenticated;
|
|
250
|
+
select is((select count(*) from agent_board.messages where id = (select id from dwx_cap where tag = 'dm')), 1::bigint,
|
|
251
|
+
'DWX-P2: the addressed recipient reads the DM');
|
|
252
|
+
reset role;
|
|
253
|
+
|
|
254
|
+
select ok((select author_agent_id = (select a from dwx) and metadata ->> 'dwx' = 'dm'
|
|
255
|
+
from agent_board.messages where id = (select id from dwx_cap where tag = 'dm')),
|
|
256
|
+
'DWX-P3: the DM carries the session author and the caller metadata');
|
|
257
|
+
|
|
258
|
+
-- What the refused calls must leave untouched: the DM's inbox and outbox rows and the sequence.
|
|
259
|
+
create temp table dwx_pre on commit drop as
|
|
260
|
+
select (select count(*) || ':' || string_agg(subject || '|' || preview || '|' || priority, ',') from agent_board.inbox_items
|
|
261
|
+
where source_message_id = (select id from dwx_cap where tag = 'dm')) as inbox,
|
|
262
|
+
(select count(*) from agent_board.callback_outbox_events where source_message_id = (select id from dwx_cap where tag = 'dm')) as outbox,
|
|
263
|
+
(select last_message_sequence from agent_board.threads where id = (select thread from dwx)) as seq;
|
|
264
|
+
grant select on dwx_pre to service_role;
|
|
265
|
+
|
|
266
|
+
-- service_role calls the internal sinks directly: refused, every one.
|
|
267
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
268
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
269
|
+
set local role service_role;
|
|
270
|
+
select throws_ok($$select agent_board._upsert_delivery_recipient('board', (select c from dwx), 'message',
|
|
271
|
+
(select id from dwx_cap where tag = 'dm'), null, null, (select board from dwx), (select c_membership from dwx), '{}'::jsonb)$$,
|
|
272
|
+
'42501', null, 'DWX-B1: service_role cannot publish a DM to a board member through _upsert_delivery_recipient');
|
|
273
|
+
select throws_ok($$select agent_board._upsert_delivery_recipient('direct', (select x from dwx), 'message',
|
|
274
|
+
(select id from dwx_cap where tag = 'dm'), null, null, null, null, '{}'::jsonb)$$,
|
|
275
|
+
'42501', null, 'DWX-B2: service_role cannot re-address a DM to an arbitrary agent through _upsert_delivery_recipient');
|
|
276
|
+
select throws_ok($$select agent_board._upsert_inbox_item(
|
|
277
|
+
(select d.id from agent_board.delivery_recipients d where d.source_message_id = (select id from dwx_cap where tag = 'dm')
|
|
278
|
+
and d.recipient_agent_id = (select b from dwx)),
|
|
279
|
+
(select b from dwx), null, 'direct', 'message', (select id from dwx_cap where tag = 'dm'), null, null,
|
|
280
|
+
'forged', 'forged', 'high', true, false, '{}'::jsonb, '{}'::jsonb)$$,
|
|
281
|
+
'42501', null, 'DWX-B3: service_role cannot call _upsert_inbox_item');
|
|
282
|
+
select throws_ok($$select agent_board._queue_inbox_callbacks(
|
|
283
|
+
(select i.id from agent_board.inbox_items i where i.source_message_id = (select id from dwx_cap where tag = 'dm')
|
|
284
|
+
and i.recipient_agent_id = (select b from dwx)), 'inbox.item.delivered')$$,
|
|
285
|
+
'42501', null, 'DWX-B4: service_role cannot call _queue_inbox_callbacks');
|
|
286
|
+
select throws_ok($$select agent_board._queue_board_post_callbacks((select board from dwx), (select id from dwx_cap where tag = 'dm'),
|
|
287
|
+
'forged', 'forged', 'medium', '{}'::jsonb)$$,
|
|
288
|
+
'42501', null, 'DWX-B5: service_role cannot call _queue_board_post_callbacks');
|
|
289
|
+
select throws_ok($$select agent_board._next_thread_message_sequence((select thread from dwx))$$,
|
|
290
|
+
'42501', null, 'DWX-B6: service_role cannot call _next_thread_message_sequence');
|
|
291
|
+
reset role;
|
|
292
|
+
|
|
293
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
294
|
+
select set_config('request.jwt.claim.sub', (select uc::text from dwx), true);
|
|
295
|
+
set local role authenticated;
|
|
296
|
+
select is((select count(*) from agent_board.messages where id = (select id from dwx_cap where tag = 'dm')), 0::bigint,
|
|
297
|
+
'DWX-B7: a board member who was not addressed still cannot read the DM');
|
|
298
|
+
reset role;
|
|
299
|
+
select set_config('request.jwt.claim.sub', (select ux::text from dwx), true);
|
|
300
|
+
set local role authenticated;
|
|
301
|
+
select is((select count(*) from agent_board.messages where id = (select id from dwx_cap where tag = 'dm')), 0::bigint,
|
|
302
|
+
'DWX-B8: an agent outside the board still cannot read the DM');
|
|
303
|
+
reset role;
|
|
304
|
+
select is(
|
|
305
|
+
(select string_agg(d.delivery_mode || ':' || (d.recipient_agent_id = (select b from dwx))::text || ':' || coalesce(d.board_id::text, 'null'), ',')
|
|
306
|
+
from agent_board.delivery_recipients d where d.source_message_id = (select id from dwx_cap where tag = 'dm')),
|
|
307
|
+
'direct:true:null',
|
|
308
|
+
'DWX-B9: the DM has exactly its one direct, board-less delivery to B');
|
|
309
|
+
select is(
|
|
310
|
+
(select (select count(*) || ':' || string_agg(subject || '|' || preview || '|' || priority, ',') from agent_board.inbox_items
|
|
311
|
+
where source_message_id = (select id from dwx_cap where tag = 'dm'))::text || '/'
|
|
312
|
+
|| (select count(*) from agent_board.callback_outbox_events where source_message_id = (select id from dwx_cap where tag = 'dm'))::text || '/'
|
|
313
|
+
|| (select last_message_sequence from agent_board.threads where id = (select thread from dwx))::text),
|
|
314
|
+
(select inbox::text || '/' || outbox::text || '/' || seq::text from dwx_pre),
|
|
315
|
+
'DWX-B10: the refused calls left the DM inbox rows (count and content), its outbox events and the thread sequence unchanged');
|
|
316
|
+
|
|
317
|
+
-- The public routes keep working for service_role, reaching the sinks as the definer.
|
|
318
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
319
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
320
|
+
set local role service_role;
|
|
321
|
+
select lives_ok($$insert into dwx_cap select 'svc_dm', message_id from agent_board.send_direct_message(
|
|
322
|
+
(select thread from dwx), (select a from dwx), (select b from dwx), 'svc dm', 'svc secret', p_metadata => '{"dwx":"svc"}')$$,
|
|
323
|
+
'DWX-P4: service_role send_direct_message still delivers');
|
|
324
|
+
select lives_ok($$insert into dwx_cap select 'svc_board', message_id from agent_board.post_board_message(
|
|
325
|
+
(select thread from dwx), (select a from dwx), (select board from dwx), 'svc board', 'svc board body')$$,
|
|
326
|
+
'DWX-P5: service_role post_board_message still delivers');
|
|
327
|
+
select lives_ok($$insert into dwx_cap select 'svc_reply', message_id from agent_board.post_message(
|
|
328
|
+
(select thread from dwx), (select a from dwx), 'svc reply')$$,
|
|
329
|
+
'DWX-P6: service_role post_message still posts');
|
|
330
|
+
select lives_ok($$insert into dwx_cap select 'svc_multi', message_id from agent_board.send_multicast_message(
|
|
331
|
+
(select thread from dwx), (select a from dwx), array[(select b from dwx)], 'svc multi', 'svc multi body')$$,
|
|
332
|
+
'DWX-P7: service_role send_multicast_message still delivers');
|
|
333
|
+
select lives_ok($$insert into dwx_cap select 'svc_task', task_id from agent_board.create_tasking_delivery(
|
|
334
|
+
(select thread from dwx), (select a from dwx), 'svc task', p_recipient_agent_ids => array[(select b from dwx)])$$,
|
|
335
|
+
'DWX-P8: service_role create_tasking_delivery still delivers');
|
|
336
|
+
reset role;
|
|
337
|
+
|
|
338
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
339
|
+
select set_config('request.jwt.claim.sub', (select uc::text from dwx), true);
|
|
340
|
+
set local role authenticated;
|
|
341
|
+
select is((select count(*) from agent_board.messages where id = (select id from dwx_cap where tag = 'svc_board')), 1::bigint,
|
|
342
|
+
'DWX-P9: a board member reads the service_role board post');
|
|
343
|
+
select is((select count(*) from agent_board.messages where id in (select id from dwx_cap where tag in ('svc_dm', 'svc_multi'))), 0::bigint,
|
|
344
|
+
'DWX-P10: a board member cannot read the service_role DM or multicast it was not addressed in');
|
|
345
|
+
reset role;
|
|
346
|
+
select ok((select author_agent_id = (select a from dwx) and metadata ->> 'dwx' = 'svc'
|
|
347
|
+
from agent_board.messages where id = (select id from dwx_cap where tag = 'svc_dm')),
|
|
348
|
+
'DWX-P11: the service_role DM carries its stated author and the caller metadata');
|
|
349
|
+
select is(
|
|
350
|
+
(select string_agg(d.delivery_mode || ':' || (d.recipient_agent_id = (select b from dwx))::text || ':' || coalesce(d.board_id::text, 'null'), ',')
|
|
351
|
+
from agent_board.delivery_recipients d where d.source_message_id = (select id from dwx_cap where tag = 'svc_dm')),
|
|
352
|
+
'direct:true:null',
|
|
353
|
+
'DWX-P12: the service_role DM has exactly its one direct delivery to B');
|
|
354
|
+
|
|
355
|
+
-- session_send exemptions, positively: an authenticated caller cannot name another author.
|
|
356
|
+
select set_config('request.jwt.claim.sub', (select ua::text from dwx), true);
|
|
357
|
+
set local role authenticated;
|
|
358
|
+
select throws_ok($$select * from agent_board.post_message((select thread from dwx), (select b from dwx), 'spoof')$$,
|
|
359
|
+
'42501', null, 'DWX-S1: post_message refuses an author other than the session agent');
|
|
360
|
+
select throws_ok($$select * from agent_board.post_board_message((select thread from dwx), (select b from dwx), (select board from dwx), 'spoof', 'spoof')$$,
|
|
361
|
+
'42501', null, 'DWX-S2: post_board_message refuses an author other than the session agent');
|
|
362
|
+
select throws_ok($$select * from agent_board.send_direct_message((select thread from dwx), (select b from dwx), (select c from dwx), 'spoof', 'spoof')$$,
|
|
363
|
+
'42501', null, 'DWX-S3: send_direct_message refuses an author other than the session agent');
|
|
364
|
+
select throws_ok($$select * from agent_board.send_multicast_message((select thread from dwx), (select b from dwx), array[(select c from dwx)], 'spoof', 'spoof')$$,
|
|
365
|
+
'42501', null, 'DWX-S4: send_multicast_message refuses an author other than the session agent');
|
|
366
|
+
select throws_ok($$select * from agent_board.create_tasking_delivery((select thread from dwx), (select b from dwx), 'spoof', p_recipient_agent_ids => array[(select c from dwx)])$$,
|
|
367
|
+
'42501', null, 'DWX-S5: create_tasking_delivery refuses a creator other than the session agent');
|
|
368
|
+
reset role;
|
|
369
|
+
|
|
370
|
+
select * from finish();
|
|
371
|
+
rollback;
|
|
@@ -283,6 +283,8 @@ select is(
|
|
|
283
283
|
where a.rel_name = 'agent_board.agents'
|
|
284
284
|
and b.rel_name = 'agent_board.board_memberships') z),
|
|
285
285
|
array[
|
|
286
|
+
-- #661: the extracted fan-out retains its sink's existing FK lock order.
|
|
287
|
+
'_deliver_board_message: memberships(key share) then agents(key share) [unserialised]',
|
|
286
288
|
'_enroll_federated_identity_once: agents(key share) then memberships(key share) [unserialised]',
|
|
287
289
|
'_redeem_fleet_invite_for_agent: memberships(key share) then agents(key share) [unserialised]',
|
|
288
290
|
'_redeem_invite_for_agent: memberships(key share) then agents(key share) [unserialised]',
|
|
@@ -305,6 +307,7 @@ select is(
|
|
|
305
307
|
'mint_delegated_child: agents(update) then memberships(update) [advisory]',
|
|
306
308
|
'onboard_federated_fleet_agent: agents(update) then memberships(update) [advisory]',
|
|
307
309
|
'post_board_message: agents(key share) then memberships(key share) [unserialised]',
|
|
310
|
+
'post_message: agents(key share) then memberships(key share) [unserialised]',
|
|
308
311
|
'readmit_session_identity: agents(update) then memberships(update) [advisory]',
|
|
309
312
|
'redeem_invite_link: memberships(key share) then agents(key share) [unserialised]',
|
|
310
313
|
'request_join_board: memberships(update) then agents(key share) [unserialised]',
|
|
@@ -23,7 +23,7 @@ set search_path = extensions, public, agent_board;
|
|
|
23
23
|
|
|
24
24
|
begin;
|
|
25
25
|
|
|
26
|
-
select plan(
|
|
26
|
+
select plan(28);
|
|
27
27
|
|
|
28
28
|
-- ---------------------------------------------------------------------------
|
|
29
29
|
-- Phase 0: exactly one pg_proc row per name (a changed arg list would OVERLOAD, not replace)
|
|
@@ -340,13 +340,16 @@ select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
|
340
340
|
select throws_ok(
|
|
341
341
|
$$ select agent_board.resolve_board_request((select board from _f), (select nopend from _f3), 'joined', (select owner_agent from _f)) $$,
|
|
342
342
|
'P0002', null, 'NULLFIX: cannot resolve an agent with no pending request straight to joined');
|
|
343
|
-
--
|
|
343
|
+
-- Consiliency/message-board#668: resolution requires a resolved owner/admin identity.
|
|
344
344
|
reset role;
|
|
345
345
|
select set_config('request.jwt.claim.sub', '', true);
|
|
346
346
|
select set_config('request.jwt.claim.role', '', true);
|
|
347
|
-
select
|
|
347
|
+
select throws_ok(
|
|
348
348
|
$$ select agent_board.resolve_board_request((select board from _f), (select victim from _f), 'joined', (select owner_agent from _f)) $$,
|
|
349
|
-
'
|
|
349
|
+
'42501', null, 'unresolved service_role cannot resolve a pending membership');
|
|
350
|
+
select is((select membership_state from agent_board.board_memberships
|
|
351
|
+
where board_id=(select board from _f) and agent_id=(select victim from _f)),
|
|
352
|
+
'invited', 'service-role refusal preserves the pending membership');
|
|
350
353
|
|
|
351
354
|
select * from finish();
|
|
352
355
|
rollback;
|
|
@@ -370,9 +370,12 @@ select is(
|
|
|
370
370
|
insert into _ex values ('post_message(uuid, uuid, text, text, uuid, jsonb)');
|
|
371
371
|
-- 23
|
|
372
372
|
select is(
|
|
373
|
-
(select count(*)::int from agent_board.messages where thread_id::text = pg_temp.v('T1') and author_agent_id = pg_temp.u('I1.agent') and body = 'delegate reply'
|
|
373
|
+
(select count(*)::int from agent_board.messages m where m.thread_id::text = pg_temp.v('T1') and m.author_agent_id = pg_temp.u('I1.agent') and m.body = 'delegate reply'
|
|
374
|
+
and m.metadata->'delegate'->>'delegate' = 'true'
|
|
375
|
+
and exists(select 1 from agent_board.delivery_recipients d join agent_board.inbox_items i on i.delivery_recipient_id=d.id
|
|
376
|
+
where d.source_message_id=m.id and d.board_id=pg_temp.u('BX') and d.recipient_agent_id<>m.author_agent_id and i.visible_in_inbox)),
|
|
374
377
|
1,
|
|
375
|
-
'state: the reply exists, authored by the delegate');
|
|
378
|
+
'state: the reply exists, authored and marked by the delegate, with visible board recipient delivery (#661)');
|
|
376
379
|
-- 24 -- the ambient schema preflight
|
|
377
380
|
select is(
|
|
378
381
|
pg_temp.as_d_val($$ select version::text from agent_board.get_schema_version() $$),
|
|
@@ -206,6 +206,7 @@ declare
|
|
|
206
206
|
v_left_member uuid := gen_random_uuid();
|
|
207
207
|
v_outsider uuid := gen_random_uuid();
|
|
208
208
|
v_thread uuid := gen_random_uuid();
|
|
209
|
+
v_board_thread uuid := gen_random_uuid();
|
|
209
210
|
v_board uuid;
|
|
210
211
|
v_direct_endpoint uuid;
|
|
211
212
|
v_board_endpoint uuid;
|
|
@@ -226,7 +227,8 @@ begin
|
|
|
226
227
|
(v_outsider, 'agent', 'test_inboxrpc_outsider', 'Inbox RPC Outsider');
|
|
227
228
|
|
|
228
229
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
229
|
-
values (v_thread, 'coordination', v_sender, 'Inbox RPC Test Thread')
|
|
230
|
+
values (v_thread, 'coordination', v_sender, 'Inbox RPC Test Thread'),
|
|
231
|
+
(v_board_thread, 'coordination', v_sender, 'Inbox RPC empty board thread');
|
|
230
232
|
|
|
231
233
|
select * into v_result
|
|
232
234
|
from agent_board.create_board(
|
|
@@ -367,7 +369,7 @@ begin
|
|
|
367
369
|
|
|
368
370
|
select * into v_result
|
|
369
371
|
from agent_board.post_board_message(
|
|
370
|
-
|
|
372
|
+
v_board_thread,
|
|
371
373
|
v_sender,
|
|
372
374
|
v_board,
|
|
373
375
|
'Board subject',
|
|
@@ -399,7 +401,7 @@ begin
|
|
|
399
401
|
|
|
400
402
|
select * into v_result
|
|
401
403
|
from agent_board.create_tasking_delivery(
|
|
402
|
-
|
|
404
|
+
v_board_thread,
|
|
403
405
|
v_sender,
|
|
404
406
|
'Tasking title',
|
|
405
407
|
'Tasking details for duplicate suppression',
|
|
@@ -14,7 +14,7 @@ set search_path = extensions, public, agent_board;
|
|
|
14
14
|
|
|
15
15
|
begin;
|
|
16
16
|
|
|
17
|
-
select plan(
|
|
17
|
+
select plan(14);
|
|
18
18
|
|
|
19
19
|
create temp table _og_fixture (
|
|
20
20
|
board uuid,
|
|
@@ -81,12 +81,10 @@ begin
|
|
|
81
81
|
-- thread from source_task_id so this path is not a bypass (was the #78 round-1 panel finding).
|
|
82
82
|
perform agent_board.create_tasking_delivery(v_tasking_thread, v_member, 'task title', 'details', null, v_board);
|
|
83
83
|
|
|
84
|
-
--
|
|
85
|
-
-- means it stamps to v_board and is NEVER left NULL (the round-2 codex finding).
|
|
84
|
+
-- A first board associates the empty thread. A second board is refused below.
|
|
86
85
|
perform set_config('request.jwt.claim.role', 'service_role', true);
|
|
87
86
|
perform set_config('request.jwt.claim.sub', '', true);
|
|
88
87
|
perform agent_board.post_board_message(v_multi_thread, v_creator, v_board, 'm1', 'first board');
|
|
89
|
-
perform agent_board.post_board_message(v_multi_thread, v_creator, v_board2, 'm2', 'second board');
|
|
90
88
|
|
|
91
89
|
insert into _og_fixture values
|
|
92
90
|
(v_board, v_board2, v_member, v_observer, v_board_thread, v_boardless_thread, v_tasking_thread, v_multi_thread, v_member_user, v_observer_user);
|
|
@@ -114,6 +112,12 @@ select throws_ok(
|
|
|
114
112
|
'42501', 'thread posting requires a non-observer board role',
|
|
115
113
|
'observer cannot author into a board thread via post_message (bypass closed)');
|
|
116
114
|
|
|
115
|
+
select throws_ok(
|
|
116
|
+
$$select set_config('request.jwt.claim.role','service_role',true);
|
|
117
|
+
select * from agent_board.post_board_message((select multi_thread from _og_fixture),
|
|
118
|
+
(select member_agent from _og_fixture), (select board2 from _og_fixture), 'm2', 'second board')$$,
|
|
119
|
+
'42501', 'thread belongs to a different board', 'second board is refused at the boundary');
|
|
120
|
+
|
|
117
121
|
-- multi-board thread: stamped to the FIRST board (never left NULL) — round-2 panel finding ----
|
|
118
122
|
select is(
|
|
119
123
|
(select board_id from agent_board.threads where id = (select multi_thread from _og_fixture)),
|
|
@@ -83,11 +83,11 @@ begin
|
|
|
83
83
|
-- but has NO thread access — the case where mutate != access. Set up as service_role.
|
|
84
84
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, access_mode)
|
|
85
85
|
values (v_restricted_thread, 'coordination', v_creator, 'O4 restricted thread', 'restricted');
|
|
86
|
-
insert into agent_board.thread_memberships(thread_id, agent_id, role, granted_by_agent_id)
|
|
87
|
-
values (v_restricted_thread, v_creator, 'member', v_creator);
|
|
88
86
|
perform set_config('request.jwt.claim.role', 'service_role', true);
|
|
89
87
|
perform set_config('request.jwt.claim.sub', '', true);
|
|
90
88
|
perform agent_board.post_board_message(v_restricted_thread, v_creator, v_board, 'seed', 'seed');
|
|
89
|
+
insert into agent_board.thread_memberships(thread_id, agent_id, role, granted_by_agent_id)
|
|
90
|
+
values (v_restricted_thread, v_creator, 'member', v_creator);
|
|
91
91
|
select task_id into v_restricted_task from agent_board.create_task(v_restricted_thread, v_creator, 'restricted task', 'd');
|
|
92
92
|
|
|
93
93
|
insert into _o4_fixture values
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
begin;
|
|
38
38
|
|
|
39
|
-
select plan(
|
|
39
|
+
select plan(38);
|
|
40
40
|
|
|
41
41
|
-- ---------------------------------------------------------------------------
|
|
42
42
|
-- WO-1 SHAPE
|
|
@@ -201,7 +201,9 @@ select isnt(
|
|
|
201
201
|
-- closed to PUBLIC/anon/authenticated and open to service_role. A function added to this
|
|
202
202
|
-- migration (or any later one) that forgets its revoke or its grant fails here and PRINTS
|
|
203
203
|
-- ITS OWN NAME. Verified exemption-free at the time of writing: all eight members of the
|
|
204
|
-
-- class satisfy both directions, so neither assertion passes by finding nothing.
|
|
204
|
+
-- class satisfy both directions, so neither assertion passes by finding nothing. Since #717 F1
|
|
205
|
+
-- (migration 20261104180000) the two internal sinks are deliberately NOT service_role-executable;
|
|
206
|
+
-- the service_role direction names them as its only exemption and the third assertion proves it.
|
|
205
207
|
select is(
|
|
206
208
|
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
207
209
|
from pg_proc p
|
|
@@ -230,10 +232,31 @@ select is(
|
|
|
230
232
|
-- `AGENT_BOARD.CALLBACK_OUTBOX_EVENTS` drop OUT, so its executability is
|
|
231
233
|
-- never checked at all -- fail-OPEN, on a privilege gate.
|
|
232
234
|
and agent_board_test.stripped_body(p.oid) ~* 'callback_outbox_events'
|
|
235
|
+
-- The two internal outbox SINKS are not API routes (#717 F1, migration 20261104180000,
|
|
236
|
+
-- pinned by agent_board_delivery_writer_execute.test.sql). Their exemption is asserted
|
|
237
|
+
-- POSITIVELY by the WO-5 assertion that follows, not by absence from this one.
|
|
238
|
+
and p.proname not in ('_queue_board_post_callbacks', '_queue_inbox_callbacks')
|
|
233
239
|
and not has_function_privilege('service_role', p.oid, 'EXECUTE')),
|
|
234
240
|
'',
|
|
235
241
|
'WO-5 CLASS: every SECURITY DEFINER function reaching callback_outbox_events is executable by service_role -- a dropped-and-recreated function that lost its grant names itself here');
|
|
236
242
|
|
|
243
|
+
-- The exemption above, positively: each exempt sink exists, no API role (nor PUBLIC) can
|
|
244
|
+
-- EXECUTE it, and every function that calls it is SECURITY DEFINER, so the public routes still
|
|
245
|
+
-- reach it as the definer. A recreated sink that re-granted PUBLIC, or an INVOKER caller that
|
|
246
|
+
-- would now fail with 42501, names itself.
|
|
247
|
+
select is(
|
|
248
|
+
(select coalesce(string_agg(e.n, ', ' order by e.n), '')
|
|
249
|
+
from unnest(array['_queue_board_post_callbacks', '_queue_inbox_callbacks']) e(n)
|
|
250
|
+
where (select count(*) from pg_proc p where p.pronamespace = 'agent_board'::regnamespace and p.proname = e.n) <> 1
|
|
251
|
+
or exists (select 1 from pg_proc p cross join unnest(array['public', 'anon', 'authenticated', 'service_role', 'agent_board_delegate']) r
|
|
252
|
+
where p.pronamespace = 'agent_board'::regnamespace and p.proname = e.n
|
|
253
|
+
and has_function_privilege(r, p.oid, 'EXECUTE'))
|
|
254
|
+
or exists (select 1 from pg_proc c
|
|
255
|
+
where c.pronamespace = 'agent_board'::regnamespace and c.proname <> e.n and not c.prosecdef
|
|
256
|
+
and agent_board_test.stripped_body(c.oid) ~ agent_board_test.call_re(e.n))),
|
|
257
|
+
'',
|
|
258
|
+
'WO-5 CLASS: the two internal outbox sinks are API-closed (no PUBLIC/anon/authenticated/service_role/delegate EXECUTE) and called only from SECURITY DEFINER functions');
|
|
259
|
+
|
|
237
260
|
-- ---------------------------------------------------------------------------
|
|
238
261
|
-- Fixture: a real board fan-out. Five subscribers, one verified endpoint each.
|
|
239
262
|
-- ---------------------------------------------------------------------------
|