@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.
- package/fixtures/already-applied.json +160 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +45 -31
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +27 -11
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
- 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,
|
|
57
|
-
|
|
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;
|