@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,177 @@
|
|
|
1
|
+
-- Consiliency/message-board#193 -- enroll_federated_human skipped its board and
|
|
2
|
+
-- provider locking entirely when the provider row did not exist, then locked the
|
|
3
|
+
-- human owner and delegated. The delegate re-reads the provider under a FRESH READ
|
|
4
|
+
-- COMMITTED snapshot, so a provider inserted and committed in that gap was visible
|
|
5
|
+
-- to it and its default board was auto-joined while this transaction already held
|
|
6
|
+
-- auth.users:
|
|
7
|
+
--
|
|
8
|
+
-- portal fedsession (absent-provider path) holds owner -> wants board
|
|
9
|
+
-- onboard_federated_fleet_agent holds board -> wants owner
|
|
10
|
+
--
|
|
11
|
+
-- reproduced as `ERROR: deadlock detected`. This is the third instance of one
|
|
12
|
+
-- family (#153 machine<->domain, #189 ceiling-ABSENCE, this) plus #194: an
|
|
13
|
+
-- ordering guarantee that holds on the common path and escapes on an edge no
|
|
14
|
+
-- textual check can see. Here the escaping edge is ABSENCE -- there is no row to
|
|
15
|
+
-- lock, so the ordering simply does not happen.
|
|
16
|
+
--
|
|
17
|
+
-- THE FIX is to fail before acquiring anything, which is the only ordering-safe
|
|
18
|
+
-- answer when the thing to order on does not exist. The neighbouring case (a
|
|
19
|
+
-- concurrent NULL -> board designation) was already handled by 20260901000140 via
|
|
20
|
+
-- the snapshot/lock/compare; provider-absent-entirely was the case that escaped it.
|
|
21
|
+
--
|
|
22
|
+
-- BEHAVIOUR CHANGE IN THE RACE, written down rather than left to be discovered.
|
|
23
|
+
-- Pre-fix, a caller that lost a provider-creation race could OCCASIONALLY SUCCEED:
|
|
24
|
+
-- the delegate re-read the provider on a fresh READ COMMITTED snapshot and, if the
|
|
25
|
+
-- creation had committed in the gap, proceeded -- which is precisely the
|
|
26
|
+
-- interleaving that produced the deadlock. Post-fix that caller deterministically
|
|
27
|
+
-- fails ONCE, with the same 42501 a permanently unallowlisted issuer gets, and
|
|
28
|
+
-- succeeds on its next /portal-session call. That is the trade: an occasional
|
|
29
|
+
-- silent success bought with a cross-route cycle, exchanged for one deterministic
|
|
30
|
+
-- refusal.
|
|
31
|
+
--
|
|
32
|
+
-- WHY 42501 AND NOT A RETRYABLE 40001. No shipped caller distinguishes them.
|
|
33
|
+
-- Nothing under packages/*/src references 40001 or 40P01, and
|
|
34
|
+
-- AgentBoardClientError.retryable is set from HTTP status and only ever consumed by
|
|
35
|
+
-- the session-mint loop -- no code path maps a SQLSTATE to a retry. So the two
|
|
36
|
+
-- codes are operationally identical to every client that exists today, while 40001
|
|
37
|
+
-- would additionally instruct any FUTURE retry loop to spin forever on a genuinely
|
|
38
|
+
-- unallowlisted issuer. A serialization-based alternative (advisory-lock the
|
|
39
|
+
-- absence, re-read under it, let the race loser proceed) was built and reverted for
|
|
40
|
+
-- the same reason: it is machinery for a difference no caller can observe.
|
|
41
|
+
|
|
42
|
+
-- Recreated with CREATE OR REPLACE (ACL and COMMENT survive); grants and comment
|
|
43
|
+
-- restated below regardless, per the repo rule. Body is otherwise byte-identical to
|
|
44
|
+
-- 20260901000140.
|
|
45
|
+
--
|
|
46
|
+
-- WHAT THIS DOES NOT CHANGE. A permanently unallowlisted (issuer, audience) already
|
|
47
|
+
-- failed with 42501 -- from the delegate's step (4), after the owner had been
|
|
48
|
+
-- provisioned and locked. The verdict and the SQLSTATE are unchanged; only the
|
|
49
|
+
-- point at which they are reached moves, and with it the lock the transaction was
|
|
50
|
+
-- holding when it happened. tests/isolation/specs/fedsession_provider_absent_owner_lock.spec
|
|
51
|
+
-- pins exactly that: with a guard session holding the owner row, the pre-fix
|
|
52
|
+
-- function BLOCKS and the fixed one raises without waiting.
|
|
53
|
+
|
|
54
|
+
create or replace function agent_board.enroll_federated_human(
|
|
55
|
+
p_issuer text,
|
|
56
|
+
p_audience text,
|
|
57
|
+
p_subject text,
|
|
58
|
+
p_human_identity_claim jsonb,
|
|
59
|
+
p_display_name text default null
|
|
60
|
+
)
|
|
61
|
+
returns table(
|
|
62
|
+
agent_id uuid,
|
|
63
|
+
auth_user_id uuid,
|
|
64
|
+
handle text,
|
|
65
|
+
kind text,
|
|
66
|
+
status text,
|
|
67
|
+
provider_id text,
|
|
68
|
+
created_at timestamptz
|
|
69
|
+
)
|
|
70
|
+
language plpgsql
|
|
71
|
+
security definer
|
|
72
|
+
set search_path = agent_board, public
|
|
73
|
+
as $$
|
|
74
|
+
declare
|
|
75
|
+
v_provider_id agent_board.identity_providers.id%type;
|
|
76
|
+
v_default_board_id uuid;
|
|
77
|
+
v_locked_default_board_id uuid;
|
|
78
|
+
v_handle text;
|
|
79
|
+
v_uid uuid;
|
|
80
|
+
begin
|
|
81
|
+
if not agent_board._is_service_role() then
|
|
82
|
+
raise exception 'service_role required for enroll_federated_human' using errcode = '42501';
|
|
83
|
+
end if;
|
|
84
|
+
if p_subject is null then
|
|
85
|
+
raise exception 'verified subject is required' using errcode = '22023';
|
|
86
|
+
end if;
|
|
87
|
+
|
|
88
|
+
-- Snapshot the provider's auto-join target without a row lock so a non-null
|
|
89
|
+
-- target can be locked in canonical board -> provider order. Then lock the
|
|
90
|
+
-- provider row and require that the target did not change across the gap. A
|
|
91
|
+
-- concurrent NULL -> board designation is rejected for retry rather than
|
|
92
|
+
-- falling back to provider -> board and recreating the cross-route cycle.
|
|
93
|
+
select ip.id, ip.default_board_id
|
|
94
|
+
into v_provider_id, v_default_board_id
|
|
95
|
+
from agent_board.identity_providers ip
|
|
96
|
+
where ip.issuer = p_issuer
|
|
97
|
+
and ip.audience = p_audience
|
|
98
|
+
limit 1;
|
|
99
|
+
|
|
100
|
+
-- #193 PROVIDER-ABSENCE GATE, and it must come BEFORE the owner is provisioned
|
|
101
|
+
-- or locked. PostgreSQL cannot row-lock a row that does not exist, so an absent
|
|
102
|
+
-- provider left NOTHING to serialize on: this function used to skip the whole
|
|
103
|
+
-- board/provider ordering, lock the owner, and delegate -- and under READ
|
|
104
|
+
-- COMMITTED the delegate takes a FRESH snapshot, so a provider committed in that
|
|
105
|
+
-- gap became visible and its default board was auto-joined while this
|
|
106
|
+
-- transaction held auth.users. That is owner -> board, inverted against fleet
|
|
107
|
+
-- onboarding's board -> owner, and it deadlocked.
|
|
108
|
+
--
|
|
109
|
+
-- Rejecting here is a VALID serialization -- it orders this call before the
|
|
110
|
+
-- provider's creation, which is exactly what its own snapshot saw -- and it is
|
|
111
|
+
-- the same verdict _enroll_federated_human_after_revocation_gate step (4) would
|
|
112
|
+
-- have reached anyway for an allowlist miss, only now reached before any lock.
|
|
113
|
+
-- The message differs from the delegate's so a test can tell which gate fired.
|
|
114
|
+
-- NOTE: this is a permanent 42501, not a retryable 40001. A genuinely
|
|
115
|
+
-- unallowlisted issuer is a configuration error, and telling a caller to retry it
|
|
116
|
+
-- forever is worse than telling it no; the rare loser of a real provider-creation
|
|
117
|
+
-- race succeeds on its next /portal-session call.
|
|
118
|
+
if v_provider_id is null then
|
|
119
|
+
raise exception 'issuer/audience not in the identity-provider allowlist (rejected before any owner or board lock)'
|
|
120
|
+
using errcode = '42501';
|
|
121
|
+
end if;
|
|
122
|
+
|
|
123
|
+
if v_default_board_id is not null then
|
|
124
|
+
perform 1 from agent_board.boards b
|
|
125
|
+
where b.id = v_default_board_id
|
|
126
|
+
for update;
|
|
127
|
+
if not found then
|
|
128
|
+
raise exception 'federated human default board changed during enrollment'
|
|
129
|
+
using errcode = '40001';
|
|
130
|
+
end if;
|
|
131
|
+
end if;
|
|
132
|
+
|
|
133
|
+
-- Freeze the designation this transaction will delegate against. The provider row
|
|
134
|
+
-- is held FOR UPDATE through commit, and (issuer, audience) is UNIQUE, so the
|
|
135
|
+
-- delegate's own lookup cannot resolve to a different row or a changed
|
|
136
|
+
-- default_board_id than the one locked above.
|
|
137
|
+
select ip.default_board_id
|
|
138
|
+
into v_locked_default_board_id
|
|
139
|
+
from agent_board.identity_providers ip
|
|
140
|
+
where ip.id = v_provider_id
|
|
141
|
+
for update;
|
|
142
|
+
if not found or v_locked_default_board_id is distinct from v_default_board_id then
|
|
143
|
+
raise exception 'federated human default board changed during enrollment'
|
|
144
|
+
using errcode = '40001';
|
|
145
|
+
end if;
|
|
146
|
+
|
|
147
|
+
v_handle := 'fed-human-' || substr(
|
|
148
|
+
encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'),
|
|
149
|
+
1, 32);
|
|
150
|
+
v_uid := agent_board._ensure_agent_auth_user(v_handle);
|
|
151
|
+
|
|
152
|
+
perform 1 from auth.users u where u.id = v_uid for update;
|
|
153
|
+
if not found then
|
|
154
|
+
raise exception 'federated human owner not found after provisioning' using errcode = 'P0002';
|
|
155
|
+
end if;
|
|
156
|
+
if exists (
|
|
157
|
+
select 1
|
|
158
|
+
from agent_board.fleet_human_revocations r
|
|
159
|
+
where r.issuer = p_issuer
|
|
160
|
+
and r.subject = p_subject
|
|
161
|
+
) then
|
|
162
|
+
raise exception 'human identity is durably revoked' using errcode = '42501';
|
|
163
|
+
end if;
|
|
164
|
+
|
|
165
|
+
return query
|
|
166
|
+
select * from agent_board._enroll_federated_human_after_revocation_gate(
|
|
167
|
+
p_issuer, p_audience, p_subject, p_human_identity_claim, p_display_name
|
|
168
|
+
);
|
|
169
|
+
end;
|
|
170
|
+
$$;
|
|
171
|
+
|
|
172
|
+
comment on function agent_board.enroll_federated_human(text, text, text, jsonb, text) is
|
|
173
|
+
'Service-role federated-human enrollment used by /portal-session. Requires an allowlisted (issuer, audience) BEFORE provisioning or locking anything (#193: an absent provider cannot be row-locked, so proceeding left the board/provider ordering unenforced and inverted board -> owner against fleet onboarding), then locks a non-null default board before its provider row, freezes that designation, holds the deterministic auth.users owner lock, rejects durable issuer/subject revocation, and delegates to the internal provisioner.';
|
|
174
|
+
revoke all on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
175
|
+
from public, anon, authenticated;
|
|
176
|
+
grant execute on function agent_board.enroll_federated_human(text, text, text, jsonb, text)
|
|
177
|
+
to service_role;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
-- WAKERECOVERY / message-board#191 — a gap-safe wake recovery surface.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT THIS CLOSES. A wake consumer that gates recovery on a monotonic
|
|
4
|
+
-- cursor silently drops a late-committing writer. `callback_outbox_events.seq`
|
|
5
|
+
-- (#187) is an identity column and identity sequences are NON-TRANSACTIONAL, so
|
|
6
|
+
-- `seq` is monotonic in ASSIGNMENT order, not COMMIT order: a transaction that
|
|
7
|
+
-- inserts early and commits late holds a seq BELOW a cursor the consumer has
|
|
8
|
+
-- already persisted, and `seq > my_cursor` never returns it. The committed
|
|
9
|
+
-- isolation probe `outbox_wake_ordering.spec` records exactly that
|
|
10
|
+
-- (`late_committer_holds_a_lower_seq_than_the_persisted_cursor = t`).
|
|
11
|
+
--
|
|
12
|
+
-- THE CLASS IS WIDER THAN `seq`. `created_at` / `updated_at` default to `now()`,
|
|
13
|
+
-- which is `transaction_timestamp()` — also assigned before commit, and worse:
|
|
14
|
+
-- a fan-out writes N rows with byte-identical timestamps, so a timestamp cursor
|
|
15
|
+
-- cannot even totally order what it DOES see. The unsafe class is therefore
|
|
16
|
+
-- "gates recovery on ANY value assigned before commit", not "gates on seq".
|
|
17
|
+
--
|
|
18
|
+
-- THE FIX IS TO STOP ASKING AN ORDERING QUESTION. Restart recovery does not need
|
|
19
|
+
-- "what is new since I last looked" (a cursor); it needs "what have I not dealt
|
|
20
|
+
-- with" (a STATE). A late-committing insert lands with `action_state = 'new'`, so
|
|
21
|
+
-- the moment it is visible it is in the set. There is no value to compare and
|
|
22
|
+
-- therefore nothing to skip. This is the same property that makes
|
|
23
|
+
-- `claim_callback_outbox_events` lossless (#187: "the claim path is lossless
|
|
24
|
+
-- because it partitions on STATUS, never on seq"), applied to the surface a wake
|
|
25
|
+
-- consumer actually fetches from.
|
|
26
|
+
--
|
|
27
|
+
-- WHY A VIEW AND NOT A FILTER IN THE CONSUMER. A predicate that lives only in a
|
|
28
|
+
-- TypeScript descriptor cannot be pinned: an isolation spec could only assert a
|
|
29
|
+
-- query the spec itself writes, and mutating the spec to make the spec fail
|
|
30
|
+
-- proves nothing. A view puts the predicate in pg_catalog where a probe can read
|
|
31
|
+
-- it and a mutation can break it. `pg_get_viewdef` is regenerated from the parse
|
|
32
|
+
-- tree, so comments and formatting are gone by construction — a class assertion
|
|
33
|
+
-- over a view definition cannot be defeated by prose the way a `prosrc` scanner
|
|
34
|
+
-- in this repo was once defeated by a COMMENT placed beside an inverted lock.
|
|
35
|
+
--
|
|
36
|
+
-- WHAT THIS MIGRATION DOES NOT DO. No producer changes. `inbox_items` gains no
|
|
37
|
+
-- column, no trigger, no policy. `callback_outbox_events` and its `seq` are
|
|
38
|
+
-- untouched — `seq` keeps exactly the meaning #187 shipped it with (deterministic
|
|
39
|
+
-- claim ordering and observability, never a recovery cursor).
|
|
40
|
+
-- `agent_board.schema_version` stays at 8: this adds no enforcement switch.
|
|
41
|
+
--
|
|
42
|
+
-- RECIPIENT SCOPING IS IN THIS VIEW, NOT IN THE CONSUMER'S FILTER. RLS on
|
|
43
|
+
-- `inbox_items` is NOT self-scoped: `agent_board_authenticated_select_inbox_items`
|
|
44
|
+
-- (20260713000000) admits a second disjunct --
|
|
45
|
+
-- board_id is not null AND visible_in_board_feed AND caller is a co-member
|
|
46
|
+
-- -- and `post_board_message` upserts every board delivery with BOTH visibility
|
|
47
|
+
-- flags true. So an unfiltered read of the unhandled set hands a caller its
|
|
48
|
+
-- CO-MEMBERS' items, which a triage consumer would then act on. Measured, not
|
|
49
|
+
-- reasoned: driving the real `post_board_message` path and reading this view as
|
|
50
|
+
-- agent A returned 1 row belonging to agent B before this predicate was added.
|
|
51
|
+
--
|
|
52
|
+
-- The scoping deliberately lives HERE and not in the runtime's fetch descriptor.
|
|
53
|
+
-- The FLEETNOTIFY contract states that a hint's filters are never authority; a
|
|
54
|
+
-- security boundary placed in `filters` is one a caller can drop or mistype, and
|
|
55
|
+
-- no catalog assertion could pin it. In the view it is server-side, applies to
|
|
56
|
+
-- every caller regardless of the descriptor, and is pinnable from pg_catalog --
|
|
57
|
+
-- the same argument that made this a view rather than a TypeScript predicate.
|
|
58
|
+
--
|
|
59
|
+
-- ACL NOTE. There is no DROP FUNCTION here, so the ACL/COMMENT-discard trap does
|
|
60
|
+
-- not arise. A `create or replace view` preserves an existing ACL, so the revoke
|
|
61
|
+
-- below is stated explicitly rather than implied by omitting a grant: re-running
|
|
62
|
+
-- this migration over an earlier copy of the view must CONVERGE, not inherit.
|
|
63
|
+
--
|
|
64
|
+
-- `service_role` is deliberately DENIED, which is why the grants are NOT a mirror
|
|
65
|
+
-- of `v_agent_inbox`'s. `service_role` holds BYPASSRLS and `current_agent_id()`
|
|
66
|
+
-- returns NULL for it, so a scoped view would hand it a silently EMPTY recovery
|
|
67
|
+
-- set -- an identity-less principal being told "nothing to recover" is precisely
|
|
68
|
+
-- the silent-drop failure this migration exists to close. Denying SELECT converts
|
|
69
|
+
-- that silence into a loud `permission denied`. This surface is defined relative
|
|
70
|
+
-- to the session's own agent identity; a principal without one has no answer here,
|
|
71
|
+
-- and must read `v_agent_inbox` with an explicit recipient filter instead.
|
|
72
|
+
|
|
73
|
+
-- Built on `v_agent_inbox`, not on `inbox_items`, so `visible_in_inbox = true` is
|
|
74
|
+
-- INHERITED rather than restated: one rule, one place.
|
|
75
|
+
--
|
|
76
|
+
-- WHAT `security_invoker = true` BUYS -- corrected AGAIN, from a full matrix.
|
|
77
|
+
-- Cross-agent exclusion is enforced by the `current_agent_id()` predicate below,
|
|
78
|
+
-- not by RLS. With that predicate deleted and the read taken as agent A, the four
|
|
79
|
+
-- cells are (co-member rows / stranger rows returned):
|
|
80
|
+
--
|
|
81
|
+
-- this view v_agent_inbox co-member stranger
|
|
82
|
+
-- invoker invoker 1 0
|
|
83
|
+
-- reset invoker 1 0
|
|
84
|
+
-- invoker reset 1 1
|
|
85
|
+
-- reset reset 1 1
|
|
86
|
+
--
|
|
87
|
+
-- THIS view's flag is inert in every cell. The bound is carried entirely by
|
|
88
|
+
-- `v_agent_inbox`'s flag: an inner `security_invoker` view re-anchors base-table
|
|
89
|
+
-- RLS to the real querying user through any outer definer view. An earlier
|
|
90
|
+
-- version of this paragraph said the bound held "on this view OR on
|
|
91
|
+
-- `v_agent_inbox`" -- attributing it to a flag that does not carry it. The flag
|
|
92
|
+
-- is still declared here because it becomes load-bearing the moment this view is
|
|
93
|
+
-- rebuilt directly on `inbox_items`, and `v_agent_inbox`'s flag is pinned as a
|
|
94
|
+
-- catalog-derived class over every agent_board view in
|
|
95
|
+
-- agent_board_inbox_reads.test.sql.
|
|
96
|
+
--
|
|
97
|
+
-- WHAT RECOVERY COMPLETENESS NOW DEPENDS ON, stated because this migration is what
|
|
98
|
+
-- makes it depend on them. `current_agent_id()` resolves `auth.uid()` to one
|
|
99
|
+
-- active agent. It carries no ORDER BY, but that is NOT a hazard: partial unique
|
|
100
|
+
-- indexes `idx_agent_board_agents_auth_user_id_unique` (auth_user_id IS NOT NULL)
|
|
101
|
+
-- and `..._active_unique` make two agents on one auth user impossible, so the
|
|
102
|
+
-- resolution is single-valued. (Checked -- a review seat reported "no unique
|
|
103
|
+
-- constraint on agents.auth_user_id", which is true of pg_constraint and false of
|
|
104
|
+
-- the schema: the guarantee is enforced by unique INDEXES.)
|
|
105
|
+
--
|
|
106
|
+
-- The real exposure is the other one: if the caller has NO active agent -- revoked,
|
|
107
|
+
-- or an operator session with no agent identity -- `current_agent_id()` returns
|
|
108
|
+
-- NULL and this view returns an EMPTY set rather than raising. Empty is
|
|
109
|
+
-- indistinguishable from "nothing to recover", which is the same silent-drop shape
|
|
110
|
+
-- the `service_role` revoke below closes, one layer up. A consumer that can be
|
|
111
|
+
-- identity-less must check its identity, not infer it from an empty recovery set.
|
|
112
|
+
--
|
|
113
|
+
-- There is deliberately NO `ORDER BY`. The unhandled set is a SET, not a cursor
|
|
114
|
+
-- stream. Ordering is presentation and belongs to the caller; putting one here
|
|
115
|
+
-- would invite keyset pagination over `created_at`, which is the exact defect
|
|
116
|
+
-- this migration closes.
|
|
117
|
+
--
|
|
118
|
+
-- The state set is `('new', 'restored', 'deferred')` VERBATIM from the existing
|
|
119
|
+
-- partial index `idx_agent_board_inbox_items_agent_pending`, so the index serves
|
|
120
|
+
-- this view. The pgTAP asserts that equality from the catalog on BOTH sides, so
|
|
121
|
+
-- editing either one alone fails.
|
|
122
|
+
create or replace view agent_board.v_agent_wake_recovery
|
|
123
|
+
with (security_invoker = true)
|
|
124
|
+
as
|
|
125
|
+
select
|
|
126
|
+
vi.inbox_item_id,
|
|
127
|
+
vi.delivery_recipient_id,
|
|
128
|
+
vi.recipient_agent_id,
|
|
129
|
+
vi.recipient_handle,
|
|
130
|
+
vi.recipient_display_name,
|
|
131
|
+
vi.board_id,
|
|
132
|
+
vi.board_slug,
|
|
133
|
+
vi.board_name,
|
|
134
|
+
vi.delivery_mode,
|
|
135
|
+
vi.source_kind,
|
|
136
|
+
vi.source_message_id,
|
|
137
|
+
vi.source_task_id,
|
|
138
|
+
vi.source_delegation_id,
|
|
139
|
+
vi.subject,
|
|
140
|
+
vi.preview,
|
|
141
|
+
vi.priority,
|
|
142
|
+
vi.action_state,
|
|
143
|
+
vi.visible_in_board_feed,
|
|
144
|
+
vi.fetch_descriptor,
|
|
145
|
+
vi.created_at,
|
|
146
|
+
vi.updated_at
|
|
147
|
+
from agent_board.v_agent_inbox vi
|
|
148
|
+
where vi.action_state in ('new', 'restored', 'deferred')
|
|
149
|
+
and vi.recipient_agent_id = agent_board.current_agent_id();
|
|
150
|
+
|
|
151
|
+
grant select on agent_board.v_agent_wake_recovery to authenticated;
|
|
152
|
+
revoke all on agent_board.v_agent_wake_recovery from service_role;
|
|
153
|
+
|
|
154
|
+
comment on view agent_board.v_agent_wake_recovery is
|
|
155
|
+
'message-board#191 gap-safe wake recovery surface: the caller''s UNHANDLED inbox set (action_state in new/restored/deferred), partitioned on STATUS and carrying no cursor. Recovery must never gate on a pre-commit-assigned value (callback_outbox_events.seq, created_at, updated_at): those order ASSIGNMENT, not COMMIT, so a transaction that inserts early and commits late lands below an already-persisted cursor and is silently dropped. Scoped to the calling agent (current_agent_id()): RLS on inbox_items is NOT self-scoped -- it also admits co-members'' board-feed-visible items -- so the scoping is stated here rather than left to a caller''s filter. EXIT PATHS, verified against pg_catalog and not against the design doc: agent_board.update_inbox_item_state is the ONLY function that writes inbox_items.action_state. mark_read does NOT -- it writes read_receipts, so a read item is still unhandled. notification_ack is not a database function at all; it is a client-side alias that calls update_inbox_item_state with action_state = ''acknowledged''. DRAIN IN BOUNDED PASSES, NEVER "UNTIL EMPTY": ''deferred'' is itself an explicit ack write AND is a member of this set, so deferring an item acknowledges it and leaves it here -- a consumer that defers anything and loops until empty never terminates. Concurrent inserts can also outpace processing. A drain MUST bound its own passes and must not treat a non-empty set as a reason to keep looping. WHY THE ONE-WRITER CLAIM IS STRUCTURAL: inbox_items carries no user trigger, and authenticated/service_role hold SELECT only (no direct UPDATE), so nothing can move an item out of this set except the definer RPC. The second DML writer, _upsert_inbox_item, never touches action_state on conflict -- though it does overwrite visible_in_inbox, which would hide an item from this set with no ack if a caller ever passed false (no current caller does; the argument is positional and this is not pinned). security_invoker, so RLS on inbox_items still applies underneath the scoping.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|