@consiliency/agent-board-schema 1.12.0 → 1.23.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 (156) hide show
  1. package/fixtures/already-applied.json +160 -0
  2. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  3. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  4. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  5. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  6. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  7. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  8. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  9. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  10. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  11. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  12. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  13. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  14. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  15. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  16. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  17. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  18. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  19. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  20. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  21. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  22. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  23. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  24. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  25. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  26. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  27. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  28. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  29. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  30. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  31. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  32. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  33. package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
  34. package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
  35. package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
  36. package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
  37. package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
  38. package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
  39. package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
  40. package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
  41. package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
  42. package/package.json +1 -1
  43. package/schema-manifest.json +363 -3
  44. package/src/tooling.test.mjs +113 -2
  45. package/tests/agent_board_actorspoof.test.sql +349 -0
  46. package/tests/agent_board_agentsdir.test.sql +154 -0
  47. package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
  48. package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
  49. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
  50. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  51. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  52. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  53. package/tests/agent_board_claimkey.test.sql +229 -0
  54. package/tests/agent_board_consent_backfill.test.sql +48 -0
  55. package/tests/agent_board_consent_emission.test.sql +601 -0
  56. package/tests/agent_board_consent_gating.test.sql +489 -0
  57. package/tests/agent_board_consent_substrate.test.sql +720 -0
  58. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  59. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  60. package/tests/agent_board_delegation_policies.test.sql +107 -1
  61. package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
  62. package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
  63. package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
  64. package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
  65. package/tests/agent_board_fedsession.test.sql +51 -5
  66. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
  67. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  68. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  69. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  70. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  71. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  72. package/tests/agent_board_fleet_policy.test.sql +2 -2
  73. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  74. package/tests/agent_board_identity_providers.test.sql +9 -0
  75. package/tests/agent_board_inbox_reads.test.sql +45 -31
  76. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  77. package/tests/agent_board_invitepolicy.test.sql +205 -0
  78. package/tests/agent_board_leaveidem.test.sql +194 -0
  79. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  80. package/tests/agent_board_mint_harden.test.sql +303 -0
  81. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  82. package/tests/agent_board_obsgate.test.sql +178 -0
  83. package/tests/agent_board_obsgate2.test.sql +153 -0
  84. package/tests/agent_board_obsgate3.test.sql +170 -0
  85. package/tests/agent_board_obsgate4.test.sql +196 -0
  86. package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
  87. package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
  88. package/tests/agent_board_pubexec_class.test.sql +53 -0
  89. package/tests/agent_board_rejectsem.test.sql +271 -0
  90. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  91. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  92. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  93. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  94. package/tests/agent_board_rolereset.test.sql +351 -0
  95. package/tests/agent_board_roomdeleg.test.sql +5 -3
  96. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  97. package/tests/agent_board_roomlink.test.sql +27 -11
  98. package/tests/agent_board_rowid.test.sql +1274 -0
  99. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  100. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  101. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  102. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  103. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  104. package/tests/agent_board_signing_store.test.sql +6 -6
  105. package/tests/agent_board_wake_recovery.test.sql +756 -0
  106. package/tests/fixtures/isolation-substrate.sql +93 -0
  107. package/tests/fixtures/test-prereqs.sql +33 -0
  108. package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
  109. package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
  110. package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
  111. package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
  112. package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
  113. package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
  114. package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
  115. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
  116. package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
  117. package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
  118. package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
  119. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  120. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  121. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  122. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  123. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  124. package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
  125. package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
  126. package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
  127. package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
  128. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  129. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  130. package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
  131. package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
  132. package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
  133. package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
  134. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
  135. package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
  136. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
  137. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
  138. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
  139. package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
  140. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
  141. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
  142. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  143. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  144. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  145. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  146. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  147. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
  148. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
  149. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
  150. package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
  151. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  152. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  153. package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
  154. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  155. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
  156. package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
