@consiliency/agent-board-schema 1.64.0 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +19 -0
  2. package/fixtures/already-applied.json +44 -4
  3. package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
  4. package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
  5. package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
  6. package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
  7. package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
  8. package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
  9. package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
  10. package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
  11. package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
  12. package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
  13. package/package.json +1 -1
  14. package/schema-manifest.json +93 -3
  15. package/src/deployment-drift-main.test.mjs +156 -0
  16. package/src/deployment-drift-script.test.mjs +14 -1
  17. package/src/policy-comment.test.mjs +17 -0
  18. package/src/tooling.test.mjs +12 -2
  19. package/tests/agent_board_661_reply_delivery.test.sql +185 -0
  20. package/tests/agent_board_667_boundary.test.sql +162 -0
  21. package/tests/agent_board_667_conversion_branches.test.sql +91 -0
  22. package/tests/agent_board_667_conversion_profile.test.sql +112 -0
  23. package/tests/agent_board_667_history_class.test.sql +785 -0
  24. package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
  25. package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
  26. package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
  27. package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
  28. package/tests/agent_board_667_retention.test.sql +201 -0
  29. package/tests/agent_board_667_routing.test.sql +294 -0
  30. package/tests/agent_board_667_sender_effects.test.sql +100 -0
  31. package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
  32. package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
  33. package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
  34. package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
  35. package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
  36. package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
  37. package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
  38. package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
  39. package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
  40. package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
  41. package/tests/agent_board_667_writers.test.sql +281 -0
  42. package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
  43. package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
  44. package/tests/agent_board_addressing.test.sql +8 -5
  45. package/tests/agent_board_claim_subject_gate.test.sql +7 -3
  46. package/tests/agent_board_claimpolicy_race.test.sql +39 -11
  47. package/tests/agent_board_consent_gating.test.sql +4 -4
  48. package/tests/agent_board_delegate_role_class.test.sql +33 -15
  49. package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
  50. package/tests/agent_board_fk_lock_edges.test.sql +3 -0
  51. package/tests/agent_board_gatenull_membstate.test.sql +7 -4
  52. package/tests/agent_board_hosted_delegates.test.sql +5 -2
  53. package/tests/agent_board_inbox_rpcs.test.sql +5 -3
  54. package/tests/agent_board_obsgate.test.sql +8 -4
  55. package/tests/agent_board_obsgate4.test.sql +2 -2
  56. package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
  57. package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
  58. package/tests/agent_board_policy_correlation.test.sql +189 -0
  59. package/tests/agent_board_request_removed.test.sql +96 -0
  60. package/tests/agent_board_resolution_authority.test.sql +190 -0
  61. package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
  62. package/tests/agent_board_session_identity_limits.test.sql +1 -1
  63. package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
  64. package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
  65. package/tests/agent_board_session_provenance.test.sql +106 -0
  66. package/tests/agent_board_sidserver_authority.test.sql +69 -0
  67. package/tests/agent_board_sidserver_catalog.test.sql +47 -0
  68. package/tests/agent_board_signed_write_gateway.test.sql +4 -2
  69. package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
  70. package/tests/agent_board_v2_invariants.test.sql +5 -2
  71. package/tests/agent_board_write_enforcement_class.test.sql +3 -3
  72. package/tests/catalog/function_execute_privileges.json +43 -7
  73. package/tests/catalog/p_metadata_parameters.json +16 -0
  74. package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
  75. package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
  76. package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
  77. package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
  78. package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
  79. package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
  80. package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
  81. package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
  82. package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
  83. package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
  84. package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
  85. package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
  86. package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
  87. package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
  88. package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
  89. package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
  90. package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
  91. package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
  92. package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
  93. package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
  94. package/tests/isolation/expected/association_board_vs_board.out +71 -0
  95. package/tests/isolation/expected/association_post_wait_authority.out +77 -0
  96. package/tests/isolation/expected/association_strong_history_writers.out +203 -0
  97. package/tests/isolation/expected/association_strong_isolation.out +185 -0
  98. package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
  99. package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
  100. package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
  101. package/tests/isolation/expected/association_vs_private_message.out +67 -0
  102. package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
  103. package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
  104. package/tests/isolation/expected/consent_approval_lock.out +1 -1
  105. package/tests/isolation/expected/message_replay_scope.out +209 -0
  106. package/tests/isolation/expected/tasking_replay_scope.out +125 -0
  107. package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
  108. package/tests/isolation/specs/association_board_vs_board.spec +37 -0
  109. package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
  110. package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
  111. package/tests/isolation/specs/association_strong_isolation.spec +50 -0
  112. package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
  113. package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
  114. package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
  115. package/tests/isolation/specs/association_vs_private_message.spec +33 -0
  116. package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
  117. package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
  118. package/tests/isolation/specs/consent_approval_lock.spec +13 -7
  119. package/tests/isolation/specs/message_replay_scope.spec +77 -0
  120. package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
  121. package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
  122. package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
  123. package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
  124. package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
  125. package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
  126. package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
  127. package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
  128. package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
  129. package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
  130. package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
  131. package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
  132. package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
  133. package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
  134. package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
  135. package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
  136. package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
  137. package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
  138. package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
  139. package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
  140. package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
  141. package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
  142. package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
  143. package/tests/mutations/717-f1/run.sh +17 -0
  144. package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
  145. package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
  146. package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
  147. package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
  148. package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
  149. package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
  150. package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
  151. package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
  152. package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
  153. package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
  154. package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
  155. package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
  156. package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
  157. package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
  158. package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
  159. package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
  160. package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
  161. package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
  162. package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
  163. package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
  164. package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
  165. package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
  166. package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
  167. package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
  168. package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
  169. package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
  170. package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
  171. package/tests/mutations/717-f2b/run.sh +82 -0
