@consiliency/agent-board-schema 1.22.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 (60) hide show
  1. package/fixtures/already-applied.json +36 -0
  2. package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
  3. package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
  4. package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
  5. package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
  6. package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
  7. package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
  8. package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
  9. package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
  10. package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
  11. package/package.json +1 -1
  12. package/schema-manifest.json +84 -3
  13. package/src/tooling.test.mjs +2 -2
  14. package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
  15. package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
  16. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
  17. package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
  18. package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
  19. package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
  20. package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
  21. package/tests/agent_board_fedsession.test.sql +46 -2
  22. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
  23. package/tests/agent_board_inbox_reads.test.sql +16 -19
  24. package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
  25. package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
  26. package/tests/agent_board_roomlink.test.sql +2 -2
  27. package/tests/agent_board_wake_recovery.test.sql +756 -0
  28. package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
  29. package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
  30. package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
  31. package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
  32. package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
  33. package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
  34. package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
  35. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
  36. package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
  37. package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
  38. package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
  39. package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
  40. package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
  41. package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
  42. package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
  43. package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
  44. package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
  45. package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
  46. package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
  47. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
  48. package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
  49. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
  50. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
  51. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
  52. package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
  53. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
  54. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
  55. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
  56. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
  57. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
  58. package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
  59. package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
  60. package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
