@consiliency/agent-board-schema 1.68.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.
Files changed (155) hide show
  1. package/fixtures/already-applied.json +28 -4
  2. package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
  3. package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
  4. package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
  5. package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
  6. package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
  7. package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
  8. package/package.json +1 -1
  9. package/schema-manifest.json +57 -3
  10. package/src/tooling.test.mjs +8 -2
  11. package/tests/agent_board_661_reply_delivery.test.sql +185 -0
  12. package/tests/agent_board_667_boundary.test.sql +162 -0
  13. package/tests/agent_board_667_conversion_branches.test.sql +91 -0
  14. package/tests/agent_board_667_conversion_profile.test.sql +112 -0
  15. package/tests/agent_board_667_history_class.test.sql +785 -0
  16. package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
  17. package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
  18. package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
  19. package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
  20. package/tests/agent_board_667_retention.test.sql +201 -0
  21. package/tests/agent_board_667_routing.test.sql +294 -0
  22. package/tests/agent_board_667_sender_effects.test.sql +100 -0
  23. package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
  24. package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
  25. package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
  26. package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
  27. package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
  28. package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
  29. package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
  30. package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
  31. package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
  32. package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
  33. package/tests/agent_board_667_writers.test.sql +281 -0
  34. package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
  35. package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
  36. package/tests/agent_board_addressing.test.sql +8 -5
  37. package/tests/agent_board_claim_subject_gate.test.sql +7 -3
  38. package/tests/agent_board_claimpolicy_race.test.sql +39 -11
  39. package/tests/agent_board_delegate_role_class.test.sql +33 -15
  40. package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
  41. package/tests/agent_board_fk_lock_edges.test.sql +3 -0
  42. package/tests/agent_board_hosted_delegates.test.sql +5 -2
  43. package/tests/agent_board_inbox_rpcs.test.sql +5 -3
  44. package/tests/agent_board_obsgate.test.sql +8 -4
  45. package/tests/agent_board_obsgate4.test.sql +2 -2
  46. package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
  47. package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
  48. package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
  49. package/tests/agent_board_session_identity_limits.test.sql +1 -1
  50. package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
  51. package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
  52. package/tests/agent_board_sidserver_authority.test.sql +69 -0
  53. package/tests/agent_board_sidserver_catalog.test.sql +47 -0
  54. package/tests/agent_board_signed_write_gateway.test.sql +4 -2
  55. package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
  56. package/tests/agent_board_v2_invariants.test.sql +5 -2
  57. package/tests/agent_board_write_enforcement_class.test.sql +3 -3
  58. package/tests/catalog/function_execute_privileges.json +41 -5
  59. package/tests/catalog/p_metadata_parameters.json +16 -0
  60. package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
  61. package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
  62. package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
  63. package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
  64. package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
  65. package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
  66. package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
  67. package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
  68. package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
  69. package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
  70. package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
  71. package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
  72. package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
  73. package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
  74. package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
  75. package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
  76. package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
  77. package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
  78. package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
  79. package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
  80. package/tests/isolation/expected/association_board_vs_board.out +71 -0
  81. package/tests/isolation/expected/association_post_wait_authority.out +77 -0
  82. package/tests/isolation/expected/association_strong_history_writers.out +203 -0
  83. package/tests/isolation/expected/association_strong_isolation.out +185 -0
  84. package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
  85. package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
  86. package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
  87. package/tests/isolation/expected/association_vs_private_message.out +67 -0
  88. package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
  89. package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
  90. package/tests/isolation/expected/message_replay_scope.out +209 -0
  91. package/tests/isolation/expected/tasking_replay_scope.out +125 -0
  92. package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
  93. package/tests/isolation/specs/association_board_vs_board.spec +37 -0
  94. package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
  95. package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
  96. package/tests/isolation/specs/association_strong_isolation.spec +50 -0
  97. package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
  98. package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
  99. package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
  100. package/tests/isolation/specs/association_vs_private_message.spec +33 -0
  101. package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
  102. package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
  103. package/tests/isolation/specs/message_replay_scope.spec +77 -0
  104. package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
  105. package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
  106. package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
  107. package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
  108. package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
  109. package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
  110. package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
  111. package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
  112. package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
  113. package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
  114. package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
  115. package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
  116. package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
  117. package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
  118. package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
  119. package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
  120. package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
  121. package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
  122. package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
  123. package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
  124. package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
  125. package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
  126. package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
  127. package/tests/mutations/717-f1/run.sh +17 -0
  128. package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
  129. package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
  130. package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
  131. package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
  132. package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
  133. package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
  134. package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
  135. package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
  136. package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
  137. package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
  138. package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
  139. package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
  140. package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
  141. package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
  142. package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
  143. package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
  144. package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
  145. package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
  146. package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
  147. package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
  148. package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
  149. package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
  150. package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
  151. package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
  152. package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
  153. package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
  154. package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
  155. 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]',
@@ -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
- v_thread,
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
- v_thread,
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(13);
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
- -- Multi-board thread: fan the SAME thread to v_board then v_board2 (as operator). first-board-wins
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(37);
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
  -- ---------------------------------------------------------------------------
@@ -40,7 +40,7 @@ select plan(40);
40
40
  -- The class, derived from pg_proc.