@@ -0,0 +1 @@
1
+ grant execute on function agent_board._queue_inbox_callbacks(uuid,text) to service_role;
@@ -0,0 +1 @@
1
+ revoke execute on function agent_board.claim_callback_outbox_events(integer) from service_role;
@@ -0,0 +1 @@
1
+ create function agent_board.wo_invoker_probe(p uuid) returns integer language sql security invoker as $f$ select agent_board._queue_inbox_callbacks(p, 'inbox.item.delivered') $f$;
@@ -0,0 +1 @@
1
+ grant execute on function agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb) to public;
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: PG* env set; run.sh <test.sql> <mutation.sql>... Each mutation runs in ONE transaction with
3
+ # the test file and is rolled back; the agent_board catalog fingerprint must match before and after.
4
+ set -uo pipefail
5
+ TEST="$1"; shift
6
+ fp() { psql -qtAX -c "select md5(string_agg(p.oid::regprocedure::text||coalesce(p.proacl::text,'')||p.prosrc||coalesce(obj_description(p.oid,'pg_proc'),''), '|' order by p.oid::regprocedure::text)) from pg_proc p where p.pronamespace='agent_board'::regnamespace"; }
7
+ for m in "$@"; do
8
+ before="$(fp)"
9
+ out="$(printf 'begin;\n\\i %s\n\\i %s\nrollback;\n' "$m" "$TEST" | psql -qtA 2>&1)"
10
+ after="$(fp)"
11
+ plan="$(sed -nE 's/^1\.\.([0-9]+)$/\1/p' <<<"$out" | head -1)"
12
+ nok="$(grep -cE '^ok [0-9]+' <<<"$out")"; nnot="$(grep -cE '^not ok [0-9]+' <<<"$out")"
13
+ errs="$(grep -E '(ERROR|FATAL):' <<<"$out" | head -3 | tr '\n' ' ')"
14
+ labels="$(grep -E '^not ok [0-9]+' <<<"$out" | sed -E 's/^not ok ([0-9]+) - ([A-Z]+-[A-Za-z0-9-]+( CLASS)?).*/#\1:\2/' | tr '\n' ' ')"
15
+ [ "$before" = "$after" ] && fpok=restored || fpok="FINGERPRINT-CHANGED"
16
+ echo "$(basename "$m" .sql) | plan=${plan:-none} ok=$nok not_ok=$nnot emitted=$((nok+nnot)) | $fpok | red: ${labels:-<none>} ${errs:+| errors: $errs}"
17
+ done
@@ -0,0 +1,4 @@
1
+ -- a new API route that calls the delivery writer
2
+ create function agent_board.zz_f2b_new_route(p uuid) returns void language plpgsql security definer set search_path = agent_board, public as $f$
3
+ begin perform agent_board._upsert_delivery_recipient('direct', p, 'message', p); end $f$;
4
+ grant execute on function agent_board.zz_f2b_new_route(uuid) to authenticated;
@@ -0,0 +1,3 @@
1
+ -- a new routine that reads a message back by its idempotencyKey
2
+ create function agent_board.zz_f2b_new_lookup(p text) returns uuid language sql as $f$
3
+ select id from agent_board.messages where metadata ->> 'idempotencyKey' = p limit 1 $f$;
@@ -0,0 +1,2 @@
1
+ -- an internal delivery helper re-granted to authenticated
2
+ grant execute on function agent_board._deliver_board_message(uuid,text,text,text) to authenticated;
@@ -0,0 +1,9 @@
1
+ -- a routes-core wrapper that writes a delivery itself
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.signed_send_direct_message(jsonb,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ v_evidence_id := agent_board._record_signed_write_evidence($q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_direct_message(jsonb,jsonb): %', left($q$ v_evidence_id := agent_board._record_signed_write_evidence($q$, 60); end if;
4
+ n := replace(d, $q$ v_evidence_id := agent_board._record_signed_write_evidence($q$, $q$ if false then perform agent_board._upsert_delivery_recipient('direct', v_actor, 'message', v_row.message_id); end if;
5
+ v_evidence_id := agent_board._record_signed_write_evidence($q$);
6
+ if n = d then raise exception 'mutation is a no-op'; end if;
7
+ d := n;
8
+ execute d;
9
+ end $m$;
@@ -0,0 +1,9 @@
1
+ -- a class member that builds dynamic SQL (outside the name-match boundary)
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.signed_send_direct_message(jsonb,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ v_evidence_id := agent_board._record_signed_write_evidence($q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_direct_message(jsonb,jsonb): %', left($q$ v_evidence_id := agent_board._record_signed_write_evidence($q$, 60); end if;
4
+ n := replace(d, $q$ v_evidence_id := agent_board._record_signed_write_evidence($q$, $q$ if false then execute format('select 1'); end if;
5
+ v_evidence_id := agent_board._record_signed_write_evidence($q$);
6
+ if n = d then raise exception 'mutation is a no-op'; end if;
7
+ d := n;
8
+ execute d;
9
+ end $m$;
@@ -0,0 +1,2 @@
1
+ -- the delivery_recipients board-scope CHECK the cores' mode conjunct relies on is dropped
2
+ alter table agent_board.delivery_recipients drop constraint delivery_recipients_board_scope;
@@ -0,0 +1,14 @@
1
+ do $m$
2
+ declare d text := pg_get_functiondef('agent_board.update_inbox_item_state(uuid,uuid,text,jsonb)'::regprocedure);
3
+ anchor text := ' v_callback_count := agent_board._queue_inbox_callbacks';
4
+ begin
5
+ if position(anchor in d)=0 then raise exception 'missing anchor'; end if;
6
+ execute replace(d, anchor, $inject$
7
+ perform agent_board._upsert_inbox_item(
8
+ v_inbox.delivery_recipient_id, v_inbox.recipient_agent_id, v_inbox.board_id,
9
+ v_inbox.delivery_mode, v_inbox.source_kind, v_inbox.source_message_id,
10
+ v_inbox.source_task_id, v_inbox.source_delegation_id, v_inbox.subject,
11
+ 'REVIEW740-TRANSITIVE-CORRUPTION', v_inbox.priority, v_inbox.visible_in_inbox,
12
+ v_inbox.visible_in_board_feed, v_inbox.fetch_descriptor, v_inbox.metadata);
13
+ $inject$ || anchor);
14
+ end $m$;
@@ -0,0 +1,8 @@
1
+ -- direct: the idempotency lock is keyed by recipient (split domain)
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$, 60); end if;
4
+ n := replace(d, $q$ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$, $q$ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_recipient_agent_id::text,$q$);
5
+ if n = d then raise exception 'mutation is a no-op'; end if;
6
+ d := n;
7
+ execute d;
8
+ end $m$;
@@ -0,0 +1,12 @@
1
+ -- direct: the idempotency lock is removed
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ perform agent_board._lock_idempotency_key(
4
+ 'send_direct_message:'$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ perform agent_board._lock_idempotency_key(
5
+ 'send_direct_message:'$q$, 60); end if;
6
+ n := replace(d, $q$ perform agent_board._lock_idempotency_key(
7
+ 'send_direct_message:'$q$, $q$ perform concat(
8
+ 'send_direct_message:'$q$);
9
+ if n = d then raise exception 'mutation is a no-op'; end if;
10
+ d := n;
11
+ execute d;
12
+ end $m$;
@@ -0,0 +1,8 @@
1
+ -- multicast: the idempotency lock is keyed by the recipient set (split domain)
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$, 60); end if;
4
+ n := replace(d, $q$ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,$q$, $q$ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || array_to_string(v_recipient_ids, ','),$q$);
5
+ if n = d then raise exception 'mutation is a no-op'; end if;
6
+ d := n;
7
+ execute d;
8
+ end $m$;
@@ -0,0 +1,12 @@
1
+ -- multicast: the idempotency lock is removed
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ perform agent_board._lock_idempotency_key(
4
+ 'send_multicast_message:'$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ perform agent_board._lock_idempotency_key(
5
+ 'send_multicast_message:'$q$, 60); end if;
6
+ n := replace(d, $q$ perform agent_board._lock_idempotency_key(
7
+ 'send_multicast_message:'$q$, $q$ perform concat(
8
+ 'send_multicast_message:'$q$);
9
+ if n = d then raise exception 'mutation is a no-op'; end if;
10
+ d := n;
11
+ execute d;
12
+ end $m$;
@@ -0,0 +1,17 @@
1
+ -- pre-fix send_direct_message + send_multicast_message (exploit baseline)
2
+ do $m$
3
+ declare d text; s int; e int;
4
+ begin
5
+ foreach d in array array[pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure), pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure)] loop
6
+ s := position(' -- #717 F2b:' in d);
7
+ e := position(' raise exception ''message replay cannot widen delivery scope'' using errcode = ''42501'';
8
+ end if;
9
+
10
+ ' in d);
11
+ if s = 0 or e = 0 or e < s then raise exception 'M0 anchors not found'; end if;
12
+ execute left(d, s - 1) || substr(d, e + length(' raise exception ''message replay cannot widen delivery scope'' using errcode = ''42501'';
13
+ end if;
14
+
15
+ '));
16
+ end loop;
17
+ end $m$;
@@ -0,0 +1,15 @@
1
+ -- multicast: the replay lookup is widened across authors
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ where thread_id = p_thread_id
4
+ and author_agent_id = v_actor_agent_id
5
+ $q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ where thread_id = p_thread_id
6
+ and author_agent_id = v_actor_agent_id
7
+ $q$, 60); end if;
8
+ n := replace(d, $q$ where thread_id = p_thread_id
9
+ and author_agent_id = v_actor_agent_id
10
+ $q$, $q$ where thread_id = p_thread_id
11
+ $q$);
12
+ if n = d then raise exception 'mutation is a no-op'; end if;
13
+ d := n;
14
+ execute d;
15
+ end $m$;
@@ -0,0 +1,8 @@
1
+ -- direct: the core swallows its refusal and returns nothing
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$, 60); end if;
4
+ n := replace(d, $q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$, $q$ return;$q$);
5
+ if n = d then raise exception 'mutation is a no-op'; end if;
6
+ d := n;
7
+ execute d;
8
+ end $m$;
@@ -0,0 +1,8 @@
1
+ -- multicast: the core swallows its refusal and returns nothing
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$, 60); end if;
4
+ n := replace(d, $q$ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';$q$, $q$ return;$q$);
5
+ if n = d then raise exception 'mutation is a no-op'; end if;
6
+ d := n;
7
+ execute d;
8
+ end $m$;
@@ -0,0 +1,32 @@
1
+ -- signed direct wrapper catches the core's 42501 and records evidence anyway
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.signed_send_direct_message(jsonb,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ select *
4
+ into v_row
5
+ from agent_board.send_direct_message($q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_direct_message(jsonb,jsonb): %', left($q$ select *
6
+ into v_row
7
+ from agent_board.send_direct_message($q$, 60); end if;
8
+ n := replace(d, $q$ select *
9
+ into v_row
10
+ from agent_board.send_direct_message($q$, $q$ begin
11
+ select *
12
+ into v_row
13
+ from agent_board.send_direct_message($q$);
14
+ if n = d then raise exception 'mutation is a no-op'; end if;
15
+ d := n;
16
+ if position($q$ );
17
+
18
+ v_evidence_id :=$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_direct_message(jsonb,jsonb): %', left($q$ );
19
+
20
+ v_evidence_id :=$q$, 60); end if;
21
+ n := replace(d, $q$ );
22
+
23
+ v_evidence_id :=$q$, $q$ );
24
+ exception when insufficient_privilege then
25
+ select null::uuid as message_id, null::uuid as delivery_recipient_id, null::uuid as inbox_item_id, 0 as callback_outbox_count, now() as created_at into v_row;
26
+ end;
27
+
28
+ v_evidence_id :=$q$);
29
+ if n = d then raise exception 'mutation is a no-op'; end if;
30
+ d := n;
31
+ execute d;
32
+ end $m$;
@@ -0,0 +1,20 @@
1
+ -- signed multicast wrapper (both branches) catches the core's 42501
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.signed_send_multicast_message(jsonb,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ if p_payload ? 'p_recipient_agent_ids' then$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_multicast_message(jsonb,jsonb): %', left($q$ if p_payload ? 'p_recipient_agent_ids' then$q$, 60); end if;
4
+ n := replace(d, $q$ if p_payload ? 'p_recipient_agent_ids' then$q$, $q$ begin
5
+ if p_payload ? 'p_recipient_agent_ids' then$q$);
6
+ if n = d then raise exception 'mutation is a no-op'; end if;
7
+ d := n;
8
+ if position($q$ end if;
9
+ v_evidence_id :=$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.signed_send_multicast_message(jsonb,jsonb): %', left($q$ end if;
10
+ v_evidence_id :=$q$, 60); end if;
11
+ n := replace(d, $q$ end if;
12
+ v_evidence_id :=$q$, $q$ end if;
13
+ exception when insufficient_privilege then
14
+ select null::uuid as message_id, 0 as delivered_recipient_count, 0 as inbox_item_count, 0 as callback_outbox_count, now() as created_at into v_row;
15
+ end;
16
+ v_evidence_id :=$q$);
17
+ if n = d then raise exception 'mutation is a no-op'; end if;
18
+ d := n;
19
+ execute d;
20
+ end $m$;
@@ -0,0 +1,7 @@
1
+ do $m$
2
+ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure);
3
+ anchor text := ' and dr.source_message_id = v_message.id';
4
+ begin
5
+ if position(anchor in d)=0 then raise exception 'missing anchor'; end if;
6
+ execute replace(d,anchor,'');
7
+ end $m$;
@@ -0,0 +1,16 @@
1
+ -- direct: the replay ceiling never fires
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ if v_message.id is not null and not exists (
4
+ select 1
5
+ from agent_board.delivery_recipients dr$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ if v_message.id is not null and not exists (
6
+ select 1
7
+ from agent_board.delivery_recipients dr$q$, 60); end if;
8
+ n := replace(d, $q$ if v_message.id is not null and not exists (
9
+ select 1
10
+ from agent_board.delivery_recipients dr$q$, $q$ if false and not exists (
11
+ select 1
12
+ from agent_board.delivery_recipients dr$q$);
13
+ if n = d then raise exception 'mutation is a no-op'; end if;
14
+ d := n;
15
+ execute d;
16
+ end $m$;
@@ -0,0 +1,16 @@
1
+ -- multicast: the replay ceiling never fires
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ if v_message.id is not null and exists (
4
+ select 1
5
+ from unnest(v_recipient_ids) as requested(recipient_id)$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ if v_message.id is not null and exists (
6
+ select 1
7
+ from unnest(v_recipient_ids) as requested(recipient_id)$q$, 60); end if;
8
+ n := replace(d, $q$ if v_message.id is not null and exists (
9
+ select 1
10
+ from unnest(v_recipient_ids) as requested(recipient_id)$q$, $q$ if false and exists (
11
+ select 1
12
+ from unnest(v_recipient_ids) as requested(recipient_id)$q$);
13
+ if n = d then raise exception 'mutation is a no-op'; end if;
14
+ d := n;
15
+ execute d;
16
+ end $m$;
@@ -0,0 +1,12 @@
1
+ -- direct: caller metadata naming the recipient substitutes for a delivery
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.board_id is null
4
+ ) then$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ and dr.board_id is null
5
+ ) then$q$, 60); end if;
6
+ n := replace(d, $q$ and dr.board_id is null
7
+ ) then$q$, $q$ and dr.board_id is null
8
+ ) and coalesce(p_metadata ->> 'recipientAgentId', '') <> p_recipient_agent_id::text then$q$);
9
+ if n = d then raise exception 'mutation is a no-op'; end if;
10
+ d := n;
11
+ execute d;
12
+ end $m$;
@@ -0,0 +1,16 @@
1
+ -- multicast: caller metadata naming the recipient substitutes for a delivery
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.board_id is null
4
+ )
5
+ ) then$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ and dr.board_id is null
6
+ )
7
+ ) then$q$, 60); end if;
8
+ n := replace(d, $q$ and dr.board_id is null
9
+ )
10
+ ) then$q$, $q$ and dr.board_id is null
11
+ ) and not (coalesce(p_metadata -> 'recipientAgentIds', '[]'::jsonb) ? requested.recipient_id::text)
12
+ ) then$q$);
13
+ if n = d then raise exception 'mutation is a no-op'; end if;
14
+ d := n;
15
+ execute d;
16
+ end $m$;
@@ -0,0 +1,11 @@
1
+ -- direct: a delivery of the source in ANY mode counts as proof
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.delivery_mode = 'direct'
4
+ $q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ and dr.delivery_mode = 'direct'
5
+ $q$, 60); end if;
6
+ n := replace(d, $q$ and dr.delivery_mode = 'direct'
7
+ $q$, $q$$q$);
8
+ if n = d then raise exception 'mutation is a no-op'; end if;
9
+ d := n;
10
+ execute d;
11
+ end $m$;
@@ -0,0 +1,11 @@
1
+ -- direct: the `board_id is null` conjunct is dropped (EQUIVALENT given delivery_recipients_board_scope; see C6 and assertion 64)
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.board_id is null
4
+ ) then$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ and dr.board_id is null
5
+ ) then$q$, 60); end if;
6
+ n := replace(d, $q$ and dr.board_id is null
7
+ ) then$q$, $q$ ) then$q$);
8
+ if n = d then raise exception 'mutation is a no-op'; end if;
9
+ d := n;
10
+ execute d;
11
+ end $m$;
@@ -0,0 +1,11 @@
1
+ -- multicast: a delivery of the source in ANY mode counts as proof
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.delivery_mode = 'multicast'
4
+ $q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb): %', left($q$ and dr.delivery_mode = 'multicast'
5
+ $q$, 60); end if;
6
+ n := replace(d, $q$ and dr.delivery_mode = 'multicast'
7
+ $q$, $q$$q$);
8
+ if n = d then raise exception 'mutation is a no-op'; end if;
9
+ d := n;
10
+ execute d;
11
+ end $m$;
@@ -0,0 +1,12 @@
1
+ -- direct: a recipient on the thread's stamped board counts as proof
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ and dr.board_id is null
4
+ ) then$q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ and dr.board_id is null
5
+ ) then$q$, 60); end if;
6
+ n := replace(d, $q$ and dr.board_id is null
7
+ ) then$q$, $q$ and dr.board_id is null
8
+ ) and not exists (select 1 from agent_board.threads th join agent_board.board_memberships bm on bm.board_id = th.board_id where th.id = p_thread_id and bm.agent_id = p_recipient_agent_id and bm.membership_state = 'joined') then$q$);
9
+ if n = d then raise exception 'mutation is a no-op'; end if;
10
+ d := n;
11
+ execute d;
12
+ end $m$;
@@ -0,0 +1,15 @@
1
+ -- direct: the replay lookup is widened across authors
2
+ do $m$ declare d text := pg_get_functiondef('agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)'::regprocedure); n text; begin
3
+ if position($q$ where thread_id = p_thread_id
4
+ and author_agent_id = v_actor_agent_id
5
+ $q$ in d) = 0 then raise exception 'mutation anchor not found in agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb): %', left($q$ where thread_id = p_thread_id
6
+ and author_agent_id = v_actor_agent_id
7
+ $q$, 60); end if;
8
+ n := replace(d, $q$ where thread_id = p_thread_id
9
+ and author_agent_id = v_actor_agent_id
10
+ $q$, $q$ where thread_id = p_thread_id
11
+ $q$);
12
+ if n = d then raise exception 'mutation is a no-op'; end if;
13
+ d := n;
14
+ execute d;
15
+ end $m$;
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ # #717 F2b mutation receipts. Connection comes ONLY from libpq environment (PGHOST/PGPORT/PGUSER/
3
+ # PGPASSWORD/PGDATABASE), never argv. The target must be an OWNED database holding this tree's
4
+ # applied bundle.
5
+ #
6
+ # Per mutation:
7
+ # pgTAP the mutation and the test file run in ONE transaction that is rolled back; the file's
8
+ # plan / emitted / ok / not ok counts and the failing numbers are recorded (counted,
9
+ # never inferred from absence).
10
+ # isolation (M*, L* only) the mutation is COMMITTED, the spec is diffed against its golden, and
11
+ # every touched definition is restored from text captured BEFORE mutating.
12
+ # Both legs require the agent_board catalog fingerprint (every routine's signature, ACL, source
13
+ # and comment) to equal the pre-run value afterwards, or the run stops.
14
+ # Usage: run.sh <receipt-path> [mutation.sql ...] (default: every *.sql beside this script)
15
+ set -uo pipefail
16
+ HERE="$(cd "$(dirname "$0")" && pwd)"
17
+ PKG="$(cd "$HERE/../../.." && pwd)"
18
+ TEST="$PKG/tests/agent_board_717_message_replay_scope.test.sql"
19
+ SPEC="$PKG/tests/isolation/specs/message_replay_scope.spec"
20
+ GOLDEN="$PKG/tests/isolation/expected/message_replay_scope.out"
21
+ OUT="${1:?receipt path}"; shift
22
+ if [ "$#" -gt 0 ]; then MUTS=("$@"); else mapfile -t MUTS < <(find "$HERE" -maxdepth 1 -name '*.sql' | sort -V); fi
23
+ [ -n "${PGDATABASE:-}" ] || { echo "FATAL: PGDATABASE unset (libpq environment required)" >&2; exit 3; }
24
+ ISO="$(ls -1 /usr/lib/postgresql/*/lib/pgxs/src/test/isolation/isolationtester 2>/dev/null | sort -V | tail -1)"
25
+ [ -x "$ISO" ] || { echo "FATAL: isolationtester not found" >&2; exit 3; }
26
+ WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
27
+ P=(psql -X -qtA)
28
+ FNS=("agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)"
29
+ "agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)"
30
+ "agent_board.signed_send_direct_message(jsonb,jsonb)"
31
+ "agent_board.signed_send_multicast_message(jsonb,jsonb)")
32
+ fp() { "${P[@]}" -c "select md5(string_agg(p.oid::regprocedure::text || coalesce(p.proacl::text, '') || p.prosrc || coalesce(obj_description(p.oid, 'pg_proc'), ''), '|' order by p.oid::regprocedure::text)) from pg_proc p where p.pronamespace = 'agent_board'::regnamespace"; }
33
+ : > "$WORK/restore.sql"
34
+ for f in "${FNS[@]}"; do
35
+ "${P[@]}" -c "select pg_get_functiondef('$f'::regprocedure)" >> "$WORK/restore.sql" || exit 3
36
+ printf ';\n' >> "$WORK/restore.sql"
37
+ done
38
+ ORIG="$(fp)"
39
+
40
+ tap() { # $1 label, $2 mutation file or empty
41
+ local out plan ok nok
42
+ if [ -n "$2" ]; then out="$(printf 'begin;\n\\i %s\n\\i %s\nrollback;\n' "$2" "$TEST" | "${P[@]}" 2>&1)"
43
+ else out="$(printf 'begin;\n\\i %s\nrollback;\n' "$TEST" | "${P[@]}" 2>&1)"; fi
44
+ plan="$(sed -nE 's/^1\.\.([0-9]+)$/\1/p' <<<"$out" | head -1)"
45
+ ok="$(grep -cE '^ok [0-9]+' <<<"$out")"; nok="$(grep -cE '^not ok [0-9]+' <<<"$out")"
46
+ printf '%s: pgtap plan=%s emitted=%s ok=%s not_ok=%s\n' "$1" "${plan:-NONE}" "$((ok + nok))" "$ok" "$nok"
47
+ grep -E '^not ok [0-9]+' <<<"$out" | sed 's/^/ /'
48
+ grep -E '(ERROR|FATAL):' <<<"$out" | grep -v 'mutation' | head -3 | sed 's/^/ ERR /'
49
+ grep -E 'ERROR: .*mutation' <<<"$out" | head -1 | sed 's/^/ FATAL-MUTATION /'
50
+ }
51
+ iso() { # $1 label
52
+ "$ISO" "dbname=$PGDATABASE" < "$SPEC" > "$WORK/iso.out" 2>&1
53
+ if diff -q "$GOLDEN" "$WORK/iso.out" >/dev/null; then echo " isolation=identical"
54
+ else
55
+ echo " isolation=DIFFERS ($(diff "$GOLDEN" "$WORK/iso.out" | grep -cE '^[<>]') lines)"
56
+ diff "$GOLDEN" "$WORK/iso.out" | grep -E '^[<>] .*(ERROR|\|)' | head -6 | sed 's/^/ /'
57
+ fi
58
+ }
59
+
60
+ {
61
+ echo "# #717 F2b mutation receipt -- $(date -u +%FT%TZ)"
62
+ echo "# tree $(git -C "$PKG" rev-parse HEAD)$(git -C "$PKG" diff --quiet HEAD -- . || echo ' + uncommitted changes')"
63
+ echo "# catalog fingerprint before: $ORIG"
64
+ tap "BASELINE" ""; iso "BASELINE"
65
+ for m in "${MUTS[@]}"; do
66
+ name="$(basename "$m" .sql)"
67
+ echo "-- $name: $(head -1 "$m" | sed 's/^-- //')"
68
+ tap "$name" "$m"
69
+ [ "$(fp)" = "$ORIG" ] || { echo "FATAL: fingerprint changed after the rolled-back pgTAP leg of $name"; exit 3; }
70
+ case "$name" in
71
+ M*|L*)
72
+ "${P[@]}" -v ON_ERROR_STOP=1 -1 -f "$m" >/dev/null 2>&1 || { echo "FATAL: $name did not apply for the isolation leg"; exit 3; }
73
+ [ "$(fp)" != "$ORIG" ] || { echo "FATAL: $name applied but the fingerprint is unchanged (no-op)"; exit 3; }
74
+ iso "$name"
75
+ "${P[@]}" -v ON_ERROR_STOP=1 -1 -f "$WORK/restore.sql" >/dev/null || { echo "FATAL: restore failed after $name"; exit 3; }
76
+ [ "$(fp)" = "$ORIG" ] || { echo "FATAL: restore fingerprint differs after $name"; exit 3; }
77
+ echo " restored: fingerprint identical" ;;
78
+ esac
79
+ done
80
+ tap "AFTER-RESTORE" ""; iso "AFTER-RESTORE"
81
+ echo "# catalog fingerprint after: $(fp)"
82
+ } | tee "$OUT"