@@ -0,0 +1,170 @@
1
+ -- Consiliency/message-board#193 pgTAP: enroll_federated_human must reject an
2
+ -- ABSENT (issuer, audience) BEFORE it provisions or locks the human owner.
3
+ --
4
+ -- WHY THE GATE IS WHERE IT IS. PostgreSQL cannot row-lock a row that does not
5
+ -- exist, so with the provider absent there was nothing for the board/provider
6
+ -- ordering to attach to: the function skipped it, locked auth.users, and delegated
7
+ -- -- and the delegate's fresh READ COMMITTED snapshot could then see a provider
8
+ -- committed in the gap and auto-join its default board while this transaction held
9
+ -- the owner. Owner -> board, inverted against fleet onboarding.
10
+ --
11
+ -- WHAT THIS FILE CAN AND CANNOT PROVE. A single pgTAP transaction cannot exhibit
12
+ -- the interleaving; the load-bearing proof is
13
+ -- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec, where a guard
14
+ -- session holds the owner row and the pre-fix function BLOCKS while the fixed one
15
+ -- raises without waiting. What is pinned HERE is (a) that the rejection comes from
16
+ -- the pre-lock gate and not from the delegate's identical-verdict step (4) -- which
17
+ -- is why the two messages differ -- (b) that it is positionally ahead of the owner
18
+ -- provisioning in the shipped definition, and (c) that the deeper provider gates and
19
+ -- the auto-join allow path are unchanged.
20
+ create extension if not exists pgtap with schema extensions;
21
+ set search_path = extensions, public, agent_board;
22
+
23
+ begin;
24
+
25
+ select plan(8);
26
+
27
+ -- G-1: the ABSENT-provider rejection, identified by the pre-lock gate's own message.
28
+ -- The delegate raises the same 42501 with a DIFFERENT message, so an errcode-only
29
+ -- assertion here would pass with the gate removed.
30
+ select throws_ok(
31
+ $$ select agent_board.enroll_federated_human(
32
+ 'https://lo193-absent.test', 'message-board', 'lo193-orphan',
33
+ jsonb_build_object('sub','lo193-orphan'), null) $$,
34
+ '42501',
35
+ 'issuer/audience not in the identity-provider allowlist (rejected before any owner or board lock)',
36
+ 'G-1: an absent (issuer,audience) is rejected by the PRE-LOCK gate');
37
+
38
+ -- G-2: and it is positionally ahead of owner provisioning in the shipped body.
39
+ --
40
+ -- WHAT THIS ASSERTION HAD TO SURVIVE, because each version read as evidence while
41
+ -- being defeatable. The marker is located on a body with comments and dollar-quoted
42
+ -- blocks stripped and single-quoted literals KEPT (the raise message is a literal,
43
+ -- so stripping literals would remove the very thing being located), and it is
44
+ -- anchored to the RAISE SHAPE rather than to the bare message:
45
+ -- * bare message on raw text -> a one-line comment quoting the message passes it
46
+ -- with the gate moved below _ensure_agent_auth_user;
47
+ -- * bare message on the stripped body -> `raise debug 'ordering note: %', '<msg>'`
48
+ -- or `declare v_note text := '<msg>'` passes it the same way. Both verified.
49
+ -- * raise-exception-shaped on the stripped body -> all four masks fail, including
50
+ -- a composite that also makes the provider freeze null-tolerant and goes 8/8
51
+ -- GREEN against every other assertion here with the owner provisioned before the
52
+ -- verdict.
53
+ -- RESIDUAL, stated rather than implied: this narrows the vector, it does not close
54
+ -- the shape. A dead-code `raise exception '<msg>'` earlier in the body still passes.
55
+ -- What that residual cannot reach is the LOCK order: a gate moved below the
56
+ -- auth.users FOR UPDATE is caught by
57
+ -- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec (the call goes
58
+ -- `<waiting ...>` instead of raising), verified by mutation. A gate moved below
59
+ -- _ensure_agent_auth_user but ABOVE that lock is caught by NEITHER the spec nor the
60
+ -- pre-remedy G-2 -- which is precisely the hole this anchoring closes.
61
+ select is(
62
+ (select case
63
+ when p_gate = 0 then 'gate-missing'
64
+ when p_ensure = 0 or p_lock is null then 'owner-provisioning-not-found'
65
+ when p_gate < p_ensure and p_gate < p_lock then 'gate-before-owner'
66
+ else 'gate-after-owner'
67
+ end
68
+ from (
69
+ select coalesce(strpos(d, substring(d from
70
+ $g$raise\s+exception\s+'issuer/audience not in the identity-provider allowlist \(rejected before any owner or board lock\)'$g$)), 0) as p_gate,
71
+ strpos(d, '_ensure_agent_auth_user') as p_ensure,
72
+ strpos(d, substring(d from 'from\s+auth\.users[^;]{0,250}?for\s+update')) as p_lock
73
+ from (select regexp_replace(p.prosrc,
74
+ $re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
75
+ '\2', 'g') as d
76
+ from pg_proc p
77
+ join pg_namespace n on n.oid = p.pronamespace
78
+ where n.nspname = 'agent_board'
79
+ and p.proname = 'enroll_federated_human') s
80
+ ) t),
81
+ 'gate-before-owner',
82
+ 'G-2: the absence gate precedes both _ensure_agent_auth_user and the auth.users lock');
83
+
84
+ -- Fixtures for the allow side and for the deeper gates the new one must not shadow.
85
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
86
+ values ('193a0000-0000-0000-0000-000000000001', 'agent', 'agent',
87
+ 'lo193-owner', 'LO193 Owner', 'active');
88
+
89
+ select count(*) from agent_board.create_board(
90
+ '193a0000-0000-0000-0000-000000000001', 'lo193-board', 'LO193 Board',
91
+ 'Consiliency/message-board#193 provider-absence gate', jsonb_build_object(), 'private');
92
+
93
+ insert into agent_board.identity_providers(
94
+ issuer, audience, supported, policy_approved,
95
+ allowed_scopes, allowed_board_scopes, allowed_agent_kinds, default_board_id)
96
+ values
97
+ ('https://lo193-autojoin.test', 'message-board', true, true,
98
+ ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'],
99
+ (select id from agent_board.boards where slug = 'lo193-board')),
100
+ ('https://lo193-noboard.test', 'message-board', true, true,
101
+ ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null),
102
+ ('https://lo193-unapproved.test', 'message-board', true, false,
103
+ ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null);
104
+
105
+ -- G-3/G-4: provider PRESENT with a designated default board -- enrollment succeeds
106
+ -- and the auto-join still lands. The gate narrowed the absent case only.
107
+ select lives_ok(
108
+ $$ select agent_board.enroll_federated_human(
109
+ 'https://lo193-autojoin.test', 'message-board', 'lo193-joiner',
110
+ jsonb_build_object('sub','lo193-joiner'), 'LO193 Joiner') $$,
111
+ 'G-3: an allowlisted provider with a default board still enrolls');
112
+ select is(
113
+ (select count(*)::int
114
+ from agent_board.board_memberships m
115
+ join agent_board.agents a on a.id = m.agent_id
116
+ where m.board_id = (select id from agent_board.boards where slug = 'lo193-board')
117
+ and a.metadata #>> ARRAY['federation','subject'] = 'lo193-joiner'
118
+ and m.membership_state = 'joined'),
119
+ 1, 'G-4: the fedsession auto-join still lands a joined membership');
120
+
121
+ -- G-5: provider PRESENT with a NULL default board -- enrolls, no auto-join. This is
122
+ -- the neighbouring absence 20260901000140 already handled (NULL designation), kept
123
+ -- distinct from the provider-absent case this migration closes.
124
+ select lives_ok(
125
+ $$ select agent_board.enroll_federated_human(
126
+ 'https://lo193-noboard.test', 'message-board', 'lo193-plain',
127
+ jsonb_build_object('sub','lo193-plain'), 'LO193 Plain') $$,
128
+ 'G-5: an allowlisted provider with no default board still enrolls');
129
+
130
+ -- G-6: the deeper provider gates are NOT shadowed -- an allowlisted but
131
+ -- unapproved provider still fails inside the delegate, with the delegate's own
132
+ -- message. Asserted positively so "42501 came from somewhere" is never enough.
133
+ select throws_ok(
134
+ $$ select agent_board.enroll_federated_human(
135
+ 'https://lo193-unapproved.test', 'message-board', 'lo193-unapproved',
136
+ jsonb_build_object('sub','lo193-unapproved'), null) $$,
137
+ '42501', 'identity provider is not policy-approved',
138
+ 'G-6: a present-but-unapproved provider is still rejected by the delegate');
139
+
140
+ -- G-7: the pre-lock gate is the ONLY new rejection -- a present provider never
141
+ -- reaches it. (Mutating the gate to fire unconditionally fails G-3/G-5/G-7.)
142
+ select is(
143
+ (select count(*)::int
144
+ from agent_board.agents a
145
+ where a.kind = 'human'
146
+ and a.metadata #>> ARRAY['federation','subject'] in ('lo193-joiner', 'lo193-plain')),
147
+ 2, 'G-7: both allowlisted enrollments provisioned their human agents');
148
+
149
+
150
+ -- G-8: the CLASS of routes that reject an absent provider, enumerated from the
151
+ -- catalog rather than described in prose. All four reject with 42501; none
152
+ -- serializes the absence, and that is the recorded decision (#195) -- the
153
+ -- serialization alternative was built, measured and reverted because no shipped
154
+ -- caller can distinguish 42501 from a retryable code. A new route joining this
155
+ -- class fails here and names itself.
156
+ select is(
157
+ (select string_agg(p.proname, ', ' order by p.proname)
158
+ from pg_proc p
159
+ join pg_namespace n on n.oid = p.pronamespace
160
+ where n.nspname = 'agent_board'
161
+ and p.prokind = 'f'
162
+ and regexp_replace(p.prosrc,
163
+ $re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
164
+ '\2', 'g') ~ 'not in the identity-provider allowlist'),
165
+ '_enroll_federated_human_after_revocation_gate, _enroll_federated_identity_once, '
166
+ || 'enroll_federated_human, enroll_federated_identity',
167
+ 'G-8: exactly four routes reject an absent identity provider');
168
+
169
+ select * from finish();
170
+ rollback;
@@ -20,25 +20,22 @@ select has_view('agent_board', 'v_callback_delivery_health', 'v_callback_deliver
20
20
  select has_column('agent_board', 'v_agent_dashboard', 'inbox_unread_count', 'v_agent_dashboard exposes inbox_unread_count');
21
21
  select has_column('agent_board', 'v_agent_dashboard', 'inbox_pending_action_count', 'v_agent_dashboard exposes inbox_pending_action_count');
22
22
 
23
- select ok(
24
- (
25
- select count(*) = 9
26
- from pg_class c
27
- where c.relnamespace = 'agent_board'::regnamespace
28
- and c.relname in (
29
- 'v_agent_inbox',
30
- 'v_agent_inbox_counts',
31
- 'v_board_directory',
32
- 'v_board_memberships',
33
- 'v_board_feed',
34
- 'v_callback_endpoint_status',
35
- 'v_callback_subscriptions',
36
- 'v_callback_delivery_health',
37
- 'v_agent_dashboard'
38
- )
39
- and c.reloptions @> array['security_invoker=true']
40
- ),
41
- 'all inbox read views use security_invoker=true'
23
+ -- Derived from the catalog, not from a list written here (message-board#191 board
24
+ -- review). This is the flag that ACTUALLY carries cross-agent isolation for
25
+ -- everything built on these views: an inner `security_invoker` view re-anchors
26
+ -- base-table RLS to the real querying user through any outer definer view, so the
27
+ -- outer view's own flag is inert while this one is load-bearing. A view added to
28
+ -- the schema and omitted from the old nine-name list was an unpinned read surface.
29
+ -- There are no exemptions -- all 26 agent_board views are invoker -- so a new one
30
+ -- that is not fails here and NAMES ITSELF.
31
+ select is(
32
+ (select coalesce(string_agg(c.relname, ', ' order by c.relname), '')
33
+ from pg_class c
34
+ where c.relnamespace = 'agent_board'::regnamespace
35
+ and c.relkind in ('v', 'm')
36
+ and not coalesce(c.reloptions, '{}'::text[]) @> array['security_invoker=true']),
37
+ '',
38
+ 'every agent_board view is security_invoker -- catalog-derived, no exemption list; a definer read surface names itself here'
42
39
  );
43
40
 
44
41
  select ok(
@@ -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;