41
41
  -- ---------------------------------------------------------------------------
42
42
  select is(
43
- (select array_agg(p.proname::text order by p.proname)
43
+ (select array_agg(p.proname::text order by p.proname::text collate "C")
44
44
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
45
45
  where n.nspname = 'agent_board'
46
46
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into|merge into', 'inbox_items')),
@@ -48,17 +48,17 @@ select is(
48
48
  'OPN-01: _upsert_inbox_item is the only function that inserts into inbox_items');
49
49
 
50
50
  select is(
51
- (select array_agg(p.proname::text order by p.proname)
51
+ (select array_agg(p.proname::text order by p.proname::text collate "C")
52
52
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
53
53
  where n.nspname = 'agent_board'
54
54
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
55
55
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.qname_re('board_memberships')
56
56
  and p.proname <> '_upsert_inbox_item'),
57
- array['create_tasking_delivery', 'post_board_message'],
57
+ array['_deliver_board_message', 'create_tasking_delivery'],
58
58
  'OPN-02: the board-expanding delivery writers are exactly the two this file asserts behaviourally');
59
59
 
60
60
  select is(
61
- (select array_agg(p.proname::text order by p.proname)
61
+ (select array_agg(p.proname::text order by p.proname::text collate "C")
62
62
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
63
63
  where n.nspname = 'agent_board'
64
64
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_inbox_item')
@@ -73,7 +73,7 @@ select is(
73
73
  create temp table _opn (
74
74
  a_user uuid, b_user uuid,
75
75
  a_agent uuid, b_agent uuid,
76
- board_ab uuid, board_s uuid, thread uuid, thread_unlinked uuid, thread_unlinked2 uuid
76
+ board_ab uuid, board_s uuid, thread uuid, thread_unlinked uuid, thread_unlinked2 uuid, thread_sole_post uuid
77
77
  ) on commit drop;
78
78
  create temp table _opn_post (message_id uuid, board_id uuid, delivered_recipient_count integer,
79
79
  callback_outbox_count integer, created_at timestamptz) on commit drop;
@@ -106,6 +106,7 @@ declare
106
106
  v_thread uuid := '76080000-0000-4000-8000-000000000010';
107
107
  v_thread_unlinked uuid := '76080000-0000-4000-8000-000000000011';
108
108
  v_thread_unlinked2 uuid := '76080000-0000-4000-8000-000000000012';
109
+ v_thread_sole_post uuid := '76080000-0000-4000-8000-000000000013';
109
110
  v_board_ab uuid;
110
111
  v_board_s uuid;
111
112
  v_endpoint uuid;
@@ -120,7 +121,8 @@ begin
120
121
  insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
121
122
  values (v_thread, 'coordination', v_a, 'OPN thread'),
122
123
  (v_thread_unlinked, 'coordination', v_a, 'OPN unlinked thread'),
123
- (v_thread_unlinked2, 'coordination', v_a, 'OPN second unlinked thread');
124
+ (v_thread_unlinked2, 'coordination', v_a, 'OPN second unlinked thread'),
125
+ (v_thread_sole_post, 'coordination', v_a, 'OPN empty sole-post thread');
124
126
 
125
127
  select board_id into v_board_ab
126
128
  from agent_board.create_board(v_a, 'opn-board-ab', 'OPN AB', 'two members', '{}'::jsonb, 'private');
@@ -137,7 +139,7 @@ begin
137
139
  perform agent_board.verify_callback_endpoint(v_endpoint, v_b, '{"controlProofRef":"opn/b"}'::jsonb);
138
140
  perform agent_board.subscribe_callback_endpoint(v_endpoint, v_b, 'own_inbox', 'inbox.item.delivered', null, '{}'::jsonb);
139
141
 
140
- insert into _opn values (v_a_user, v_b_user, v_a, v_b, v_board_ab, v_board_s, v_thread, v_thread_unlinked, v_thread_unlinked2);
142
+ insert into _opn values (v_a_user, v_b_user, v_a, v_b, v_board_ab, v_board_s, v_thread, v_thread_unlinked, v_thread_unlinked2, v_thread_sole_post);
141
143
  end;
142
144
  $$;
143
145
 
@@ -219,7 +221,7 @@ set local role authenticated;
219
221
  select lives_ok(
220
222
  $$ insert into _opn_sole
221
223
  select * from agent_board.post_board_message(
222
- (select thread from _opn), null, (select board_s from _opn),
224
+ (select thread_sole_post from _opn), null, (select board_s from _opn),
223
225
  'opn sole post', 'the author posts to a board it is alone on', 'comment', 'medium', null, '{}'::jsonb) $$,
224
226
  'OPN-12: the author can post to a board it is the only member of');
225
227
 
@@ -479,7 +481,7 @@ select is(
479
481
  -- (explicit_set recursion), the read-only resolve_recipients, and _addressing_agent_ids_or_raise --
480
482
  -- the one delivery path, where the #608 rule lives. A new caller that delivers fails this by name.
481
483
  select is(
482
- (select array_agg(p.proname::text order by p.proname)
484
+ (select array_agg(p.proname::text order by p.proname::text collate "C")
483
485
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
484
486
  where n.nspname = 'agent_board'
485
487
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_resolve_addressing_recipient_rows')),