@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,74 @@
|
|
|
1
|
+
-- Consiliency/message-board#162 follow-up: /portal-session provisions through
|
|
2
|
+
-- enroll_federated_human, so that route must share the durable human-revocation
|
|
3
|
+
-- owner lock and marker gate with fleet enrollment.
|
|
4
|
+
|
|
5
|
+
alter function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
6
|
+
rename to _enroll_federated_human_after_revocation_gate;
|
|
7
|
+
|
|
8
|
+
revoke all on function agent_board._enroll_federated_human_after_revocation_gate(text, text, text, jsonb, text)
|
|
9
|
+
from public, anon, authenticated, service_role;
|
|
10
|
+
comment on function agent_board._enroll_federated_human_after_revocation_gate(text, text, text, jsonb, text) is
|
|
11
|
+
'Internal federated-human provisioner. Call only through enroll_federated_human, which holds the deterministic auth.users owner lock and rejects durable issuer/subject revocation markers.';
|
|
12
|
+
|
|
13
|
+
create or replace function agent_board.enroll_federated_human(
|
|
14
|
+
p_issuer text,
|
|
15
|
+
p_audience text,
|
|
16
|
+
p_subject text,
|
|
17
|
+
p_human_identity_claim jsonb,
|
|
18
|
+
p_display_name text default null
|
|
19
|
+
)
|
|
20
|
+
returns table(
|
|
21
|
+
agent_id uuid,
|
|
22
|
+
auth_user_id uuid,
|
|
23
|
+
handle text,
|
|
24
|
+
kind text,
|
|
25
|
+
status text,
|
|
26
|
+
provider_id text,
|
|
27
|
+
created_at timestamptz
|
|
28
|
+
)
|
|
29
|
+
language plpgsql
|
|
30
|
+
security definer
|
|
31
|
+
set search_path = agent_board, public
|
|
32
|
+
as $$
|
|
33
|
+
declare
|
|
34
|
+
v_handle text;
|
|
35
|
+
v_uid uuid;
|
|
36
|
+
begin
|
|
37
|
+
if not agent_board._is_service_role() then
|
|
38
|
+
raise exception 'service_role required for enroll_federated_human' using errcode = '42501';
|
|
39
|
+
end if;
|
|
40
|
+
if p_subject is null then
|
|
41
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
42
|
+
end if;
|
|
43
|
+
|
|
44
|
+
v_handle := 'fed-human-' || substr(
|
|
45
|
+
encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'),
|
|
46
|
+
1, 32);
|
|
47
|
+
v_uid := agent_board._ensure_agent_auth_user(v_handle);
|
|
48
|
+
|
|
49
|
+
perform 1 from auth.users u where u.id = v_uid for update;
|
|
50
|
+
if not found then
|
|
51
|
+
raise exception 'federated human owner not found after provisioning' using errcode = 'P0002';
|
|
52
|
+
end if;
|
|
53
|
+
if exists (
|
|
54
|
+
select 1
|
|
55
|
+
from agent_board.fleet_human_revocations r
|
|
56
|
+
where r.issuer = p_issuer
|
|
57
|
+
and r.subject = p_subject
|
|
58
|
+
) then
|
|
59
|
+
raise exception 'human identity is durably revoked' using errcode = '42501';
|
|
60
|
+
end if;
|
|
61
|
+
|
|
62
|
+
return query
|
|
63
|
+
select * from agent_board._enroll_federated_human_after_revocation_gate(
|
|
64
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, p_display_name
|
|
65
|
+
);
|
|
66
|
+
end;
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
comment on function agent_board.enroll_federated_human(text, text, text, jsonb, text) is
|
|
70
|
+
'Service-role federated-human enrollment used by /portal-session. Derives the deterministic owner, holds its auth.users row lock through commit, rejects durable issuer/subject revocation, then delegates to the internal provisioner.';
|
|
71
|
+
revoke all on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
72
|
+
from public, anon, authenticated;
|
|
73
|
+
grant execute on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
74
|
+
to service_role;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- Consiliency/message-board#162 follow-up: a federated-human provisioner may
|
|
2
|
+
-- auto-join its provider's default board. Fleet onboarding takes board -> owner,
|
|
3
|
+
-- so /portal-session must take the same order rather than owner -> board.
|
|
4
|
+
|
|
5
|
+
create or replace function agent_board.enroll_federated_human(
|
|
6
|
+
p_issuer text,
|
|
7
|
+
p_audience text,
|
|
8
|
+
p_subject text,
|
|
9
|
+
p_human_identity_claim jsonb,
|
|
10
|
+
p_display_name text default null
|
|
11
|
+
)
|
|
12
|
+
returns table(
|
|
13
|
+
agent_id uuid,
|
|
14
|
+
auth_user_id uuid,
|
|
15
|
+
handle text,
|
|
16
|
+
kind text,
|
|
17
|
+
status text,
|
|
18
|
+
provider_id text,
|
|
19
|
+
created_at timestamptz
|
|
20
|
+
)
|
|
21
|
+
language plpgsql
|
|
22
|
+
security definer
|
|
23
|
+
set search_path = agent_board, public
|
|
24
|
+
as $$
|
|
25
|
+
declare
|
|
26
|
+
v_provider_id agent_board.identity_providers.id%type;
|
|
27
|
+
v_default_board_id uuid;
|
|
28
|
+
v_locked_default_board_id uuid;
|
|
29
|
+
v_handle text;
|
|
30
|
+
v_uid uuid;
|
|
31
|
+
begin
|
|
32
|
+
if not agent_board._is_service_role() then
|
|
33
|
+
raise exception 'service_role required for enroll_federated_human' using errcode = '42501';
|
|
34
|
+
end if;
|
|
35
|
+
if p_subject is null then
|
|
36
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
37
|
+
end if;
|
|
38
|
+
|
|
39
|
+
-- Snapshot the provider's auto-join target without a row lock so a non-null
|
|
40
|
+
-- target can be locked in canonical board -> provider order. Then lock the
|
|
41
|
+
-- provider row and require that the target did not change across the gap. A
|
|
42
|
+
-- concurrent NULL -> board designation is rejected for retry rather than
|
|
43
|
+
-- falling back to provider -> board and recreating the cross-route cycle.
|
|
44
|
+
select ip.id, ip.default_board_id
|
|
45
|
+
into v_provider_id, v_default_board_id
|
|
46
|
+
from agent_board.identity_providers ip
|
|
47
|
+
where ip.issuer = p_issuer
|
|
48
|
+
and ip.audience = p_audience
|
|
49
|
+
limit 1;
|
|
50
|
+
if v_provider_id is not null then
|
|
51
|
+
if v_default_board_id is not null then
|
|
52
|
+
perform 1 from agent_board.boards b
|
|
53
|
+
where b.id = v_default_board_id
|
|
54
|
+
for update;
|
|
55
|
+
if not found then
|
|
56
|
+
raise exception 'federated human default board changed during enrollment'
|
|
57
|
+
using errcode = '40001';
|
|
58
|
+
end if;
|
|
59
|
+
end if;
|
|
60
|
+
|
|
61
|
+
select ip.default_board_id
|
|
62
|
+
into v_locked_default_board_id
|
|
63
|
+
from agent_board.identity_providers ip
|
|
64
|
+
where ip.id = v_provider_id
|
|
65
|
+
for update;
|
|
66
|
+
if not found or v_locked_default_board_id is distinct from v_default_board_id then
|
|
67
|
+
raise exception 'federated human default board changed during enrollment'
|
|
68
|
+
using errcode = '40001';
|
|
69
|
+
end if;
|
|
70
|
+
end if;
|
|
71
|
+
|
|
72
|
+
v_handle := 'fed-human-' || substr(
|
|
73
|
+
encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'),
|
|
74
|
+
1, 32);
|
|
75
|
+
v_uid := agent_board._ensure_agent_auth_user(v_handle);
|
|
76
|
+
|
|
77
|
+
perform 1 from auth.users u where u.id = v_uid for update;
|
|
78
|
+
if not found then
|
|
79
|
+
raise exception 'federated human owner not found after provisioning' using errcode = 'P0002';
|
|
80
|
+
end if;
|
|
81
|
+
if exists (
|
|
82
|
+
select 1
|
|
83
|
+
from agent_board.fleet_human_revocations r
|
|
84
|
+
where r.issuer = p_issuer
|
|
85
|
+
and r.subject = p_subject
|
|
86
|
+
) then
|
|
87
|
+
raise exception 'human identity is durably revoked' using errcode = '42501';
|
|
88
|
+
end if;
|
|
89
|
+
|
|
90
|
+
return query
|
|
91
|
+
select * from agent_board._enroll_federated_human_after_revocation_gate(
|
|
92
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, p_display_name
|
|
93
|
+
);
|
|
94
|
+
end;
|
|
95
|
+
$$;
|
|
96
|
+
|
|
97
|
+
comment on function agent_board.enroll_federated_human(text, text, text, jsonb, text) is
|
|
98
|
+
'Service-role federated-human enrollment used by /portal-session. Locks a non-null default board before its provider row, freezes that designation, then holds the deterministic auth.users owner lock, rejects durable issuer/subject revocation, and delegates to the internal provisioner.';
|
|
99
|
+
revoke all on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
100
|
+
from public, anon, authenticated;
|
|
101
|
+
grant execute on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
102
|
+
to service_role;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
-- OUTBOXWAKE / #187 — monotonic wake ordering for the notification outbox.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT IS ALREADY LIVE, not hypothetical. `agent_board.callback_outbox_events`
|
|
4
|
+
-- has no monotonic anchor: its only ordering columns are `id uuid default
|
|
5
|
+
-- gen_random_uuid()` and `created_at`/`available_at timestamptz default now()`.
|
|
6
|
+
-- `claim_callback_outbox_events` orders its candidate set by
|
|
7
|
+
-- order by coe.available_at asc, coe.created_at asc, coe.id asc
|
|
8
|
+
-- whose FINAL tiebreak is a random v4 uuid. And `now()` is `transaction_timestamp()`,
|
|
9
|
+
-- constant within a transaction, so a multicast fan-out writes N rows with
|
|
10
|
+
-- byte-identical `created_at`. Timestamp collision is the NORMAL path here, not the
|
|
11
|
+
-- edge case, so which rows a bounded batch claims — and in what order a worker sees
|
|
12
|
+
-- them — is decided by uuid randomness today.
|
|
13
|
+
--
|
|
14
|
+
-- This repo already met and solved this hazard once, on the consent substrate
|
|
15
|
+
-- (`20260806000100_agent_board_consent_substrate.sql`), with `board_membership_events.seq`.
|
|
16
|
+
-- That was, until now, the only identity column in the whole schema. The notification
|
|
17
|
+
-- path never got the same treatment. This migration gives it one.
|
|
18
|
+
--
|
|
19
|
+
-- WHY `generated always` AND NOT `by default`:
|
|
20
|
+
-- `seq` is EVIDENCE of write order, in the same sense that provenance is derived from
|
|
21
|
+
-- the session and never from a caller argument. `generated always` makes the value
|
|
22
|
+
-- un-suppliable: an INSERT that names `seq` is rejected by the server (SQLSTATE 428C9),
|
|
23
|
+
-- so no writer — present or future — can assert an ordering it did not earn. Only an
|
|
24
|
+
-- explicit `overriding system value` can bypass that, and the class assertion in
|
|
25
|
+
-- `agent_board_outbox_wake_ordering.test.sql` fails, by name, on any function that uses it.
|
|
26
|
+
--
|
|
27
|
+
-- BACKFILL: none required. `callback_outbox_events` holds 0 rows in production
|
|
28
|
+
-- (verified before writing this migration), so adding the identity column needs no
|
|
29
|
+
-- decision about what order pre-existing rows "should" have had.
|
|
30
|
+
--
|
|
31
|
+
-- VISIBILITY IS NOT WIDENED. `seq` is exposed only on surfaces a consumer can already
|
|
32
|
+
-- read: the table itself (RLS unchanged) and the service_role-only claim RPC, which
|
|
33
|
+
-- already returns the row's contents. No view is created or altered.
|
|
34
|
+
--
|
|
35
|
+
-- WHAT THIS MIGRATION DOES NOT GIVE YOU — read this before building a consumer on `seq`:
|
|
36
|
+
-- `seq` is monotonic in ASSIGNMENT order, NOT in COMMIT order. The sequence is
|
|
37
|
+
-- non-transactional, so a transaction that inserts early and commits late holds a LOWER
|
|
38
|
+
-- seq than rows a consumer has already seen and acknowledged. `seq > my persisted cursor`
|
|
39
|
+
-- is therefore NOT a safe recovery predicate: it silently skips the late committer.
|
|
40
|
+
-- Recovery must gate on transaction visibility or on row STATUS — never on `seq` alone.
|
|
41
|
+
-- `claim_callback_outbox_events` is safe because it selects on `outbox_status`, not on a
|
|
42
|
+
-- cursor. `agent-board-runtime/src/notifications.ts` (`shouldReplayMissedEvent`) does gate
|
|
43
|
+
-- on `seq` today and is exposed to this; fixing it needs the wake contract and is tracked
|
|
44
|
+
-- separately, NOT in this migration.
|
|
45
|
+
--
|
|
46
|
+
-- What this migration delivers, stated exactly: DETERMINISTIC CLAIM ORDERING over rows
|
|
47
|
+
-- that share a timestamp, and a total order to build a gap-safe protocol on later.
|
|
48
|
+
|
|
49
|
+
alter table agent_board.callback_outbox_events
|
|
50
|
+
add column if not exists seq bigint generated always as identity;
|
|
51
|
+
|
|
52
|
+
comment on column agent_board.callback_outbox_events.seq is
|
|
53
|
+
'Monotonic write-order evidence for wake ordering (#187). GENERATED ALWAYS: not caller-suppliable. Gives same-timestamp rows a total order, which `created_at` cannot because now() is transaction_timestamp() and a fan-out shares it. CAUTION: monotonic in ASSIGNMENT order, NOT commit order -- a late-committing transaction holds a lower seq than rows already consumed, so `seq > cursor` is NOT a safe recovery predicate. Gate recovery on transaction visibility or on outbox_status, never on seq alone.';
|
|
54
|
+
|
|
55
|
+
create unique index if not exists uq_agent_board_callback_outbox_events_seq
|
|
56
|
+
on agent_board.callback_outbox_events(seq);
|
|
57
|
+
|
|
58
|
+
-- The consumer's access path is "rows for me newer than the cursor I persisted".
|
|
59
|
+
create index if not exists idx_agent_board_callback_outbox_events_recipient_seq
|
|
60
|
+
on agent_board.callback_outbox_events(recipient_agent_id, seq);
|
|
61
|
+
|
|
62
|
+
-- Claim ordering: `available_at` stays FIRST because it is scheduling (a row is not
|
|
63
|
+
-- eligible before its time), and `seq` replaces the (created_at, id) pair because it is
|
|
64
|
+
-- ordering. The uuid tiebreak is dropped entirely.
|
|
65
|
+
--
|
|
66
|
+
-- The returns table gains `outbox_seq` so a worker can observe and record the order it was
|
|
67
|
+
-- handed rows in. It is NOT a lossless recovery cursor — see the caution above. That is a
|
|
68
|
+
-- RETURN TYPE change, so the function must be dropped and recreated rather than replaced.
|
|
69
|
+
--
|
|
70
|
+
-- DROP FUNCTION DISCARDS THE ACL AND THE COMMENT. Recreating a SECURITY DEFINER function
|
|
71
|
+
-- grants EXECUTE to PUBLIC by default. This repo shipped a real gate bypass by omitting
|
|
72
|
+
-- exactly this restatement, so the revoke, the grant, and the comment are restated below
|
|
73
|
+
-- in this SAME migration, and asserted by the test file.
|
|
74
|
+
drop function if exists agent_board.claim_callback_outbox_events(integer);
|
|
75
|
+
|
|
76
|
+
create function agent_board.claim_callback_outbox_events(
|
|
77
|
+
p_batch_size integer default 25
|
|
78
|
+
)
|
|
79
|
+
returns table(
|
|
80
|
+
outbox_event_id uuid,
|
|
81
|
+
endpoint_id uuid,
|
|
82
|
+
subscription_id uuid,
|
|
83
|
+
recipient_agent_id uuid,
|
|
84
|
+
inbox_item_id uuid,
|
|
85
|
+
board_id uuid,
|
|
86
|
+
endpoint_url text,
|
|
87
|
+
signing_key_version integer,
|
|
88
|
+
event_category text,
|
|
89
|
+
delivery_mode text,
|
|
90
|
+
source_kind text,
|
|
91
|
+
source_message_id uuid,
|
|
92
|
+
source_task_id uuid,
|
|
93
|
+
source_delegation_id uuid,
|
|
94
|
+
idempotency_key text,
|
|
95
|
+
subject text,
|
|
96
|
+
preview text,
|
|
97
|
+
priority text,
|
|
98
|
+
fetch_descriptor jsonb,
|
|
99
|
+
outbox_created_at timestamptz,
|
|
100
|
+
queued_at timestamptz,
|
|
101
|
+
claimed_at timestamptz,
|
|
102
|
+
attempt_number integer,
|
|
103
|
+
endpoint_status text,
|
|
104
|
+
subscription_status text,
|
|
105
|
+
metadata jsonb,
|
|
106
|
+
outbox_seq bigint
|
|
107
|
+
)
|
|
108
|
+
language plpgsql
|
|
109
|
+
security definer
|
|
110
|
+
set search_path = agent_board, public
|
|
111
|
+
as $$
|
|
112
|
+
begin
|
|
113
|
+
if p_batch_size is null or p_batch_size < 1 or p_batch_size > 100 then
|
|
114
|
+
raise exception 'callback batch size must be between 1 and 100' using errcode = '22023';
|
|
115
|
+
end if;
|
|
116
|
+
|
|
117
|
+
return query
|
|
118
|
+
with candidates as (
|
|
119
|
+
select coe.id
|
|
120
|
+
from agent_board.callback_outbox_events coe
|
|
121
|
+
join agent_board.callback_endpoints ce on ce.id = coe.endpoint_id
|
|
122
|
+
join agent_board.callback_subscriptions cs on cs.id = coe.subscription_id
|
|
123
|
+
where coe.outbox_status in ('pending', 'retryable')
|
|
124
|
+
and coe.available_at <= now()
|
|
125
|
+
and ce.endpoint_status = 'verified'
|
|
126
|
+
and cs.subscription_status = 'active'
|
|
127
|
+
order by coe.available_at asc, coe.seq asc
|
|
128
|
+
for update of coe skip locked
|
|
129
|
+
limit p_batch_size
|
|
130
|
+
),
|
|
131
|
+
updated as (
|
|
132
|
+
update agent_board.callback_outbox_events coe
|
|
133
|
+
set outbox_status = 'delivering',
|
|
134
|
+
locked_at = now(),
|
|
135
|
+
updated_at = now()
|
|
136
|
+
from candidates
|
|
137
|
+
where coe.id = candidates.id
|
|
138
|
+
returning coe.*
|
|
139
|
+
)
|
|
140
|
+
select
|
|
141
|
+
u.id,
|
|
142
|
+
u.endpoint_id,
|
|
143
|
+
u.subscription_id,
|
|
144
|
+
u.recipient_agent_id,
|
|
145
|
+
u.inbox_item_id,
|
|
146
|
+
u.board_id,
|
|
147
|
+
ce.endpoint_url,
|
|
148
|
+
ce.signing_key_version,
|
|
149
|
+
u.event_category,
|
|
150
|
+
u.delivery_mode,
|
|
151
|
+
u.source_kind,
|
|
152
|
+
u.source_message_id,
|
|
153
|
+
u.source_task_id,
|
|
154
|
+
u.source_delegation_id,
|
|
155
|
+
u.idempotency_key,
|
|
156
|
+
u.subject,
|
|
157
|
+
u.preview,
|
|
158
|
+
u.priority,
|
|
159
|
+
u.fetch_descriptor,
|
|
160
|
+
u.created_at,
|
|
161
|
+
u.available_at,
|
|
162
|
+
u.locked_at,
|
|
163
|
+
agent_board._next_callback_attempt_number(u.id),
|
|
164
|
+
ce.endpoint_status,
|
|
165
|
+
cs.subscription_status,
|
|
166
|
+
coalesce(u.metadata, '{}'::jsonb),
|
|
167
|
+
u.seq
|
|
168
|
+
from updated u
|
|
169
|
+
join agent_board.callback_endpoints ce on ce.id = u.endpoint_id
|
|
170
|
+
join agent_board.callback_subscriptions cs on cs.id = u.subscription_id
|
|
171
|
+
-- The batch is also RETURNED in wake order. Without this the claimed SET would be
|
|
172
|
+
-- deterministic while the order the worker observes it in stayed arbitrary, which is
|
|
173
|
+
-- the same defect one join away.
|
|
174
|
+
order by u.seq asc;
|
|
175
|
+
end;
|
|
176
|
+
$$;
|
|
177
|
+
|
|
178
|
+
revoke all on function agent_board.claim_callback_outbox_events(integer)
|
|
179
|
+
from public, anon, authenticated;
|
|
180
|
+
grant execute on function agent_board.claim_callback_outbox_events(integer)
|
|
181
|
+
to service_role;
|
|
182
|
+
|
|
183
|
+
comment on function agent_board.claim_callback_outbox_events(integer) is
|
|
184
|
+
'Service-role notification worker claim: locks the ready outbox batch with SKIP LOCKED and marks it delivering. Ordered by (available_at, seq) — scheduling first, then monotonic write order (#187), so rows sharing a timestamp are claimed deterministically. Losslessness comes from selecting on outbox_status, NOT from seq: seq is monotonic in assignment order, not commit order, so `seq > cursor` is not a safe recovery predicate. Returns outbox_seq for observability and ordering, not as a recovery cursor.';
|