@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,828 @@
1
+ -- #717 F2b: a same-key send_direct_message / send_multicast_message replay cannot widen the
2
+ -- source message's audience.
3
+ --
4
+ -- THE DEFECT (main 21b7b158). Idempotency is keyed on (rpc, thread, resolved author, key). A
5
+ -- replay reused the author's ORIGINAL message and delivered it to whatever recipient the retry
6
+ -- named. The authenticated messages and artifacts policies admit any delivery recipient of a
7
+ -- message, so a newly named agent X read A's original body AND an artifact a different agent C
8
+ -- had anchored to it.
9
+ --
10
+ -- THE CONTRACT pinned here: identity, lock and lookup unchanged. On a replay every requested
11
+ -- recipient must already hold a persisted delivery of THAT source message in the function's own
12
+ -- mode ('direct' / 'multicast') with no board scope. A direct retry may only name its existing
13
+ -- private recipient; a multicast retry the same normalized set or a nonempty subset, and an
14
+ -- omitted recipient keeps its delivery. Anything else is 42501 'message replay cannot widen
15
+ -- delivery scope' with no side effect. Metadata, a forged lookup source, the thread's board stamp
16
+ -- and board-mode deliveries are not evidence. A new audience needs a new key, which creates a new
17
+ -- message carrying none of the source's artifacts.
18
+ --
19
+ -- METHOD (AGENTS.md rules 2 and 3).
20
+ -- * Every actor runs under its OWN role AND claims (`set local role authenticated|service_role`
21
+ -- plus a JSON claim set); the flattened request.jwt.claim.* GUCs are cleared on every switch.
22
+ -- * Every call runs in a helper with its own exception block returning 'ok' or
23
+ -- '<sqlstate>:<message>'; refusals compare the exact message, and the same helper reports the
24
+ -- side-effect fingerprint delta around the call. State reads are separate queries.
25
+ -- * The artifact on each source message is written by its RECIPIENT C through add_artifact,
26
+ -- so the row that must stay hidden is another agent's, not the author's.
27
+ create extension if not exists pgtap with schema extensions;
28
+ set search_path = extensions, public, agent_board;
29
+
30
+ begin;
31
+
32
+ select plan(67);
33
+
34
+ create temp table _h(k text primary key, v text);
35
+ create function pg_temp.v(p_k text) returns text language sql as $$ select v from _h where k = p_k $$;
36
+ -- Never raises: a value that is not a uuid (an 'ERR ...' result under a mutation) reads as NULL,
37
+ -- so a broken implementation fails an assertion instead of aborting the file (AGENTS.md rule 3).
38
+ create function pg_temp.u(p_k text) returns uuid language sql as $$
39
+ select case when v ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' then v::uuid end from _h where k = p_k $$;
40
+ create function pg_temp.put(p_k text, p_v text) returns void language sql as $$
41
+ insert into _h values (p_k, p_v) on conflict (k) do update set v = excluded.v $$;
42
+
43
+ -- Identity switch: one JSON claim set, every flattened claim GUC cleared.
44
+ create function pg_temp.ident(p_role text, p_sub uuid) returns void language plpgsql as $$
45
+ begin
46
+ perform set_config('request.jwt.claim.sub', '', true);
47
+ perform set_config('request.jwt.claim.role', '', true);
48
+ perform set_config('request.jwt.claim.agent_credential_id', '', true);
49
+ perform set_config('request.jwt.claim.identity_lease_epoch', '', true);
50
+ perform set_config('request.jwt.claims',
51
+ case when p_role is null then ''
52
+ else jsonb_strip_nulls(jsonb_build_object('role', p_role, 'sub', p_sub))::text end,
53
+ true);
54
+ end $$;
55
+
56
+ -- Run p_sql AS actor p_who (its role and claims). Returns 'ok' or '<sqlstate>:<message>'.
57
+ create function pg_temp.as_(p_who text, p_sql text) returns text language plpgsql as $$
58
+ declare
59
+ v text;
60
+ v_role text := pg_temp.v(p_who || '.role');
61
+ begin
62
+ perform pg_temp.ident(v_role, pg_temp.u(p_who || '.auth'));
63
+ execute format('set local role %I', v_role);
64
+ begin
65
+ execute p_sql;
66
+ v := 'ok';
67
+ exception when others then
68
+ v := sqlstate || ':' || sqlerrm;
69
+ end;
70
+ reset role;
71
+ perform pg_temp.ident(null, null);
72
+ return v;
73
+ end $$;
74
+
75
+ -- Run p_sql AS actor p_who and return its single scalar.
76
+ create function pg_temp.val(p_who text, p_sql text) returns text language plpgsql as $$
77
+ declare
78
+ v text;
79
+ v_role text := pg_temp.v(p_who || '.role');
80
+ begin
81
+ perform pg_temp.ident(v_role, pg_temp.u(p_who || '.auth'));
82
+ execute format('set local role %I', v_role);
83
+ begin
84
+ execute p_sql into v;
85
+ exception when others then
86
+ v := 'ERR ' || sqlstate || ':' || sqlerrm;
87
+ end;
88
+ reset role;
89
+ perform pg_temp.ident(null, null);
90
+ return coalesce(v, '(null)');
91
+ end $$;
92
+
93
+ -- Everything a message send can write, counted globally (this transaction is the only writer),
94
+ -- plus a digest of every inbox item's presentation: an accepted retry may refresh it, a refused
95
+ -- one must not.
96
+ create function pg_temp.fp() returns text language plpgsql as $$
97
+ declare v text;
98
+ begin
99
+ select concat_ws(' ',
100
+ 'messages=' || (select count(*) from agent_board.messages),
101
+ 'artifacts=' || (select count(*) from agent_board.artifacts),
102
+ 'deliveries=' || (select count(*) from agent_board.delivery_recipients),
103
+ 'inbox=' || (select count(*) from agent_board.inbox_items),
104
+ 'callbacks=' || (select count(*) from agent_board.callback_outbox_events),
105
+ 'evidence=' || (select count(*) from agent_board.signature_evidence),
106
+ 'telemetry=' || (select count(*) from agent_board.send_authz_telemetry),
107
+ 'stamped=' || (select count(*) from agent_board.threads where board_id is not null),
108
+ 'inboxtxt=' || (select md5(coalesce(string_agg(id::text || subject || coalesce(preview, ''), ',' order by id), ''))
109
+ from agent_board.inbox_items))
110
+ into v;
111
+ return v;
112
+ end $$;
113
+
114
+ -- The row counts alone (no inbox presentation digest): what an ACCEPTED retry must leave unchanged.
115
+ create function pg_temp.fp_rows() returns text language sql as $$ select split_part(pg_temp.fp(), ' inboxtxt=', 1) $$;
116
+
117
+ -- A call that must be REFUSED: '<result> | side-effects=none' when nothing changed.
118
+ create function pg_temp.refuse(p_who text, p_sql text) returns text language plpgsql as $$
119
+ declare v_before text := pg_temp.fp(); v_res text; v_after text;
120
+ begin
121
+ v_res := pg_temp.as_(p_who, p_sql);
122
+ v_after := pg_temp.fp();
123
+ return v_res || ' | side-effects=' || case when v_after = v_before then 'none' else v_before || ' -> ' || v_after end;
124
+ end $$;
125
+
126
+ -- What actor p_who can read of message p_msg: the message, then every table that references
127
+ -- messages and is readable by an API role (derived and pinned in 56/57): artifacts, deliveries,
128
+ -- inbox items, callback outbox events, signature evidence; then the body.
129
+ create function pg_temp.sees(p_who text, p_msg text) returns text language plpgsql as $$
130
+ begin
131
+ return pg_temp.val(p_who, format($q$
132
+ select (select count(*) from agent_board.messages where id = %1$L)
133
+ || '/' || (select count(*) from agent_board.artifacts where message_id = %1$L)
134
+ || '/' || (select count(*) from agent_board.delivery_recipients where source_message_id = %1$L)
135
+ || '/' || (select count(*) from agent_board.inbox_items where source_message_id = %1$L)
136
+ || '/' || (select count(*) from agent_board.callback_outbox_events where source_message_id = %1$L)
137
+ || '/' || (select count(*) from agent_board.signature_evidence where source_message_id = %1$L)
138
+ || ' ' || coalesce((select body from agent_board.messages where id = %1$L), '-')
139
+ $q$, pg_temp.v(p_msg)));
140
+ end $$;
141
+
142
+ -- A message's delivery scope: sorted 'recipient-handle:mode:board-slug|private'.
143
+ create function pg_temp.scope(p_msg text) returns text language sql as $$
144
+ select coalesce(string_agg(a.handle || ':' || dr.delivery_mode || ':' || coalesce(b.slug, 'private'), ',' order by a.handle), '(none)')
145
+ from agent_board.delivery_recipients dr
146
+ join agent_board.agents a on a.id = dr.recipient_agent_id
147
+ left join agent_board.boards b on b.id = dr.board_id
148
+ where dr.source_message_id = pg_temp.u(p_msg)
149
+ $$;
150
+
151
+ -- A source message's rows byte for byte: the message (id, author, body, metadata), its
152
+ -- artifacts (ids, authors, content) and its delivery rows.
153
+ create function pg_temp.history(p_msg text) returns text language sql as $$
154
+ select md5(concat_ws('#',
155
+ (select m::text from agent_board.messages m where m.id = pg_temp.u(p_msg)),
156
+ (select string_agg(ar::text, ',' order by ar.id) from agent_board.artifacts ar where ar.message_id = pg_temp.u(p_msg)),
157
+ (select string_agg(dr.id::text || dr.delivery_mode || dr.recipient_agent_id::text || coalesce(dr.board_id::text, '-'), ',' order by dr.id)
158
+ from agent_board.delivery_recipients dr where dr.source_message_id = pg_temp.u(p_msg))))
159
+ $$;
160
+
161
+ create function pg_temp.dm(p_who text, p_thread text, p_to text, p_key text, p_body text default 'retry body',
162
+ p_metadata jsonb default '{}'::jsonb) returns text language sql as $$
163
+ select format('select * from agent_board.send_direct_message(%L::uuid, %L::uuid, %L::uuid, %L, %L, ''comment'', ''medium'', %L, %L::jsonb)',
164
+ pg_temp.v(p_thread), pg_temp.v(p_who), pg_temp.v(p_to), 'subj ' || p_body, p_body, p_key, p_metadata::text)
165
+ $$;
166
+ create function pg_temp.mc(p_who text, p_thread text, p_to text, p_key text, p_body text default 'retry body',
167
+ p_metadata jsonb default '{}'::jsonb) returns text language sql as $$
168
+ select format('select * from agent_board.send_multicast_message(%L::uuid, %L::uuid, %s, %L, %L, ''comment'', ''medium'', %L, %L::jsonb)',
169
+ pg_temp.v(p_thread), pg_temp.v(p_who), p_to, 'subj ' || p_body, p_body, p_key, p_metadata::text)
170
+ $$;
171
+ -- uuid[] literal of the named agents, in the order given (duplicates kept).
172
+ create function pg_temp.ids(variadic p_who text[]) returns text language sql as $$
173
+ select 'array[' || string_agg(quote_literal(pg_temp.v(w)), ',' order by o) || ']::uuid[]'
174
+ from unnest(p_who) with ordinality as x(w, o)
175
+ $$;
176
+ -- addressing_v1 descriptor array of the named agents.
177
+ create function pg_temp.desc_(variadic p_who text[]) returns jsonb language sql as $$
178
+ select jsonb_agg(jsonb_build_object('kind', 'agent_id', 'agentId', pg_temp.v(w)) order by o)
179
+ from unnest(p_who) with ordinality as x(w, o)
180
+ $$;
181
+
182
+ -- Signed wrappers, as service_role with a gateway decision for p_actor. The decision is a SQL
183
+ -- fixture: this proves the wrappers' routing and atomicity, not signature verification.
184
+ create function pg_temp.gw(p_actor text) returns text language sql as $$
185
+ select quote_literal(jsonb_build_object('agent_id', pg_temp.v(p_actor), 'verification_status', 'warn',
186
+ 'payload_digest', 'sha256:' || repeat('7', 64), 'enforcement_mode', 'warn')::text) || '::jsonb'
187
+ $$;
188
+ create function pg_temp.signed(p_fn text, p_payload jsonb, p_actor text default 'A') returns text language sql as $$
189
+ select format('select * from agent_board.%I(%L::jsonb, %s)', p_fn, p_payload::text, pg_temp.gw(p_actor))
190
+ $$;
191
+
192
+ -- ===========================================================================
193
+ -- Fixtures
194
+ -- ===========================================================================
195
+ -- Keyed agents: A author, A2 second author, C and C2 private recipients (C authors the
196
+ -- artifacts), X an agent never addressed, M a member of board B, Y a forged-evidence target.
197
+ insert into auth.users(id, instance_id, aud, role, email)
198
+ select ('717b0000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
199
+ '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'f2b717-' || n || '@t.test'
200
+ from generate_series(1, 7) n;
201
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
202
+ select ('717b1000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, 'agent', 'agent',
203
+ 'f2b717-' || h, 'F2b ' || h, 'active', ('717b0000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
204
+ from unnest(array['a', 'a2', 'c', 'c2', 'x', 'm', 'y']) with ordinality as x(h, n);
205
+ do $$
206
+ declare h text; n int := 0;
207
+ begin
208
+ foreach h in array array['A', 'A2', 'C', 'C2', 'X', 'M', 'Y'] loop
209
+ n := n + 1;
210
+ perform pg_temp.put(h, '717b1000-0000-4000-8000-' || lpad(n::text, 12, '0'));
211
+ perform pg_temp.put(h || '.auth', '717b0000-0000-4000-8000-' || lpad(n::text, 12, '0'));
212
+ perform pg_temp.put(h || '.role', 'authenticated');
213
+ end loop;
214
+ end $$;
215
+ select pg_temp.put('SVC.role', 'service_role');
216
+ -- Verified own_inbox callback subscriptions for C, C2, X and M: a delivery to any of them would
217
+ -- also queue a callback, so the side-effect fingerprint discriminates a callback write.
218
+ insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status)
219
+ select ('717b3000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, pg_temp.u(h), 'https://example.test/f2b717/' || h, 'verified'
220
+ from unnest(array['C', 'C2', 'X', 'M']) with ordinality as x(h, n);
221
+ insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category)
222
+ select ('717b3000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid, pg_temp.u(h), 'own_inbox', 'inbox.item.delivered'
223
+ from unnest(array['C', 'C2', 'X', 'M']) with ordinality as x(h, n);
224
+
225
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
226
+ values ('717b2000-0000-4000-8000-000000000001', 'f2b717-b', 'F2b B', pg_temp.u('A'), 'private', 'active');
227
+ select pg_temp.put('B', '717b2000-0000-4000-8000-000000000001');
228
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
229
+ values (pg_temp.u('B'), pg_temp.u('A'), 'owner', 'joined', pg_temp.u('A')),
230
+ (pg_temp.u('B'), pg_temp.u('C'), 'member', 'joined', pg_temp.u('A')),
231
+ (pg_temp.u('B'), pg_temp.u('M'), 'member', 'joined', pg_temp.u('A')),
232
+ (pg_temp.u('B'), pg_temp.u('Y'), 'member', 'joined', pg_temp.u('A'));
233
+
234
+ -- T: a plain thread. TB: a thread stamped to board B by A's board post.
235
+ select pg_temp.put('T', pg_temp.val('A', $$ select thread_id::text from agent_board.create_or_find_thread(null, 'F2b thread', 'f2b717-thread') $$));
236
+ select pg_temp.put('TB', pg_temp.val('A', $$ select thread_id::text from agent_board.create_or_find_thread(null, 'F2b board thread', 'f2b717-board-thread') $$));
237
+ select pg_temp.put('TB.post', pg_temp.as_('A', format($$ select * from agent_board.post_board_message(%L, %L, %L, 's', 'board post') $$,
238
+ pg_temp.v('TB'), pg_temp.v('A'), pg_temp.v('B'))));
239
+
240
+ -- The private sources. DM: A -> C, key dk, on T. MC: A -> {C, C2}, key mk, on T. DMB: A -> C on the
241
+ -- board-stamped TB, key bk. Each carries an artifact written by C.
242
+ select pg_temp.put('DM', pg_temp.val('A', replace(pg_temp.dm('A', 'T', 'C', 'dk', 'A-PRIVATE-DM-BODY'), 'select *', 'select message_id::text')));
243
+ select pg_temp.put('MC', pg_temp.val('A', replace(pg_temp.mc('A', 'T', pg_temp.ids('C', 'C2'), 'mk', 'A-PRIVATE-MC-BODY'), 'select *', 'select message_id::text')));
244
+ -- A2 is given access to T by a DM from A (so A2 can author its own same-key messages there).
245
+ select pg_temp.put('A2.access', pg_temp.as_('A', pg_temp.dm('A', 'T', 'A2', 'a2-access', 'ACCESS-FOR-A2')));
246
+ select pg_temp.put('DMB', pg_temp.val('A', replace(pg_temp.dm('A', 'TB', 'C', 'bk', 'A-PRIVATE-BOARD-THREAD-DM'), 'select *', 'select message_id::text')));
247
+ create function pg_temp.art(p_msg text, p_thread text) returns text language sql as $$
248
+ select pg_temp.val('C', format($q$ select artifact_id::text from agent_board.add_artifact(%L, %L, 'url', 'external_url', %L, null, %L) $q$,
249
+ pg_temp.v('C'), 'https://example.test/c-private/' || p_msg, pg_temp.v(p_thread), pg_temp.v(p_msg)))
250
+ $$;
251
+ select pg_temp.put('DM.art', pg_temp.art('DM', 'T')), pg_temp.put('MC.art', pg_temp.art('MC', 'T')), pg_temp.put('DMB.art', pg_temp.art('DMB', 'TB'));
252
+
253
+ -- 1
254
+ select is(
255
+ pg_temp.v('TB.post') || ' ' || pg_temp.v('A2.access') || ' ' || (select coalesce(board_id::text, 'null') from agent_board.threads where id = pg_temp.u('TB'))
256
+ || ' ' || (select coalesce(board_id::text, 'null') from agent_board.threads where id = pg_temp.u('T')),
257
+ 'ok ok ' || pg_temp.v('B') || ' null',
258
+ 'fixture: TB is stamped to board B by a board post; T is a plain thread A2 can reach');
259
+ -- 2 -- exact authors, bodies and scopes of the three sources, and C as the artifacts' author
260
+ select is(
261
+ (select string_agg(a.handle || ':' || m.body, ' ' order by m.body)
262
+ from agent_board.messages m join agent_board.agents a on a.id = m.author_agent_id
263
+ where m.id in (pg_temp.u('DM'), pg_temp.u('MC'), pg_temp.u('DMB')))
264
+ || ' | ' || pg_temp.scope('DM') || ' | ' || pg_temp.scope('MC') || ' | ' || pg_temp.scope('DMB')
265
+ || ' | ' || (select string_agg(a.handle || '@' || case ar.message_id when pg_temp.u('DM') then 'DM' when pg_temp.u('MC') then 'MC' when pg_temp.u('DMB') then 'DMB' end, ',' order by ar.id::text = pg_temp.v('DMB.art'), ar.id::text = pg_temp.v('MC.art'))
266
+ from agent_board.artifacts ar join agent_board.agents a on a.id = ar.created_by_agent_id
267
+ where ar.id in (pg_temp.u('DM.art'), pg_temp.u('MC.art'), pg_temp.u('DMB.art'))),
268
+ 'f2b717-a:A-PRIVATE-BOARD-THREAD-DM f2b717-a:A-PRIVATE-DM-BODY f2b717-a:A-PRIVATE-MC-BODY'
269
+ || ' | f2b717-c:direct:private | f2b717-c:multicast:private,f2b717-c2:multicast:private | f2b717-c:direct:private'
270
+ || ' | f2b717-c@DM,f2b717-c@MC,f2b717-c@DMB',
271
+ 'fixture: A authored three private sources (direct, multicast, direct on the board thread); C authored an artifact on each');
272
+ select pg_temp.put('DM.history', pg_temp.history('DM')), pg_temp.put('MC.history', pg_temp.history('MC')), pg_temp.put('DMB.history', pg_temp.history('DMB'));
273
+
274
+ -- ===========================================================================
275
+ -- Visibility before any replay
276
+ -- ===========================================================================
277
+ -- 3
278
+ select is(pg_temp.sees('X', 'DM') || ' | ' || pg_temp.sees('X', 'MC'),
279
+ '0/0/0/0/0/0 - | 0/0/0/0/0/0 -',
280
+ 'before: X sees none of DM or MC (message/artifacts/deliveries/inbox)');
281
+ -- 4 -- private sends on a board thread stay private to the board's other members
282
+ select is(pg_temp.sees('M', 'DMB') || ' | ' || pg_temp.sees('Y', 'DMB'),
283
+ '0/0/0/0/0/0 - | 0/0/0/0/0/0 -',
284
+ 'before: board members M and Y see nothing of the private DM on the board-stamped thread');
285
+ -- 5
286
+ select is(pg_temp.sees('C', 'DM') || ' | ' || pg_temp.sees('C', 'MC') || ' | ' || pg_temp.sees('C2', 'MC') || ' | ' || pg_temp.sees('A', 'DM'),
287
+ '1/1/1/1/1/0 A-PRIVATE-DM-BODY | 1/1/1/1/1/0 A-PRIVATE-MC-BODY | 1/1/1/1/1/0 A-PRIVATE-MC-BODY | 1/1/0/0/0/0 A-PRIVATE-DM-BODY',
288
+ 'before: each recipient sees its source, C''s artifact and its own delivery, inbox item and callback; the author sees its message and the artifact');
289
+
290
+ -- ===========================================================================
291
+ -- Direct: refused replays (42501, nothing written)
292
+ -- ===========================================================================
293
+ -- 6 -- the exploit: same key, newly named recipient
294
+ select is(pg_temp.refuse('A', pg_temp.dm('A', 'T', 'X', 'dk')),
295
+ '42501:message replay cannot widen delivery scope | side-effects=none',
296
+ 'direct: a same-key replay naming a new recipient is refused');
297
+ -- 7
298
+ select is(pg_temp.refuse('SVC', pg_temp.dm('A', 'T', 'X', 'dk')),
299
+ '42501:message replay cannot widen delivery scope | side-effects=none',
300
+ 'direct, service_role on the UUID core: the same refusal, the bypass telemetry row rolled back');
301
+ -- 8
302
+ select is(pg_temp.refuse('A', format($$ select * from agent_board.send_direct_message(%L::uuid, %L::uuid, %L::jsonb, 'addressing_v1', 's', 'b', 'comment', 'medium', 'dk', '{}'::jsonb) $$,
303
+ pg_temp.v('T'), pg_temp.v('A'), (pg_temp.desc_('X') -> 0)::text)),
304
+ '42501:message replay cannot widen delivery scope | side-effects=none',
305
+ 'direct, addressing overload: a descriptor naming a new recipient is refused');
306
+ -- 9
307
+ select is(pg_temp.refuse('SVC', pg_temp.signed('signed_send_direct_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
308
+ 'p_recipient_agent_id', pg_temp.v('X'), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'dk'))),
309
+ '42501:message replay cannot widen delivery scope | side-effects=none',
310
+ 'direct, signed wrapper: refused with no signature evidence, delivery, inbox or callback');
311
+ -- 10
312
+ select is(pg_temp.refuse('A', pg_temp.dm('A', 'T', 'X', 'dk', 'b',
313
+ jsonb_build_object('rpcName', 'send_direct_message', 'idempotencyKey', 'dk', 'recipientAgentId', pg_temp.v('X'), 'deliveredTo', jsonb_build_array(pg_temp.v('X'))))),
314
+ '42501:message replay cannot widen delivery scope | side-effects=none',
315
+ 'direct: caller metadata naming X as a prior recipient is not evidence');
316
+ -- 11 -- the thread's board stamp is not evidence: M is a member of the board TB is stamped to
317
+ select is(pg_temp.refuse('A', pg_temp.dm('A', 'TB', 'M', 'bk')),
318
+ '42501:message replay cannot widen delivery scope | side-effects=none',
319
+ 'direct on a board-stamped thread: replaying to a member of that board is refused');
320
+ -- 12 -- the existing actor binding still answers first
321
+ select is(pg_temp.refuse('M', pg_temp.dm('A', 'T', 'X', 'dk')),
322
+ '42501:p_author_agent_id must match current_agent_id() | side-effects=none',
323
+ 'direct: an authenticated caller naming another author is refused by the existing actor binding');
324
+
325
+ -- Forged evidence, written as the table owner. These rows model what no API role should be able
326
+ -- to write (#717 F1 revokes service_role EXECUTE on the writer); they pin the predicate's MODE and
327
+ -- BOARD conjuncts. Y holds a BOARD-mode delivery of DMB on board B; X holds a MULTICAST-mode
328
+ -- delivery of DMB. Neither is a private direct delivery.
329
+ insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id, board_membership_id)
330
+ values ('board', pg_temp.u('Y'), 'message', pg_temp.u('DMB'), pg_temp.u('B'),
331
+ (select id from agent_board.board_memberships where board_id = pg_temp.u('B') and agent_id = pg_temp.u('Y'))),
332
+ ('multicast', pg_temp.u('X'), 'message', pg_temp.u('DMB'), null, null)
333
+ on conflict (recipient_agent_id, canonical_source_key) do nothing;
334
+ -- 13
335
+ select is(pg_temp.refuse('A', pg_temp.dm('A', 'TB', 'Y', 'bk')),
336
+ '42501:message replay cannot widen delivery scope | side-effects=none',
337
+ 'direct: a board-mode delivery of the source is not evidence of a private direct delivery');
338
+ -- 14
339
+ select is(pg_temp.refuse('A', pg_temp.dm('A', 'TB', 'X', 'bk')),
340
+ '42501:message replay cannot widen delivery scope | side-effects=none',
341
+ 'direct: a delivery of the source in another mode (multicast) is not evidence');
342
+ delete from agent_board.delivery_recipients
343
+ where source_message_id = pg_temp.u('DMB') and recipient_agent_id in (pg_temp.u('Y'), pg_temp.u('X'));
344
+
345
+ -- A forged lookup source: post_message persists caller metadata, so a reply can carry the
346
+ -- DM lookup's reserved keys. The lookup finds it, and it has no delivery at all: fail closed.
347
+ select pg_temp.put('FORGED', pg_temp.val('A', format($$ select message_id::text from agent_board.post_message(%L, %L, 'FORGED-REPLY', 'comment', null, %L::jsonb) $$,
348
+ pg_temp.v('T'), pg_temp.v('A'), jsonb_build_object('rpcName', 'send_direct_message', 'idempotencyKey', 'fk')::text)));
349
+ -- 15
350
+ select is(
351
+ (select metadata ->> 'rpcName' || '/' || (metadata ->> 'idempotencyKey') from agent_board.messages where id = pg_temp.u('FORGED'))
352
+ || ' ' || pg_temp.refuse('A', pg_temp.dm('A', 'T', 'X', 'fk')),
353
+ 'send_direct_message/fk 42501:message replay cannot widen delivery scope | side-effects=none',
354
+ 'direct: a lookup source with forged reserved metadata and no persisted delivery is refused (no reconstruction)');
355
+
356
+ -- ===========================================================================
357
+ -- Direct: accepted retries and new keys
358
+ -- ===========================================================================
359
+ create temp table _r as select * from agent_board.send_direct_message(null, null, null, null, null) where false;
360
+ grant all on _r to authenticated, service_role;
361
+ create function pg_temp.dm_row(p_who text, p_sql text) returns text language plpgsql as $$
362
+ declare v text;
363
+ begin
364
+ delete from _r;
365
+ v := pg_temp.as_(p_who, 'insert into _r ' || p_sql);
366
+ return v;
367
+ end $$;
368
+ select pg_temp.put('fp.before-retry', pg_temp.fp_rows());
369
+ -- The call runs in its own statement: a bare subquery in the SAME statement reads the statement's
370
+ -- start snapshot and would not see the call's writes.
371
+ select pg_temp.put('r16', pg_temp.dm_row('A', pg_temp.dm('A', 'T', 'C', ' dk ', 'REWORDED')));
372
+ -- 16 -- identical retry to the existing recipient: same canonical rows, intended inbox refresh
373
+ select is(
374
+ pg_temp.v('r16')
375
+ || ' ' || ((select message_id from _r) = pg_temp.u('DM'))::text
376
+ || ' ' || ((select delivery_recipient_id from _r) = (select id from agent_board.delivery_recipients where source_message_id = pg_temp.u('DM') and recipient_agent_id = pg_temp.u('C')))::text
377
+ || ' cb=' || (select callback_outbox_count from _r)
378
+ || ' ' || (select subject || '/' || preview from agent_board.inbox_items where id = (select inbox_item_id from _r)),
379
+ 'ok true true cb=0 subj REWORDED/REWORDED',
380
+ 'direct: a retry to the existing private recipient (trimmed key) reuses message and delivery, refreshes the inbox, queues no second callback');
381
+ -- 17
382
+ select is(
383
+ pg_temp.fp_rows(), pg_temp.v('fp.before-retry'),
384
+ 'direct: the accepted retry wrote no new message, delivery, inbox item, callback, evidence or telemetry row');
385
+ select pg_temp.put('r18', pg_temp.as_('SVC', pg_temp.signed('signed_send_direct_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
386
+ 'p_recipient_agent_id', pg_temp.v('C'), 'p_subject', 's', 'p_body', 'SIGNED-RETRY', 'p_idempotency_key', 'dk'))));
387
+ -- 18 -- signed retry to the existing recipient: accepted, one evidence row on the source
388
+ select is(
389
+ pg_temp.v('r18')
390
+ || ' ' || pg_temp.scope('DM')
391
+ || ' ' || (select count(*) from agent_board.signature_evidence where source_message_id = pg_temp.u('DM')),
392
+ 'ok f2b717-c:direct:private 1',
393
+ 'direct, signed wrapper: a retry to the existing recipient is accepted on the source, one evidence row, no new scope');
394
+ -- 19 -- a new key reaches X with a NEW message carrying none of DM's artifacts
395
+ select pg_temp.put('DM.new', pg_temp.val('A', replace(pg_temp.dm('A', 'T', 'X', 'dk2', 'NEW-KEY-BODY'), 'select *', 'select message_id::text')));
396
+ select is(
397
+ (pg_temp.v('DM.new') is distinct from pg_temp.v('DM'))::text || ' ' || pg_temp.scope('DM.new') || ' | ' || pg_temp.sees('X', 'DM.new') || ' | ' || pg_temp.sees('X', 'DM'),
398
+ 'true f2b717-x:direct:private | 1/0/1/1/1/0 NEW-KEY-BODY | 0/0/0/0/0/0 -',
399
+ 'direct: a new key to X is a new message with no artifact; X still sees nothing of DM');
400
+ -- 20 -- another author's same key is its own message
401
+ select pg_temp.put('DM.a2', pg_temp.val('A2', replace(pg_temp.dm('A2', 'T', 'X', 'dk', 'A2-BODY'), 'select *', 'select message_id::text')));
402
+ select is(
403
+ (pg_temp.u('DM.a2') is not null and pg_temp.v('DM.a2') not in (pg_temp.v('DM'), pg_temp.v('DM.new')))::text
404
+ || ' ' || (select a.handle from agent_board.messages m join agent_board.agents a on a.id = m.author_agent_id where m.id = pg_temp.u('DM.a2'))
405
+ || ' ' || pg_temp.sees('X', 'DM'),
406
+ 'true f2b717-a2 0/0/0/0/0/0 -',
407
+ 'direct: the same key used by another author is that author''s own new message; DM stays hidden from X');
408
+
409
+ -- ===========================================================================
410
+ -- Multicast: refused replays
411
+ -- ===========================================================================
412
+ -- 21
413
+ select is(pg_temp.refuse('A', pg_temp.mc('A', 'T', pg_temp.ids('C', 'C2', 'X'), 'mk')),
414
+ '42501:message replay cannot widen delivery scope | side-effects=none',
415
+ 'multicast: the existing set plus a new recipient is refused');
416
+ -- 22
417
+ select is(pg_temp.refuse('A', pg_temp.mc('A', 'T', pg_temp.ids('X'), 'mk')),
418
+ '42501:message replay cannot widen delivery scope | side-effects=none',
419
+ 'multicast: a disjoint set is refused');
420
+ -- 23
421
+ select is(pg_temp.refuse('SVC', pg_temp.mc('A', 'T', pg_temp.ids('C', 'X'), 'mk')),
422
+ '42501:message replay cannot widen delivery scope | side-effects=none',
423
+ 'multicast, service_role on the UUID core: refused, the bypass telemetry row rolled back');
424
+ -- 24
425
+ select is(pg_temp.refuse('A', format($$ select * from agent_board.send_multicast_message(%L::uuid, %L::uuid, %L::jsonb, 'addressing_v1', 's', 'b', 'comment', 'medium', 'mk', '{}'::jsonb) $$,
426
+ pg_temp.v('T'), pg_temp.v('A'), pg_temp.desc_('C', 'X')::text)),
427
+ '42501:message replay cannot widen delivery scope | side-effects=none',
428
+ 'multicast, addressing overload: descriptors naming a new recipient are refused');
429
+ -- 25
430
+ select is(pg_temp.refuse('SVC', pg_temp.signed('signed_send_multicast_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
431
+ 'p_recipient_agent_ids', jsonb_build_array(pg_temp.v('C'), pg_temp.v('X')), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'mk'))),
432
+ '42501:message replay cannot widen delivery scope | side-effects=none',
433
+ 'multicast, signed UUID branch: refused with no signature evidence, delivery, inbox or callback');
434
+ -- 26
435
+ select is(pg_temp.refuse('SVC', pg_temp.signed('signed_send_multicast_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
436
+ 'p_recipient_descriptors', pg_temp.desc_('X'), 'p_addressing_mode', 'addressing_v1', 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'mk'))),
437
+ '42501:message replay cannot widen delivery scope | side-effects=none',
438
+ 'multicast, signed descriptor branch: refused with no side effect');
439
+ -- 27
440
+ select is(pg_temp.refuse('A', pg_temp.mc('A', 'T', pg_temp.ids('C', 'X'), 'mk', 'b',
441
+ jsonb_build_object('rpcName', 'send_multicast_message', 'idempotencyKey', 'mk', 'recipientAgentIds', jsonb_build_array(pg_temp.v('C'), pg_temp.v('X'))))),
442
+ '42501:message replay cannot widen delivery scope | side-effects=none',
443
+ 'multicast: caller metadata naming X is not evidence');
444
+ -- 28 -- a direct delivery of the same message to X would be another mode: not evidence either
445
+ insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id)
446
+ values ('direct', pg_temp.u('X'), 'message', pg_temp.u('MC'))
447
+ on conflict (recipient_agent_id, canonical_source_key) do nothing;
448
+ select is(pg_temp.refuse('A', pg_temp.mc('A', 'T', pg_temp.ids('C', 'X'), 'mk')),
449
+ '42501:message replay cannot widen delivery scope | side-effects=none',
450
+ 'multicast: a delivery of the source in another mode (direct, forged as owner) is not evidence');
451
+ delete from agent_board.delivery_recipients where source_message_id = pg_temp.u('MC') and recipient_agent_id = pg_temp.u('X');
452
+ -- 29
453
+ select is(pg_temp.refuse('A', pg_temp.mc('A', 'T', 'array[]::uuid[]', 'mk')) || ' || ' || pg_temp.refuse('A', pg_temp.mc('A', 'T', 'null::uuid[]', 'mk')),
454
+ '22023:recipient_agent_ids must not be empty | side-effects=none || 22023:recipient_agent_ids must not be empty | side-effects=none',
455
+ 'multicast: an empty or null set is still refused as before (a subset must be nonempty)');
456
+
457
+ -- ===========================================================================
458
+ -- Multicast: accepted retries (normalization, subset) and new keys
459
+ -- ===========================================================================
460
+ create temp table _rm as select * from agent_board.send_multicast_message(null, null, null, null, null) where false;
461
+ grant all on _rm to authenticated, service_role;
462
+ create function pg_temp.mc_row(p_who text, p_sql text) returns text language plpgsql as $$
463
+ begin
464
+ delete from _rm;
465
+ return pg_temp.as_(p_who, 'insert into _rm ' || p_sql);
466
+ end $$;
467
+ select pg_temp.put('fp.before-mc-retry', pg_temp.fp_rows());
468
+ select pg_temp.put('r30', pg_temp.mc_row('A', pg_temp.mc('A', 'T', pg_temp.ids('C2', 'C', 'C2', 'C'), 'mk', 'MC-RETRY')));
469
+ -- 30 -- the same set, reordered and duplicated
470
+ select is(
471
+ pg_temp.v('r30')
472
+ || ' ' || ((select message_id from _rm) = pg_temp.u('MC'))::text
473
+ || ' ' || (select delivered_recipient_count || '/' || inbox_item_count || '/' || callback_outbox_count from _rm)
474
+ || ' ' || pg_temp.scope('MC'),
475
+ 'ok true 2/2/0 f2b717-c:multicast:private,f2b717-c2:multicast:private',
476
+ 'multicast: the same set reordered and duplicated reuses the message, re-touches two deliveries, queues no new callback');
477
+ -- 31
478
+ select is(
479
+ pg_temp.fp_rows(), pg_temp.v('fp.before-mc-retry'),
480
+ 'multicast: the accepted normalized retry wrote no new row anywhere');
481
+ select pg_temp.put('r32', pg_temp.mc_row('A', pg_temp.mc('A', 'T', pg_temp.ids('C'), 'mk', 'MC-SUBSET')));
482
+ -- 32 -- a nonempty subset: accepted, and C2's delivery is kept, not revoked
483
+ select is(
484
+ pg_temp.v('r32')
485
+ || ' ' || ((select message_id from _rm) = pg_temp.u('MC'))::text
486
+ || ' ' || (select delivered_recipient_count::text from _rm)
487
+ || ' ' || pg_temp.scope('MC') || ' | ' || pg_temp.sees('C2', 'MC'),
488
+ 'ok true 1 f2b717-c:multicast:private,f2b717-c2:multicast:private | 1/1/1/1/1/0 A-PRIVATE-MC-BODY',
489
+ 'multicast: a subset retry is accepted; the omitted recipient keeps its delivery and its access');
490
+ -- 33 -- addressing overload with the existing set: accepted on the source
491
+ select is(
492
+ pg_temp.as_('A', format($$ select * from agent_board.send_multicast_message(%L::uuid, %L::uuid, %L::jsonb, 'addressing_v1', 's', 'MC-ADDR', 'comment', 'medium', 'mk', '{}'::jsonb) $$,
493
+ pg_temp.v('T'), pg_temp.v('A'), pg_temp.desc_('C', 'C2')::text))
494
+ || ' ' || pg_temp.scope('MC'),
495
+ 'ok f2b717-c:multicast:private,f2b717-c2:multicast:private',
496
+ 'multicast, addressing overload: the existing set is accepted, scope unchanged');
497
+ select pg_temp.put('r34', pg_temp.as_('SVC', pg_temp.signed('signed_send_multicast_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
498
+ 'p_recipient_agent_ids', jsonb_build_array(pg_temp.v('C2')), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'mk')))
499
+ || ' ' || pg_temp.as_('SVC', pg_temp.signed('signed_send_multicast_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A'),
500
+ 'p_recipient_descriptors', pg_temp.desc_('C', 'C2'), 'p_addressing_mode', 'addressing_v1', 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'mk'))));
501
+ -- 34 -- both signed branches with the existing set: accepted, one evidence row each
502
+ select is(
503
+ pg_temp.v('r34')
504
+ || ' ' || pg_temp.scope('MC')
505
+ || ' ' || (select count(*) from agent_board.signature_evidence where source_message_id = pg_temp.u('MC')),
506
+ 'ok ok f2b717-c:multicast:private,f2b717-c2:multicast:private 2',
507
+ 'multicast, both signed branches: retries within the set are accepted on the source with their evidence, no new scope');
508
+ -- 35 -- a new key: a new message, no copied artifact
509
+ select pg_temp.put('MC.new', pg_temp.val('A', replace(pg_temp.mc('A', 'T', pg_temp.ids('C', 'X'), 'mk2', 'MC-NEW-KEY'), 'select *', 'select message_id::text')));
510
+ select is(
511
+ (pg_temp.v('MC.new') is distinct from pg_temp.v('MC'))::text || ' ' || pg_temp.scope('MC.new') || ' | ' || pg_temp.sees('X', 'MC.new') || ' | ' || pg_temp.sees('X', 'MC'),
512
+ 'true f2b717-c:multicast:private,f2b717-x:multicast:private | 1/0/1/1/1/0 MC-NEW-KEY | 0/0/0/0/0/0 -',
513
+ 'multicast: a new key to {C, X} is a new message with no artifact; X still sees nothing of MC');
514
+ -- 36
515
+ select pg_temp.put('MC.a2', pg_temp.val('A2', replace(pg_temp.mc('A2', 'T', pg_temp.ids('X'), 'mk', 'A2-MC'), 'select *', 'select message_id::text')));
516
+ select is(
517
+ (pg_temp.u('MC.a2') is not null and pg_temp.v('MC.a2') not in (pg_temp.v('MC'), pg_temp.v('MC.new')))::text || ' ' || pg_temp.scope('MC.a2') || ' ' || pg_temp.sees('X', 'MC'),
518
+ 'true f2b717-x:multicast:private 0/0/0/0/0/0 -',
519
+ 'multicast: the same key used by another author is its own message; MC stays hidden from X');
520
+
521
+ -- ===========================================================================
522
+ -- After every attempt: the sources are exactly as they were and invisible to X, M and Y
523
+ -- ===========================================================================
524
+ -- 37
525
+ select is(pg_temp.history('DM') || ' ' || pg_temp.history('MC') || ' ' || pg_temp.history('DMB'),
526
+ pg_temp.v('DM.history') || ' ' || pg_temp.v('MC.history') || ' ' || pg_temp.v('DMB.history'),
527
+ 'after: DM, MC and DMB keep their ids, author, body, metadata, artifacts and delivery rows');
528
+ -- 38
529
+ select is(pg_temp.sees('X', 'DM') || ' | ' || pg_temp.sees('X', 'MC') || ' | ' || pg_temp.sees('X', 'DMB'),
530
+ '0/0/0/0/0/0 - | 0/0/0/0/0/0 - | 0/0/0/0/0/0 -',
531
+ 'after: X sees none of the three sources or the artifacts C anchored to them');
532
+ -- 39
533
+ select is(pg_temp.sees('M', 'DMB') || ' | ' || pg_temp.sees('Y', 'DMB') || ' | ' || pg_temp.sees('M', 'DM'),
534
+ '0/0/0/0/0/0 - | 0/0/0/0/0/0 - | 0/0/0/0/0/0 -',
535
+ 'after: board members still see nothing of the private DM on the board-stamped thread');
536
+ -- 40 -- old recipients keep exactly their access
537
+ select is(pg_temp.sees('C', 'DM') || ' | ' || pg_temp.sees('C', 'MC') || ' | ' || pg_temp.sees('C2', 'MC') || ' | ' || pg_temp.sees('C', 'DMB'),
538
+ '1/1/1/1/1/1 A-PRIVATE-DM-BODY | 1/1/1/1/1/2 A-PRIVATE-MC-BODY | 1/1/1/1/1/2 A-PRIVATE-MC-BODY | 1/1/1/1/1/0 A-PRIVATE-BOARD-THREAD-DM',
539
+ 'after: C and C2 keep exactly their own access (plus the evidence rows of the accepted signed retries 18 and 34)');
540
+ -- 41
541
+ select is(
542
+ (select count(*)::int from agent_board.inbox_items where source_message_id in (pg_temp.u('DM'), pg_temp.u('MC'), pg_temp.u('DMB')))
543
+ || '/' || (select count(*)::int from agent_board.callback_outbox_events where source_message_id in (pg_temp.u('DM'), pg_temp.u('MC'), pg_temp.u('DMB'))),
544
+ '4/4',
545
+ 'after: the sources have exactly their four original inbox items and four callbacks (no retry duplicated one)');
546
+
547
+ -- ===========================================================================
548
+ -- Preserved behaviour beside the ceiling
549
+ -- ===========================================================================
550
+ -- 42 -- canonical reply on an inherited board thread still fans out to the board (#661)
551
+ select pg_temp.put('REPLY', pg_temp.val('A', format($$ select message_id::text from agent_board.post_message(%L, %L, 'BOARD-REPLY') $$, pg_temp.v('TB'), pg_temp.v('A'))));
552
+ select is(pg_temp.sees('M', 'REPLY'),
553
+ '1/0/4/4/1/0 BOARD-REPLY',
554
+ 'preserved: a reply on the board-stamped thread still fans out to B (#661); M reads it and the board-scoped delivery/inbox rows, and its own callback');
555
+ -- 43 -- post_message has no replay: the same reserved metadata twice is two messages
556
+ select is(
557
+ (pg_temp.val('A', format($$ select message_id::text from agent_board.post_message(%L, %L, 'R', 'comment', null, %L::jsonb) $$,
558
+ pg_temp.v('T'), pg_temp.v('A'), jsonb_build_object('rpcName', 'send_direct_message', 'idempotencyKey', 'pk')::text))
559
+ <> pg_temp.val('A', format($$ select message_id::text from agent_board.post_message(%L, %L, 'R', 'comment', null, %L::jsonb) $$,
560
+ pg_temp.v('T'), pg_temp.v('A'), jsonb_build_object('rpcName', 'send_direct_message', 'idempotencyKey', 'pk')::text)))::text,
561
+ 'true',
562
+ 'no-replay disposition: post_message never reuses a message, even under identical reserved metadata');
563
+ -- 44 -- the one-board disposition: a same-key board post cannot move to another board
564
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
565
+ values ('717b2000-0000-4000-8000-000000000002', 'f2b717-b2', 'F2b B2', pg_temp.u('A'), 'private', 'active');
566
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
567
+ values ('717b2000-0000-4000-8000-000000000002', pg_temp.u('A'), 'owner', 'joined', pg_temp.u('A'));
568
+ select is(pg_temp.refuse('A', format($$ select * from agent_board.post_board_message(%L, %L, %L, 's', 'moved', 'comment', 'medium', 'pbk') $$,
569
+ pg_temp.v('TB'), pg_temp.v('A'), '717b2000-0000-4000-8000-000000000002')),
570
+ '42501:thread belongs to a different board | side-effects=none',
571
+ 'one-board disposition: a board post on TB cannot target another board');
572
+ -- 45 -- service_role acting for A on an identical retry is still accepted on the source
573
+ select is(
574
+ pg_temp.as_('SVC', pg_temp.dm('A', 'T', 'C', 'dk', 'SVC-RETRY')) || ' ' || pg_temp.scope('DM'),
575
+ 'ok f2b717-c:direct:private',
576
+ 'direct, service_role: an identical retry for the supplied author is accepted, scope unchanged');
577
+ -- 46 -- A2's key dk already reached X (20), so a service-role replay of A2/dk to C names a NEW
578
+ -- recipient of A2's message and is refused
579
+ select is(pg_temp.refuse('SVC', pg_temp.dm('A2', 'T', 'C', 'dk')),
580
+ '42501:message replay cannot widen delivery scope | side-effects=none',
581
+ 'direct: the ceiling is per source author: A2''s own dk message cannot be widened to C either');
582
+
583
+ -- ===========================================================================
584
+ -- The cores' catalog surface
585
+ -- ===========================================================================
586
+ -- Lock identity and refusal placement are pinned BEHAVIOURALLY: tests/isolation/specs/
587
+ -- message_replay_scope.spec (a lock keyed differently or removed lets the conflicting send through,
588
+ -- or lets identical retries duplicate), 20/36 here (another author is a separate identity), and
589
+ -- every refusal above reporting side-effects=none.
590
+ -- 47
591
+ select is(
592
+ (select string_agg(p.proname || ':' || p.prosecdef::text || '/' || coalesce(array_to_string(p.proconfig, ','), '')
593
+ || ' ' || (select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
594
+ from unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r), ' | ' order by p.proname)
595
+ from pg_proc p
596
+ where p.oid in ('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure,
597
+ 'agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure)),
598
+ 'send_direct_message:true/search_path=agent_board, public agent_board_delegate=false anon=false authenticated=true service_role=true'
599
+ || ' | send_multicast_message:true/search_path=agent_board, public agent_board_delegate=false anon=false authenticated=true service_role=true',
600
+ 'both cores stay SECURITY DEFINER with their search_path, EXECUTE for authenticated and service_role only');
601
+ -- 48
602
+ select ok(
603
+ obj_description('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure, 'pg_proc') ~ 'cannot widen the message''s audience'
604
+ and obj_description('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure, 'pg_proc') ~ 'cannot widen the message''s audience',
605
+ 'both cores'' comments state the replay ceiling');
606
+
607
+ -- ===========================================================================
608
+ -- THE CLASS, derived from the catalog (AGENTS.md rule 1)
609
+ -- ===========================================================================
610
+ -- Three derived sets, unioned, each read from pg_proc through the shared matcher family
611
+ -- (tests/fixtures/test-prereqs.sql), never a regex written here:
612
+ -- WRITERS every agent_board routine whose literal-stripped body writes delivery_recipients or
613
+ -- inbox_items (writes_any_re: INSERT/UPDATE/DELETE/MERGE/COPY, every spelling CPR-0 pins);
614
+ -- CLOSURE every routine that reaches a writer through any chain of calls (fn_call_closure, by
615
+ -- name, so every overload of a matched name is included: over-inclusive by design);
616
+ -- REPLAY every routine that serializes through _lock_idempotency_key, or whose code (literals
617
+ -- kept, comments stripped) names the 'idempotencyKey' metadata key the replay lookups
618
+ -- read back -- a lookup by that key cannot be written without naming it.
619
+ -- BOUNDARY: agent_board routines only. Calls are matched by name in stripped bodies, so dynamic SQL
620
+ -- that names a writer is not followed: 52 asserts no routine in the class contains an EXECUTE at
621
+ -- all, so that hole names itself rather than hides. Views and RLS policies are not routines and
622
+ -- cannot write; out-of-schema routines are #717 F1's census (DWX), not repeated here.
623
+ -- Each member has ONE disposition, each pinned positively:
624
+ -- ceiling the two cores: refusals 6-15, 21-29 here
625
+ -- routes-core a wrapper whose body calls its core and writes nothing itself (51), and whose
626
+ -- widening is refused here (8, 9, 24, 25, 26)
627
+ -- internal not EXECUTE-able by anon/authenticated/agent_board_delegate (50); service_role
628
+ -- EXECUTE on _upsert_delivery_recipient/_upsert_inbox_item is #717 F1's to revoke
629
+ -- and is REPORTED in the value, not hidden
630
+ -- one-board post_board_message family: 44
631
+ -- no-replay post_message: 43 (two identical calls, two messages)
632
+ -- recipient-state update_inbox_item_state: UPDATEs the caller's OWN inbox item only (63)
633
+ -- tasking-f2 create_tasking_delivery family: #717 F2's ceiling (separate PR); NOT claimed here
634
+ create temp table _class(signature text primary key, disposition text not null);
635
+ insert into _class values
636
+ ('send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'ceiling'),
637
+ ('send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)', 'ceiling'),
638
+ ('send_direct_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)', 'routes-core:send_direct_message'),
639
+ ('send_multicast_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)', 'routes-core:send_multicast_message'),
640
+ ('signed_send_direct_message(jsonb,jsonb)', 'routes-core:send_direct_message'),
641
+ ('signed_send_multicast_message(jsonb,jsonb)', 'routes-core:send_multicast_message'),
642
+ ('_upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)', 'internal'),
643
+ ('_upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)', 'internal'),
644
+ ('_deliver_board_message(uuid,text,text,text)', 'internal'),
645
+ ('post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'one-board'),
646
+ ('post_board_message(uuid,uuid,text,text,text,text,text,text,text,jsonb)', 'one-board'),
647
+ ('signed_post_board_message(jsonb,jsonb)', 'one-board'),
648
+ ('post_message(uuid,uuid,text,text,uuid,jsonb)', 'no-replay'),
649
+ ('update_inbox_item_state(uuid,uuid,text,jsonb)', 'recipient-state'),
650
+ ('create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)', 'tasking-f2'),
651
+ ('create_tasking_delivery(uuid,uuid,text,text,jsonb,text,text,text,text,text,jsonb)', 'tasking-f2'),
652
+ ('signed_create_tasking_delivery(jsonb,jsonb)', 'tasking-f2');
653
+
654
+ create function pg_temp.derived() returns table(signature text, body text) language sql as $d$
655
+ with fns as (
656
+ select p.oid, p.proname::text as proname,
657
+ p.proname || '(' || replace(oidvectortypes(p.proargtypes), ' ', '') || ')' as signature,
658
+ agent_board_test.stripped_body(p.oid) as body,
659
+ agent_board_test.stripped_body(p.oid, true) as lit_body
660
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
661
+ where n.nspname = 'agent_board' and p.prokind in ('f', 'p')
662
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')),
663
+ writers as (
664
+ select oid from fns where body ~ agent_board_test.writes_any_re(array['delivery_recipients', 'inbox_items'])),
665
+ closure as (
666
+ select c.root_oid as oid from agent_board_test.fn_call_closure() c where c.reached_oid in (select oid from writers)),
667
+ replay as (
668
+ select oid from fns
669
+ where body ~ agent_board_test.call_re('_lock_idempotency_key')
670
+ or lit_body ~ 'idempotencyKey')
671
+ select f.signature, f.body
672
+ from fns f
673
+ where (f.oid in (select oid from closure) or f.oid in (select oid from replay))
674
+ and f.proname <> '_lock_idempotency_key'
675
+ $d$;
676
+ create temp table _derived as select * from pg_temp.derived();
677
+ -- 49
678
+ select is(
679
+ (select coalesce(string_agg(signature, ' ' order by signature), '(none)') from (
680
+ select signature from _derived except select signature from _class) d)
681
+ || ' || stale: ' ||
682
+ (select coalesce(string_agg(signature, ' ' order by signature), '(none)') from (
683
+ select signature from _class except select signature from _derived) d),
684
+ '(none) || stale: (none)',
685
+ 'CLASS: the derived writer/caller/replay class equals the dispositioned set exactly (a new site, or a stale exemption, names itself)');
686
+ -- 50 -- internal: positively, no end-user API role can execute it; service_role reported as found
687
+ select is(
688
+ (select string_agg(c.signature || '=' || coalesce((select string_agg(r, ',' order by r) from unnest(array['anon', 'authenticated', 'agent_board_delegate', 'service_role']) r
689
+ where has_function_privilege(r, ('agent_board.' || c.signature)::regprocedure, 'EXECUTE')), '-'), ' ' order by c.signature)
690
+ from _class c where c.disposition = 'internal'),
691
+ '_deliver_board_message(uuid,text,text,text)=-'
692
+ || ' _upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)=' || case when has_function_privilege('service_role', 'agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)', 'EXECUTE') then 'service_role' else '-' end
693
+ || ' _upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)=' || case when has_function_privilege('service_role', 'agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)', 'EXECUTE') then 'service_role' else '-' end,
694
+ 'internal writers: anon, authenticated and agent_board_delegate cannot execute any of them (service_role reported; #717 F1 revokes it)');
695
+ -- 51 -- routes-core: each wrapper calls its core and writes no delivery/inbox row itself
696
+ select is(
697
+ (select coalesce(string_agg(c.signature, ' ' order by c.signature), '(none)')
698
+ from _class c join _derived d on d.signature = c.signature
699
+ where c.disposition like 'routes-core:%'
700
+ and not (d.body ~ agent_board_test.call_re(split_part(c.disposition, ':', 2))
701
+ and d.body !~ agent_board_test.writes_any_re(array['delivery_recipients', 'inbox_items'])
702
+ and d.body !~ agent_board_test.call_re('_upsert_delivery_recipient')
703
+ and d.body !~ agent_board_test.call_re('_upsert_inbox_item'))),
704
+ '(none)',
705
+ 'routes-core: every wrapper disposition calls its named core and writes no delivery itself');
706
+ -- 52 -- the boundary: no member of the class contains an EXECUTE (dynamic SQL a name match cannot follow)
707
+ select is(
708
+ (select coalesce(string_agg(signature, ' ' order by signature), '(none)') from _derived
709
+ where body ~ agent_board_test.re(agent_board_test.lead_guard() || agent_board_test.ci_re('execute') || agent_board_test.trail_guard())),
710
+ '(none)',
711
+ 'BOUNDARY: no routine in the class contains EXECUTE, so name-matched call closure is complete for it');
712
+
713
+ -- 53 -- discriminance of the derivation itself: it must name a planted site of each kind. The
714
+ -- plants live only inside this rolled-back transaction and are dropped straight after.
715
+ create function agent_board.zz_f2b_probe_writer(p uuid) returns void language plpgsql as $f$
716
+ begin update agent_board.inbox_items set subject = subject where id = p; end $f$;
717
+ create function agent_board.zz_f2b_probe_caller() returns void language plpgsql as $f$
718
+ begin perform agent_board._upsert_delivery_recipient('direct', null, 'message'); end $f$;
719
+ create function agent_board.zz_f2b_probe_replay(p text) returns void language plpgsql as $f$
720
+ begin perform 1 from agent_board.messages where metadata ->> 'idempotencyKey' = p; end $f$;
721
+ select is(
722
+ (select string_agg(signature, ' ' order by signature) from pg_temp.derived() where signature like 'zz\_f2b\_probe\_%'),
723
+ 'zz_f2b_probe_caller() zz_f2b_probe_replay(text) zz_f2b_probe_writer(uuid)',
724
+ 'DISCRIMINANCE: the derivation names a planted writer, a planted caller of a writer and a planted replay lookup');
725
+ drop function agent_board.zz_f2b_probe_writer(uuid);
726
+ drop function agent_board.zz_f2b_probe_caller();
727
+ drop function agent_board.zz_f2b_probe_replay(text);
728
+
729
+ -- ===========================================================================
730
+ -- Direct-call controls for the signed wrappers
731
+ -- ===========================================================================
732
+ -- 54
733
+ select is(pg_temp.refuse('A', pg_temp.signed('signed_send_direct_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_recipient_agent_id', pg_temp.v('X'), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'dk')))
734
+ || ' || ' || pg_temp.refuse('A', pg_temp.signed('signed_send_multicast_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_recipient_agent_ids', jsonb_build_array(pg_temp.v('X')), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'mk'))),
735
+ '42501:permission denied for function signed_send_direct_message | side-effects=none || 42501:permission denied for function signed_send_multicast_message | side-effects=none',
736
+ 'signed wrappers: an authenticated agent cannot call them directly');
737
+ -- 55
738
+ select is(pg_temp.refuse('SVC', pg_temp.signed('signed_send_direct_message', jsonb_build_object('p_thread_id', pg_temp.v('T'), 'p_author_agent_id', pg_temp.v('A2'),
739
+ 'p_recipient_agent_id', pg_temp.v('X'), 'p_subject', 's', 'p_body', 'b', 'p_idempotency_key', 'dk'))),
740
+ '42501:actor_mismatch: signed write payload actor must match gateway actor | side-effects=none',
741
+ 'signed: a payload author that differs from the gateway actor is refused');
742
+
743
+ -- ===========================================================================
744
+ -- Visibility census: every authenticated read path to a message-anchored row follows deliveries
745
+ -- ===========================================================================
746
+ -- 56 -- the tables an authenticated agent can read that carry a message reference, from the catalog
747
+ select is(
748
+ (select string_agg(distinct c.conrelid::regclass::text, ' ' order by c.conrelid::regclass::text)
749
+ from pg_constraint c
750
+ where c.contype = 'f' and c.confrelid = 'agent_board.messages'::regclass
751
+ and has_table_privilege('authenticated', c.conrelid, 'SELECT')),
752
+ 'artifacts callback_outbox_events delivery_recipients inbox_items signature_evidence',
753
+ 'CLASS: the authenticated-readable tables referencing messages, from pg_constraint: each one is counted by sees()');
754
+ -- 57 -- the same for hosted delegates (they cannot be addressed privately; their read paths are board-scoped)
755
+ select is(
756
+ (select coalesce(string_agg(distinct c.conrelid::regclass::text, ' ' order by c.conrelid::regclass::text), '(none)')
757
+ from pg_constraint c
758
+ where c.contype = 'f' and c.confrelid = 'agent_board.messages'::regclass
759
+ and has_table_privilege('agent_board_delegate', c.conrelid, 'SELECT')),
760
+ 'delivery_recipients inbox_items signature_evidence',
761
+ 'CLASS: the delegate-readable tables referencing messages are a subset of what sees() counts');
762
+ -- 58
763
+ select is(pg_temp.sees('X', 'DM.a2') || ' | ' || pg_temp.sees('X', 'MC.a2'),
764
+ '1/0/1/1/1/0 A2-BODY | 1/0/1/1/1/0 A2-MC',
765
+ 'separate author: X sees A2''s own messages (no artifacts), which carry nothing of A''s');
766
+ -- 59
767
+ select is(pg_temp.sees('C', 'DM.a2'),
768
+ '0/0/0/0/0/0 -',
769
+ 'separate author: the refused service-role widening of A2''s message to C wrote nothing C can see');
770
+ -- 60
771
+ select is((select count(*)::int from agent_board.messages where thread_id = pg_temp.u('T') and metadata ->> 'idempotencyKey' = 'dk'),
772
+ 2,
773
+ 'identity: exactly two dk messages on T, one per author (A, A2); no retry minted another');
774
+ -- 61
775
+ select is((select count(*)::int from agent_board.messages where thread_id = pg_temp.u('T') and metadata ->> 'idempotencyKey' = 'mk'),
776
+ 2,
777
+ 'identity: exactly two mk messages on T, one per author');
778
+ -- 62
779
+ select is(pg_temp.sees('C', 'DM'), '1/1/1/1/1/1 A-PRIVATE-DM-BODY', 'after: C still reads DM after the signed retry and every refusal');
780
+
781
+ -- Snapshot protected inbox fields across every row: state, metadata and timestamp may change,
782
+ -- but a private helper reached by the state RPC must not alter content, addressing or visibility.
783
+ create function pg_temp.protected_inbox() returns text language sql as $$
784
+ select coalesce(jsonb_agg(to_jsonb(i) - array['action_state','metadata','updated_at'] order by i.id), '[]'::jsonb)::text
785
+ from agent_board.inbox_items i
786
+ $$;
787
+ select pg_temp.put('state.protected.before', pg_temp.protected_inbox());
788
+
789
+ -- 63 -- recipient-state disposition: another agent cannot touch C's inbox item; C can, and no scope moves
790
+ select pg_temp.put('C.dm.inbox', (select id::text from agent_board.inbox_items where source_message_id = pg_temp.u('DM') and recipient_agent_id = pg_temp.u('C')));
791
+ select is(
792
+ pg_temp.refuse('X', format($$ select * from agent_board.update_inbox_item_state(%L, %L, 'read') $$, pg_temp.v('C.dm.inbox'), pg_temp.v('X')))
793
+ || ' || ' || pg_temp.as_('C', format($$ select * from agent_board.update_inbox_item_state(%L, %L, 'read') $$, pg_temp.v('C.dm.inbox'), pg_temp.v('C')))
794
+ || ' ' || pg_temp.scope('DM'),
795
+ 'P0002:inbox item not found | side-effects=none || ok f2b717-c:direct:private',
796
+ 'recipient-state disposition: update_inbox_item_state reaches only the caller''s own item and creates no delivery');
797
+ select pg_temp.put('state.protected.after', pg_temp.protected_inbox());
798
+
799
+ -- 64 -- why the cores' `board_id is null` conjunct cannot be mutation-pinned on its own: the table's
800
+ -- delivery_recipients_board_scope CHECK already forbids a board-scoped direct or multicast row, so a
801
+ -- matching-mode delivery is private by construction. Dropping the conjunct alone is an EQUIVALENT
802
+ -- mutation (receipt M6, 0 failures); this assertion pins the constraint it relies on (receipt C6).
803
+ create function pg_temp.try_owner(p_sql text) returns text language plpgsql as $$
804
+ begin execute p_sql; return 'ok'; exception when others then return sqlstate; end $$;
805
+ select is(
806
+ pg_temp.try_owner(format($$ insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id, board_membership_id)
807
+ values ('direct', %L, 'message', %L, %L, (select id from agent_board.board_memberships where board_id = %L and agent_id = %L)) $$,
808
+ pg_temp.v('M'), pg_temp.v('DMB'), pg_temp.v('B'), pg_temp.v('B'), pg_temp.v('M')))
809
+ || ' ' || pg_temp.try_owner(format($$ insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id, board_membership_id)
810
+ values ('multicast', %L, 'message', %L, %L, (select id from agent_board.board_memberships where board_id = %L and agent_id = %L)) $$,
811
+ pg_temp.v('M'), pg_temp.v('DMB'), pg_temp.v('B'), pg_temp.v('B'), pg_temp.v('M'))),
812
+ '23514 23514',
813
+ 'the table CHECK forbids a board-scoped direct or multicast delivery, so a matching-mode proof is private by construction');
814
+
815
+ -- 65: protected content proof covers the actual call including every private helper it reaches.
816
+ select is(pg_temp.v('state.protected.after'), pg_temp.v('state.protected.before'),
817
+ 'recipient-state disposition: the successful update and all reached helpers preserve every inbox row protected field');
818
+ -- X already received unrelated multicast messages through the new-key and separate-author cases.
819
+ -- 66: their delivery rows cannot establish scope for A's original MC message.
820
+ select is(pg_temp.refuse('A', pg_temp.mc('A','T',pg_temp.ids('X'),'mk')),
821
+ '42501:message replay cannot widen delivery scope | side-effects=none',
822
+ 'multicast source proof: X holding unrelated multicast cannot authorize delivery of the original message');
823
+ -- 67
824
+ select is(pg_temp.sees('X','MC'), '0/0/0/0/0/0 -',
825
+ 'multicast source proof: the original body and C-authored artifact remain invisible after unrelated-delivery replay');
826
+
827
+ select * from finish();
828
+ rollback;