@@ -0,0 +1,452 @@
1
+ -- OUTBOXWAKE / #187 — monotonic wake ordering for the notification outbox.
2
+ --
3
+ -- WHAT THIS FILE PINS, and why each assertion is shaped the way it is:
4
+ --
5
+ -- * WO-1 SHAPE — `callback_outbox_events.seq` exists, is bigint, is NOT NULL, and is
6
+ -- GENERATED **ALWAYS** (`pg_attribute.attidentity = 'a'`). `by default` would be
7
+ -- 'd' and would make the ordering evidence caller-suppliable; that is the whole
8
+ -- property, so it is read from the catalog and not inferred from the DDL text.
9
+ -- * WO-2 EVIDENCE — an INSERT that names `seq` is REJECTED by the server (428C9).
10
+ -- A shape assertion alone would not prove the server enforces it.
11
+ -- * WO-3 CLASS — catalog-derived, per repo rule #1. The writer set comes from
12
+ -- `pg_get_functiondef`, not from a list written here, so a NEW writer fails and
13
+ -- NAMES ITSELF. The companion assertion closes the only escape hatch that
14
+ -- `generated always` leaves open: `overriding system value`. Neither has an
15
+ -- exemption list; each named writer is ALSO positively checked (WO-6/WO-7) to
16
+ -- actually produce monotonic seq, so this is not an argument from absence.
17
+ -- * WO-4 ORDERING — the uuid tiebreak is GONE from claim_callback_outbox_events and
18
+ -- `seq` replaced it, checked against the catalog body.
19
+ -- * WO-5 RESTATEMENT — the claim function was DROPPED and recreated (its return type
20
+ -- changed), which discards the ACL and the COMMENT and re-grants EXECUTE to PUBLIC
21
+ -- by default. These assertions fail if the migration forgot to restate them.
22
+ -- * WO-6/WO-7 BEHAVIOUR — the acceptance criteria: a real fan-out through the real
23
+ -- writer, in ONE transaction, yields N distinct ordered seq while `created_at` is
24
+ -- byte-identical across all N; a bounded claim takes the LOWEST seq; and the queue is
25
+ -- partitioned by STATUS so nothing is handed out twice or dropped.
26
+ --
27
+ -- WHAT THIS FILE DELIBERATELY DOES NOT ASSERT: that `seq > my cursor` recovers everything
28
+ -- unhandled. `seq` is monotonic in ASSIGNMENT order, not COMMIT order, so a late-committing
29
+ -- transaction lands below an already-persisted cursor. See the WO-7 block for the two
30
+ -- assertions that were removed for encoding that false property.
31
+ --
32
+ -- Allow-side calls are wrapped in lives_ok(...) with a SEPARATE state check
33
+ -- (message-board#113): a bare `is((select ... from rpc(...)), ...)` aborts the
34
+ -- transaction on a raise, pgTAP emits NO `not ok`, and every later assertion in the
35
+ -- file silently does not run.
36
+
37
+ begin;
38
+
39
+ select plan(37);
40
+
41
+ -- ---------------------------------------------------------------------------
42
+ -- WO-1 SHAPE
43
+ -- ---------------------------------------------------------------------------
44
+ select has_column('agent_board', 'callback_outbox_events', 'seq',
45
+ 'WO-1 callback_outbox_events has a seq column');
46
+ select col_type_is('agent_board', 'callback_outbox_events', 'seq', 'bigint',
47
+ 'WO-1 seq is bigint');
48
+ select col_not_null('agent_board', 'callback_outbox_events', 'seq',
49
+ 'WO-1 seq is not null');
50
+
51
+ select is(
52
+ (select a.attidentity
53
+ from pg_attribute a
54
+ where a.attrelid = 'agent_board.callback_outbox_events'::regclass
55
+ and a.attname = 'seq'),
56
+ 'a',
57
+ 'WO-1 seq is GENERATED ALWAYS as identity (attidentity=a), not BY DEFAULT (d) -- the value is evidence of write order and must not be caller-suppliable');
58
+
59
+ select ok(
60
+ (select bool_or(i.indisunique)
61
+ from pg_index i
62
+ join pg_class c on c.oid = i.indexrelid
63
+ where i.indrelid = 'agent_board.callback_outbox_events'::regclass
64
+ and pg_get_indexdef(i.indexrelid) ~ '\(seq\)$'),
65
+ 'WO-1 a UNIQUE index enforces seq uniqueness');
66
+
67
+ select ok(
68
+ (select count(*) = 1
69
+ from pg_index i
70
+ where i.indrelid = 'agent_board.callback_outbox_events'::regclass
71
+ and pg_get_indexdef(i.indexrelid) ~ '\(recipient_agent_id, seq\)$'),
72
+ 'WO-1 (recipient_agent_id, seq) index exists -- the consumer access path is "rows for me newer than my cursor"');
73
+
74
+ select isnt(
75
+ (select col_description('agent_board.callback_outbox_events'::regclass, a.attnum)
76
+ from pg_attribute a
77
+ where a.attrelid = 'agent_board.callback_outbox_events'::regclass
78
+ and a.attname = 'seq'),
79
+ null,
80
+ 'WO-1 seq carries a COMMENT stating what it is evidence of');
81
+
82
+ -- ---------------------------------------------------------------------------
83
+ -- WO-3 CLASS (catalog-derived; stated before the fixture so it cannot depend on it)
84
+ -- ---------------------------------------------------------------------------
85
+ -- The writer set is READ FROM THE CATALOG. A new function that inserts into the
86
+ -- outbox changes this string, fails the assertion, and prints its own name in the
87
+ -- diff -- which is the only way a completeness claim survives contact with a future
88
+ -- change (repo rule #1).
89
+ select is(
90
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
91
+ from pg_proc p
92
+ join pg_namespace n on n.oid = p.pronamespace
93
+ where n.nspname = 'agent_board'
94
+ and p.prokind = 'f'
95
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
96
+ and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
97
+ ~ 'insert into agent_board\.callback_outbox_events'),
98
+ '_queue_board_post_callbacks, _queue_inbox_callbacks',
99
+ 'WO-3 CLASS: the catalog names exactly the two known outbox writers -- a new writer fails here and NAMES ITSELF');
100
+
101
+ -- `generated always` is enforced by the server, and OVERRIDING SYSTEM VALUE is its
102
+ -- ONLY bypass. Asserting the empty set over the whole schema (not over a hand list of
103
+ -- writers) means a future writer cannot quietly assert an ordering it did not earn.
104
+ select is(
105
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
106
+ from pg_proc p
107
+ join pg_namespace n on n.oid = p.pronamespace
108
+ where n.nspname = 'agent_board'
109
+ and p.prokind = 'f'
110
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
111
+ and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
112
+ ~* 'insert into agent_board\.callback_outbox_events'
113
+ and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
114
+ ~* 'overriding\s+(system|user)\s+value'),
115
+ '',
116
+ 'WO-3 CLASS: no outbox writer uses OVERRIDING SYSTEM VALUE -- the identity bypass stays closed');
117
+
118
+ -- ---------------------------------------------------------------------------
119
+ -- WO-4 ORDERING
120
+ -- ---------------------------------------------------------------------------
121
+ select ok(
122
+ (select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
123
+ ~ 'order by coe\.available_at asc, coe\.seq asc'
124
+ from pg_proc p
125
+ join pg_namespace n on n.oid = p.pronamespace
126
+ where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
127
+ 'WO-4 claim_callback_outbox_events orders candidates by (available_at, seq)');
128
+
129
+ select ok(
130
+ (select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
131
+ !~ 'coe\.id asc'
132
+ from pg_proc p
133
+ join pg_namespace n on n.oid = p.pronamespace
134
+ where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
135
+ 'WO-4 the random-uuid tiebreak (coe.id asc) is GONE from the claim ordering');
136
+
137
+ -- This is the PIN for "the batch is returned in seq order", and it is a source check
138
+ -- on purpose. The behavioural assertion below (WO-7, "RETURNED in seq order") does NOT
139
+ -- pin it: deleting `order by u.seq asc` from the function leaves the plan feeding the
140
+ -- ordered candidate CTE straight through, so the observed order stays ascending and the
141
+ -- mutation produces NO failure. A property no mutation can break is not covered, so the
142
+ -- pin has to be stated where the mutation actually lands.
143
+ select ok(
144
+ (select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
145
+ ~ 'order by u\.seq asc'
146
+ from pg_proc p
147
+ join pg_namespace n on n.oid = p.pronamespace
148
+ where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
149
+ 'WO-4 the claimed batch is RETURNED ordered by seq, not merely selected in seq order');
150
+
151
+ select ok(
152
+ (select 'outbox_seq' = any(p.proargnames)
153
+ from pg_proc p
154
+ join pg_namespace n on n.oid = p.pronamespace
155
+ where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
156
+ 'WO-4 the claim RPC returns outbox_seq, so a worker can persist a cursor');
157
+
158
+ -- ---------------------------------------------------------------------------
159
+ -- WO-5 ACL + COMMENT restated after DROP FUNCTION
160
+ -- ---------------------------------------------------------------------------
161
+ select ok(
162
+ has_function_privilege('service_role', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
163
+ 'WO-5 service_role EXECUTE was re-granted after the drop/recreate');
164
+
165
+ select ok(
166
+ not has_function_privilege('authenticated', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
167
+ 'WO-5 authenticated has NO EXECUTE -- recreating a definer function re-grants PUBLIC by default');
168
+
169
+ select ok(
170
+ not has_function_privilege('anon', 'agent_board.claim_callback_outbox_events(integer)', 'EXECUTE'),
171
+ 'WO-5 anon has NO EXECUTE on the claim RPC');
172
+
173
+ select is(
174
+ (select count(*)::integer
175
+ from pg_proc p
176
+ join pg_namespace n on n.oid = p.pronamespace
177
+ where n.nspname = 'agent_board'
178
+ and p.proname = 'claim_callback_outbox_events'
179
+ and array_to_string(coalesce(p.proacl, '{}'::aclitem[]), ',') ~ '(^|,)=X/'),
180
+ 0,
181
+ 'WO-5 PUBLIC holds no EXECUTE entry in the claim RPC ACL');
182
+
183
+ select isnt(
184
+ (select obj_description(p.oid, 'pg_proc')
185
+ from pg_proc p
186
+ join pg_namespace n on n.oid = p.pronamespace
187
+ where n.nspname = 'agent_board' and p.proname = 'claim_callback_outbox_events'),
188
+ null,
189
+ 'WO-5 the COMMENT discarded by DROP FUNCTION was restated');
190
+
191
+ -- The five assertions above name ONE function, which is exactly the shape that has failed
192
+ -- here before: they verify the function this migration happens to touch, and say nothing
193
+ -- about the next one. These two are the class, derived from pg_catalog with NO exemption
194
+ -- list: every SECURITY DEFINER function whose body reaches callback_outbox_events must be
195
+ -- closed to PUBLIC/anon/authenticated and open to service_role. A function added to this
196
+ -- migration (or any later one) that forgets its revoke or its grant fails here and PRINTS
197
+ -- ITS OWN NAME. Verified exemption-free at the time of writing: all eight members of the
198
+ -- class satisfy both directions, so neither assertion passes by finding nothing.
199
+ select is(
200
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
201
+ from pg_proc p
202
+ where p.pronamespace = 'agent_board'::regnamespace
203
+ and p.prosecdef
204
+ and pg_get_functiondef(p.oid) ~ 'callback_outbox_events'
205
+ and (has_function_privilege('public', p.oid, 'EXECUTE')
206
+ or has_function_privilege('anon', p.oid, 'EXECUTE')
207
+ or has_function_privilege('authenticated', p.oid, 'EXECUTE'))),
208
+ '',
209
+ 'WO-5 CLASS: no SECURITY DEFINER function reaching callback_outbox_events is executable by PUBLIC, anon, or authenticated -- DROP FUNCTION discards the ACL and CREATE re-grants PUBLIC');
210
+
211
+ select is(
212
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
213
+ from pg_proc p
214
+ where p.pronamespace = 'agent_board'::regnamespace
215
+ and p.prosecdef
216
+ and pg_get_functiondef(p.oid) ~ 'callback_outbox_events'
217
+ and not has_function_privilege('service_role', p.oid, 'EXECUTE')),
218
+ '',
219
+ 'WO-5 CLASS: every SECURITY DEFINER function reaching callback_outbox_events is executable by service_role -- a dropped-and-recreated function that lost its grant names itself here');
220
+
221
+ -- ---------------------------------------------------------------------------
222
+ -- Fixture: a real board fan-out. Five subscribers, one verified endpoint each.
223
+ -- ---------------------------------------------------------------------------
224
+ -- claim_callback_outbox_events polls the GLOBAL ready queue. Neutralize any rows a
225
+ -- previous fixture left ready so this file controls the claim; the surrounding
226
+ -- transaction rolls it back.
227
+ update agent_board.callback_outbox_events
228
+ set outbox_status = 'delivered',
229
+ delivered_at = coalesce(delivered_at, now()),
230
+ updated_at = now()
231
+ where outbox_status in ('pending', 'retryable')
232
+ and available_at <= now();
233
+
234
+ insert into agent_board.agents(id, agent_type, handle, display_name, status)
235
+ values ('18700000-0000-4000-8000-000000000000', 'agent', 'wake187-sender', 'Wake187 Sender', 'active');
236
+ insert into agent_board.agents(id, agent_type, handle, display_name, status)
237
+ select ('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
238
+ 'agent', 'wake187-sub-' || n, 'Wake187 Subscriber ' || n, 'active'
239
+ from generate_series(1, 5) n;
240
+
241
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
242
+ values ('18701000-0000-4000-8000-000000000001', 'wake187-board', 'Wake187 Board',
243
+ '18700000-0000-4000-8000-000000000000', 'open', 'active');
244
+
245
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
246
+ values ('18702000-0000-4000-8000-000000000001', 'coordination',
247
+ '18700000-0000-4000-8000-000000000000', 'Wake187 thread',
248
+ '18701000-0000-4000-8000-000000000001');
249
+
250
+ insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
251
+ values ('18703000-0000-4000-8000-000000000001', '18702000-0000-4000-8000-000000000001',
252
+ '18700000-0000-4000-8000-000000000000', 1, 'comment', 'wake187 board post');
253
+
254
+ insert into agent_board.board_memberships(id, board_id, agent_id, role, membership_state, created_by_agent_id)
255
+ select ('18704000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
256
+ '18701000-0000-4000-8000-000000000001',
257
+ ('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
258
+ 'member', 'joined', '18700000-0000-4000-8000-000000000000'
259
+ from generate_series(1, 5) n;
260
+
261
+ insert into agent_board.callback_endpoints(id, owner_agent_id, endpoint_url, endpoint_status, verification_requested_at, verified_at)
262
+ select ('18705000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
263
+ ('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
264
+ 'https://example.test/wake187/' || n,
265
+ 'verified', now(), now()
266
+ from generate_series(1, 5) n;
267
+
268
+ insert into agent_board.callback_subscriptions(
269
+ id, endpoint_id, agent_id, subscription_scope, event_category,
270
+ board_id, board_membership_id, subscription_status)
271
+ select ('18706000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
272
+ ('18705000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
273
+ ('18700000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
274
+ 'board', 'board.post.delivered',
275
+ '18701000-0000-4000-8000-000000000001',
276
+ ('18704000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid,
277
+ 'active'
278
+ from generate_series(1, 5) n;
279
+
280
+ -- ---------------------------------------------------------------------------
281
+ -- WO-6 FAN-OUT: one transaction, one statement, five rows.
282
+ -- ---------------------------------------------------------------------------
283
+ -- The capture tables are created EMPTY first and the call INSERTs into them. If a
284
+ -- call raises, lives_ok reports a named `not ok` AND every later assertion still runs
285
+ -- against an empty table -- whereas `create temp table ... as select` would leave the
286
+ -- table missing, abort the transaction, and silently truncate the plan (#113).
287
+ create temp table wake187_fanout(inserted_count integer);
288
+ select lives_ok($$
289
+ insert into wake187_fanout
290
+ select agent_board._queue_board_post_callbacks(
291
+ '18701000-0000-4000-8000-000000000001'::uuid,
292
+ '18703000-0000-4000-8000-000000000001'::uuid,
293
+ 'Wake187 subject', 'Wake187 preview', 'high', '{}'::jsonb) as inserted_count
294
+ $$, 'WO-6 the board fan-out writer runs [call succeeds]');
295
+
296
+ select is(
297
+ (select inserted_count from wake187_fanout),
298
+ 5,
299
+ 'WO-6 _queue_board_post_callbacks wrote one outbox row per subscriber');
300
+
301
+ create temp table wake187_rows as
302
+ select id, seq, created_at, recipient_agent_id
303
+ from agent_board.callback_outbox_events
304
+ where board_id = '18701000-0000-4000-8000-000000000001';
305
+
306
+ select is(
307
+ (select count(distinct created_at)::integer from wake187_rows),
308
+ 1,
309
+ 'WO-6 all five rows share ONE created_at -- now() is transaction_timestamp(), so the fan-out CANNOT be ordered by time');
310
+
311
+ select is(
312
+ (select count(distinct seq)::integer from wake187_rows),
313
+ 5,
314
+ 'WO-6 the same five rows have five DISTINCT seq values');
315
+
316
+ select is(
317
+ (select (max(seq) - min(seq) + 1)::integer from wake187_rows),
318
+ 5,
319
+ 'WO-6 the five seq values are contiguous -- a total order with no gap or reuse');
320
+
321
+ -- ---------------------------------------------------------------------------
322
+ -- WO-7 CLAIM DETERMINISM + CURSOR RECOVERY (acceptance criteria 2 and 3)
323
+ -- ---------------------------------------------------------------------------
324
+ create temp table wake187_batch1(outbox_event_id uuid, outbox_seq bigint);
325
+ select lives_ok($$
326
+ insert into wake187_batch1
327
+ select outbox_event_id, outbox_seq from agent_board.claim_callback_outbox_events(2)
328
+ $$, 'WO-7 a bounded claim runs [call succeeds]');
329
+
330
+ select is(
331
+ (select count(*)::integer from wake187_batch1),
332
+ 2,
333
+ 'WO-7 the bounded claim returned exactly the requested batch size');
334
+
335
+ -- The defect: with a random-uuid tiebreak over identical timestamps, WHICH two rows a
336
+ -- batch of two takes was arbitrary. It must now be the two lowest seq.
337
+ select is(
338
+ (select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
339
+ (select array_agg(seq order by seq) from (select seq from wake187_rows order by seq limit 2) lo),
340
+ 'WO-7 the claim took the two LOWEST seq rows -- deterministic across identical created_at');
341
+
342
+ -- Corroboration only, NOT the pin: removing the ORDER BY from the returned batch does
343
+ -- not make this fail (see the WO-4 source assertion above for why).
344
+ select is(
345
+ (select array_agg(outbox_seq) from wake187_batch1),
346
+ (select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
347
+ 'WO-7 the batch is observed in seq order [corroborates WO-4; the ORDER BY itself is pinned there]');
348
+
349
+ -- WHAT WAS HERE BEFORE, AND WHY IT IS GONE. Two assertions used to partition this queue
350
+ -- with `seq > cursor` / `seq <= cursor` and call the result lossless recovery. Inside one
351
+ -- transaction that is true, and it generalises to a property that IS FALSE: `seq` is
352
+ -- monotonic in ASSIGNMENT order, not in COMMIT order, so a transaction that inserts early
353
+ -- and commits late lands BELOW a cursor a consumer has already persisted and is skipped.
354
+ -- The isolation spec proves exactly that (`late_committer_holds_a_lower_seq_than_the_
355
+ -- persisted_cursor`), and `agent-board-runtime/src/notifications.ts` really does gate on
356
+ -- it: `shouldReplayMissedEvent` returns `Number(stored) < hint.sequence`. An assertion
357
+ -- that encodes a false property is worse than no assertion, so both are replaced by the
358
+ -- property that actually holds and that the claim path actually relies on: the queue is
359
+ -- partitioned by STATUS, not by seq.
360
+ select is(
361
+ (select array_agg(coe.seq order by coe.seq)
362
+ from agent_board.callback_outbox_events coe
363
+ where coe.board_id = '18701000-0000-4000-8000-000000000001'
364
+ and coe.outbox_status = 'pending'),
365
+ (select array_agg(r.seq order by r.seq)
366
+ from wake187_rows r
367
+ where r.seq not in (select outbox_seq from wake187_batch1)),
368
+ 'WO-7 the rows still pending are EXACTLY the ones the claim did not take -- the claim path is lossless because it partitions on STATUS, never on seq');
369
+
370
+ select is(
371
+ (select array_agg(coe.seq order by coe.seq)
372
+ from agent_board.callback_outbox_events coe
373
+ where coe.board_id = '18701000-0000-4000-8000-000000000001'
374
+ and coe.outbox_status = 'delivering'),
375
+ (select array_agg(outbox_seq order by outbox_seq) from wake187_batch1),
376
+ 'WO-7 the rows now delivering are EXACTLY the claimed batch -- no row is handed out twice and none is silently dropped');
377
+
378
+ create temp table wake187_batch2(outbox_event_id uuid, outbox_seq bigint);
379
+ select lives_ok($$
380
+ insert into wake187_batch2
381
+ select outbox_event_id, outbox_seq from agent_board.claim_callback_outbox_events(10)
382
+ $$, 'WO-7 the follow-up claim runs [call succeeds]');
383
+
384
+ select is(
385
+ (select array_agg(outbox_seq order by outbox_seq) from wake187_batch2),
386
+ (select array_agg(seq order by seq) from (select seq from wake187_rows order by seq offset 2) hi),
387
+ 'WO-7 the follow-up claim takes the remaining rows in seq order, resuming exactly where the cursor stopped');
388
+
389
+ -- ---------------------------------------------------------------------------
390
+ -- WO-2 EVIDENCE: the identity is not caller-suppliable (checked against the SERVER,
391
+ -- not the DDL text). Stated last so its failed INSERT cannot disturb the fixture.
392
+ -- ---------------------------------------------------------------------------
393
+ select throws_ok($$
394
+ insert into agent_board.callback_outbox_events(
395
+ seq, endpoint_id, subscription_id, recipient_agent_id, board_id,
396
+ event_category, delivery_mode, source_kind, source_message_id,
397
+ idempotency_key, priority)
398
+ values (
399
+ 999999, '18705000-0000-4000-8000-000000000001'::uuid,
400
+ '18706000-0000-4000-8000-000000000001'::uuid,
401
+ '18700000-0000-4000-8000-000000000001'::uuid,
402
+ '18701000-0000-4000-8000-000000000001'::uuid,
403
+ 'board.post.delivered', 'board', 'message',
404
+ '18703000-0000-4000-8000-000000000001'::uuid,
405
+ 'wake187:forged-seq', 'high')
406
+ $$, '428C9',
407
+ null,
408
+ 'WO-2 an INSERT that supplies seq is REJECTED by the server -- GENERATED ALWAYS, so write order cannot be forged');
409
+
410
+ -- The second writer, positively: it is subject to the same identity and its row lands
411
+ -- ABOVE the fan-out. An enumeration (WO-3) that named a writer doing neither would be
412
+ -- an exemption asserted by absence, which is how a hole hid here before.
413
+ insert into agent_board.delivery_recipients(id, delivery_mode, recipient_agent_id, source_kind, source_message_id)
414
+ values ('18707000-0000-4000-8000-000000000001', 'direct',
415
+ '18700000-0000-4000-8000-000000000001', 'message',
416
+ '18703000-0000-4000-8000-000000000001');
417
+
418
+ insert into agent_board.inbox_items(
419
+ id, delivery_recipient_id, recipient_agent_id, delivery_mode, source_kind,
420
+ source_message_id, subject, preview, priority, action_state,
421
+ visible_in_inbox, visible_in_board_feed, fetch_descriptor)
422
+ values ('18708000-0000-4000-8000-000000000001', '18707000-0000-4000-8000-000000000001',
423
+ '18700000-0000-4000-8000-000000000001', 'direct', 'message',
424
+ '18703000-0000-4000-8000-000000000001', 'Wake187 inbox', 'Wake187 inbox preview',
425
+ 'high', 'new', true, false, '{}'::jsonb);
426
+
427
+ insert into agent_board.callback_subscriptions(
428
+ id, endpoint_id, agent_id, subscription_scope, event_category, subscription_status)
429
+ values ('18709000-0000-4000-8000-000000000001', '18705000-0000-4000-8000-000000000001',
430
+ '18700000-0000-4000-8000-000000000001', 'own_inbox', 'inbox.item.delivered', 'active');
431
+
432
+ create temp table wake187_inbox(inserted_count integer);
433
+ select lives_ok($$
434
+ insert into wake187_inbox
435
+ select agent_board._queue_inbox_callbacks(
436
+ '18708000-0000-4000-8000-000000000001'::uuid, 'inbox.item.delivered') as inserted_count
437
+ $$, 'WO-6 the inbox fan-out writer runs [call succeeds]');
438
+
439
+ select is(
440
+ (select inserted_count from wake187_inbox),
441
+ 1,
442
+ 'WO-6 _queue_inbox_callbacks wrote its outbox row');
443
+
444
+ select ok(
445
+ (select coe.seq > (select max(seq) from wake187_rows)
446
+ from agent_board.callback_outbox_events coe
447
+ where coe.idempotency_key = 'inbox.item.delivered:18708000-0000-4000-8000-000000000001:18709000-0000-4000-8000-000000000001'),
448
+ 'WO-6 the second writer shares the SAME sequence -- its row sorts strictly after the earlier fan-out, so the order is schema-wide, not per-writer');
449
+
450
+ select * from finish();
451
+
452
+ rollback;
@@ -47,14 +47,14 @@ begin
47
47
  select e.agent_id, e.auth_user_id into v_agent, v_auth
48
48
  from agent_board.enroll_federated_identity(
49
49
  'https://mock-portal-'||s||'.test', 'message-board', 'portal|'||s,
50
- jsonb_build_object('sub','portal|'||s),
50
+ jsonb_build_object('iss','https://mock-portal-'||s||'.test','aud','message-board','sub','portal|'||s),
51
51
  'pofx-'||s, v_pub, 'raw_ed25519_base64url', v_fp, true, null, 'agent', '{board:send}', '{}') e;
52
52
 
53
53
  -- A SECOND signing key (key_version 2; enroll auto-enrolled version 1) + a pending
54
54
  -- challenge bound to it, so the mint step below can drive PoP -> mint deterministically.
55
55
  insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
56
- values (v_key, v_agent, 'primary', 2, repeat('A',43), 'raw_ed25519_base64url',
57
- 'sha256:'||rpad(replace(v_key::text,'-',''),43,'A'), 'active', 'proven');
56
+ values (v_key, v_agent, 'primary', 2, public.test_pubkey(v_key::text), 'raw_ed25519_base64url',
57
+ public.test_key_fingerprint(v_key::text), 'active', 'proven');
58
58
  insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at, consumed_at)
59
59
  values (v_ch, v_key, v_agent, v_ch::text, 'agent-signing-key-proof.v1:'||v_ch||':n', 'pending', now() + interval '10 minutes', null);
60
60
 
@@ -97,6 +97,17 @@ select lives_ok(
97
97
  select set_config('request.jwt.claim.role', '', true);
98
98
  select set_config('request.jwt.claim.sub', '', true);
99
99
 
100
+ -- The send LANDED, not merely raised no exception (message-board#87): the prior
101
+ -- lives_ok proves no error, but the header's "board writes are unaffected" claim
102
+ -- needs the fan-out write to be observable. Assert the board post produced board
103
+ -- delivery rows for the board — the write path executed Portal-offline.
104
+ select ok(
105
+ exists(
106
+ select 1 from agent_board.delivery_recipients
107
+ where board_id = (select board from _pofx)
108
+ and delivery_mode = 'board'),
109
+ 'the Portal-offline board-member send landed board delivery rows (board writes unaffected)');
110
+
100
111
  -- revoke: the lifecycle terminates cleanly; the agent is no longer active. ---------
101
112
  select lives_ok(
102
113
  (select format($q$select agent_board.revoke_agent(%L::uuid, 'portal-offline lifecycle test')$q$, agent) from _pofx),
@@ -0,0 +1,53 @@
1
+ -- MBHARDEN / #118 — no agent_board SECURITY DEFINER function may carry a PUBLIC EXECUTE
2
+ -- ACL. CREATE FUNCTION defaults EXECUTE to PUBLIC; a SECURITY DEFINER function runs as the
3
+ -- schema owner, so a stray PUBLIC grant lets any authenticated caller drive owner-privileged
4
+ -- logic. #97 closed mark_retention_metadata; 20260812000100 closes the other five.
5
+ --
6
+ -- Assertions are catalog-derived (the durable class invariant) plus two named pins for the
7
+ -- genuine holes, and one over-revoke guard. Each is mutation-pinned:
8
+ -- * removing ANY of the five revokes -> the class count becomes >= 1 -> assert 1 fails.
9
+ -- * removing the _record_signed_write_evidence / _resolve_addressing_recipient_rows revoke
10
+ -- -> its named assert flips to true -> fails.
11
+ -- * "fixing" the invariant by nuking authenticated grants instead of PUBLIC -> assert 4
12
+ -- (an intentionally authenticated SECURITY DEFINER RPC) fails.
13
+
14
+ create extension if not exists pgtap with schema extensions;
15
+ set search_path = extensions, public;
16
+
17
+ begin;
18
+
19
+ select plan(4);
20
+
21
+ -- The class invariant. PUBLIC is the `=X` pseudo-grantee; has_function_privilege('public',…)
22
+ -- is true ONLY when PUBLIC itself holds EXECUTE, so intentional named grants (e.g. to
23
+ -- authenticated) do not trip this.
24
+ select is(
25
+ (select count(*)::int
26
+ from pg_proc p
27
+ where p.pronamespace = 'agent_board'::regnamespace
28
+ and p.prosecdef
29
+ and has_function_privilege('public', p.oid, 'EXECUTE')),
30
+ 0,
31
+ 'class invariant: no agent_board SECURITY DEFINER function carries a PUBLIC EXECUTE ACL');
32
+
33
+ -- Named pin: the forge hole. Authenticated must not be able to INSERT forged signed-write
34
+ -- attestations by calling the evidence recorder directly.
35
+ select ok(
36
+ not has_function_privilege('authenticated', 'agent_board._record_signed_write_evidence(text,uuid,text,jsonb,jsonb)', 'EXECUTE'),
37
+ 'authenticated cannot execute _record_signed_write_evidence (attestation forge)');
38
+
39
+ -- Named pin: the enumeration leak. Authenticated must not be able to run the actor-spoofable
40
+ -- directory resolver directly, bypassing resolve_recipients'' session rebind.
41
+ select ok(
42
+ not has_function_privilege('authenticated', 'agent_board._resolve_addressing_recipient_rows(uuid,jsonb)', 'EXECUTE'),
43
+ 'authenticated cannot execute _resolve_addressing_recipient_rows (directory leak)');
44
+
45
+ -- Over-revoke guard: the invariant targets PUBLIC, not intended named grants. A SECURITY
46
+ -- DEFINER RPC deliberately granted to authenticated must keep working.
47
+ select ok(
48
+ has_function_privilege('authenticated', 'agent_board.get_schema_version()', 'EXECUTE'),
49
+ 'intended authenticated grant on a SECURITY DEFINER RPC is preserved');
50
+
51
+ select * from finish();
52
+
53
+ rollback;