@consiliency/agent-board-schema 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/fixtures/already-applied.json +36 -0
  2. package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
  3. package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
  4. package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
  5. package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
  6. package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
  7. package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
  8. package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
  9. package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
  10. package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
  11. package/package.json +1 -1
  12. package/schema-manifest.json +84 -3
  13. package/src/tooling.test.mjs +2 -2
  14. package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
  15. package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
  16. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
  17. package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
  18. package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
  19. package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
  20. package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
  21. package/tests/agent_board_fedsession.test.sql +46 -2
  22. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
  23. package/tests/agent_board_inbox_reads.test.sql +16 -19
  24. package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
  25. package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
  26. package/tests/agent_board_roomlink.test.sql +2 -2
  27. package/tests/agent_board_wake_recovery.test.sql +756 -0
  28. package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
  29. package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
  30. package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
  31. package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
  32. package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
  33. package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
  34. package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
  35. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
  36. package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
  37. package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
  38. package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
  39. package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
  40. package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
  41. package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
  42. package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
  43. package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
  44. package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
  45. package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
  46. package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
  47. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
  48. package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
  49. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
  50. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
  51. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
  52. package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
  53. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
  54. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
  55. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
  56. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
  57. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
  58. package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
  59. package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
  60. package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
@@ -0,0 +1,604 @@
1
+ -- Consiliency/message-board#186 — make the ceiling-ABSENCE serialization EXPLICIT.
2
+ --
3
+ -- THE REPORTED DEFECT, AND WHY IT IS NOT REACHABLE TODAY. The #186 review held that
4
+ -- 20260901000100 locks the board's ceiling row before evaluating creator authority:
5
+ --
6
+ -- perform 1 from agent_board.board_authority_ceilings c
7
+ -- where c.board_id = v_inv.board_id for update;
8
+ --
9
+ -- and that this lock exists only when the row does. That part is true: absence is
10
+ -- the NORMAL default-owner state (_grant_within_ceiling coalesces a missing row to
11
+ -- rank 4 = owner), and PostgreSQL cannot row-lock a row that is not there. The
12
+ -- conclusion drawn from it — that a concurrent set_board_authority_ceiling
13
+ -- inserting 'observer' can interleave and let a 'member' membership commit under a
14
+ -- ceiling that forbids it — does NOT follow, and was refuted empirically before
15
+ -- this migration was written:
16
+ --
17
+ -- * board_authority_ceilings.board_id has an FK to boards. INSERTing a FIRST
18
+ -- ceiling row therefore takes a FOR KEY SHARE lock on that boards row.
19
+ -- * onboard_federated_fleet_agent already holds that same boards row FOR UPDATE
20
+ -- (`select * into v_board from agent_board.boards b where b.id = v_inv.board_id
21
+ -- for update`), acquired well BEFORE it reads the ceiling.
22
+ -- * FOR UPDATE conflicts with FOR KEY SHARE, so the two transactions already
23
+ -- mutually exclude. Verified with pg_locks: the ceiling INSERT waits on the
24
+ -- onboarding transaction id.
25
+ -- * The row-PRESENT case (ON CONFLICT DO UPDATE, no FK re-check, no parent lock)
26
+ -- is covered by the existing ceiling-row FOR UPDATE.
27
+ --
28
+ -- Removing the advisory lock added below, with everything else intact, changes
29
+ -- NOTHING observable in tests/isolation/specs/atomic_fleet_authority_races.spec —
30
+ -- confirmed by mutation. The advisory lock is therefore NOT the load-bearing
31
+ -- serializer today, and this migration must not be described as closing a live
32
+ -- hole. What is load-bearing is the boards FOR UPDATE, and it is load-bearing only
33
+ -- incidentally: nothing in 20260901000100 says the ceiling decision depends on a
34
+ -- lock taken 150 lines earlier for an unrelated reason (board liveness).
35
+ --
36
+ -- WHAT THIS MIGRATION IS FOR, THEN. It converts an incidental guarantee into a
37
+ -- stated one. A board-keyed pg_advisory_xact_lock, taken by BOTH sides before
38
+ -- either evaluates or writes the ceiling, is the correct instrument precisely
39
+ -- because the serialized thing is the ABSENCE of a row — it does not depend on an
40
+ -- FK's parent-lock side effect surviving a future refactor. Mutation-proven to be
41
+ -- genuinely redundant, not decorative: with the boards FOR UPDATE removed and this
42
+ -- lock retained the isolation spec still passes; with BOTH removed it fails, the
43
+ -- expired replay completing as reused/reused/reused under a committed observer
44
+ -- ceiling. Rejected alternatives:
45
+ -- * a sentinel ceiling row per board — changes default-owner semantics and needs
46
+ -- a backfill over every existing board;
47
+ -- * SERIALIZABLE — a repo-wide isolation posture change, out of scope.
48
+ --
49
+ -- The key derivation lives in ONE function (_lock_board_authority_ceiling) rather
50
+ -- than being written out at each site: two hand-written keys that must be
51
+ -- byte-identical to interlock are a silent-failure class, and a named helper also
52
+ -- lets a catalog assertion enumerate the participants.
53
+ --
54
+ -- LOCK ORDER — ADVISORY FIRST ON BOTH SIDES, and the reason is the FK above.
55
+ -- set_board_authority_ceiling takes the advisory lock, then reads board_memberships
56
+ -- (unlocked), then UPSERTS board_authority_ceilings — and that upsert is NOT a bare
57
+ -- write: on INSERT it takes FOR KEY SHARE on the parent boards row through
58
+ -- board_authority_ceilings_board_id_fkey. Its true order is therefore
59
+ -- advisory -> boards.
60
+ --
61
+ -- onboard_federated_fleet_agent must take the advisory lock in the same relative
62
+ -- position, i.e. BEFORE its `select * into v_board from agent_board.boards ... for
63
+ -- update`. The first cut of this migration did not: it left the advisory
64
+ -- acquisition ~70 lines below the boards lock, giving onboarding boards -> advisory
65
+ -- against the setter's advisory -> boards. That is an ABBA cycle, and it was a
66
+ -- deadlock this migration INTRODUCED — base 20260901000100 has no advisory lock
67
+ -- here and cannot exhibit it. Reproduced deterministically before the fix:
68
+ --
69
+ -- ERROR: deadlock detected
70
+ -- DETAIL: Process A waits for ExclusiveLock on advisory lock [...]; blocked by B.
71
+ -- Process B waits for ShareLock on transaction ...; blocked by A.
72
+ --
73
+ -- The lesson for the next reader: an `insert` into a child table is a lock
74
+ -- acquisition on the parent. Do not reason about lock order from the statements
75
+ -- that name a table explicitly.
76
+ --
77
+ -- KNOWN, DELIBERATE GAP. service_role retains a direct
78
+ -- insert/update grant on board_authority_ceilings (operator provisioning,
79
+ -- 20260717000400). A direct table write bypasses any function-level lock; only the
80
+ -- RPC path is serialized. Closing that would require a table trigger and is a
81
+ -- separate decision.
82
+ --
83
+ -- NOT recreated here (recorded so the class is not re-litigated). Enumerated from
84
+ -- pg_proc, not from memory: exactly four agent_board functions call
85
+ -- _grant_within_ceiling —
86
+ -- agent_board.onboard_federated_fleet_agent(...) <- serialized here
87
+ -- agent_board.grant_board_role(uuid,uuid,text,uuid)
88
+ -- agent_board.create_invite_link(uuid,text,text,integer,timestamptz,uuid,jsonb)
89
+ -- agent_board._redeem_unbounded_invite_for_agent(text,uuid)
90
+ -- The other three are UNCHANGED and are outside #186's scope. Whether they are
91
+ -- exposed to the absence race was NOT determined here: each would need its own
92
+ -- check for an equivalent incidental parent-row lock, and each recreation is a
93
+ -- separate blast radius. Do not read their absence from this migration as evidence
94
+ -- that they are safe. The catalog assertion in
95
+ -- agent_board_atomic_fleet_onboarding.test.sql pins this exact set from pg_proc,
96
+ -- so a NEW reader fails the suite and names itself rather than being silently
97
+ -- admitted to an unexamined class.
98
+
99
+ create or replace function agent_board._lock_board_authority_ceiling(p_board_id uuid)
100
+ returns void
101
+ language sql
102
+ volatile
103
+ as $$
104
+ select pg_advisory_xact_lock(
105
+ hashtextextended('agent_board.board_authority_ceilings:' || p_board_id::text, 0)
106
+ );
107
+ $$;
108
+
109
+ comment on function agent_board._lock_board_authority_ceiling(uuid) is
110
+ 'Board-keyed transaction advisory lock serializing the ABSENCE of a board_authority_ceilings row: every RPC that writes the ceiling and every RPC that reads it for authorization takes this FIRST, before any boards row lock. Held to commit. Internal: it takes a lock with NO authorization check on an arbitrary board id, so it must never be grantable to authenticated.';
111
+ -- NOT grantable to authenticated. This helper performs zero authorization and takes
112
+ -- a transaction-scoped lock on any board id it is handed, so a direct grant is an
113
+ -- onboarding DoS: any authenticated principal, with no membership on the board, can
114
+ -- hold the key and block onboard_federated_fleet_agent for the life of its
115
+ -- transaction. Proven, and proven unnecessary — set_board_authority_ceiling still
116
+ -- works when called AS authenticated, because it is SECURITY DEFINER owned by the
117
+ -- schema owner and the helper resolves against the DEFINER's privileges, not the
118
+ -- caller's.
119
+ revoke all on function agent_board._lock_board_authority_ceiling(uuid) from public, anon, authenticated;
120
+ grant execute on function agent_board._lock_board_authority_ceiling(uuid) to service_role;
121
+
122
+ create or replace function agent_board.set_board_authority_ceiling(
123
+ p_board_id uuid,
124
+ p_max_grantable_role text
125
+ )
126
+ returns table(
127
+ ceiling_board_id uuid,
128
+ ceiling_max_grantable_role text
129
+ )
130
+ language plpgsql
131
+ security definer
132
+ set search_path = agent_board, public
133
+ as $$
134
+ declare
135
+ v_actor uuid := agent_board.current_agent_id();
136
+ begin
137
+ if p_max_grantable_role not in ('owner', 'admin', 'member', 'observer') then
138
+ raise exception 'max_grantable_role must be one of owner/admin/member/observer' using errcode = '22023';
139
+ end if;
140
+
141
+ -- Same board-keyed advisory lock every ceiling READER for authorization takes.
142
+ -- Held to commit, it makes the insert of a first ceiling row serialize against a
143
+ -- concurrent authorization that would otherwise observe the pre-insert ABSENCE and
144
+ -- pass as default-owner. Taken before the owner gate so the gate and the write are
145
+ -- one critical section.
146
+ perform agent_board._lock_board_authority_ceiling(p_board_id);
147
+
148
+ if v_actor is null
149
+ or not exists (
150
+ select 1
151
+ from agent_board.board_memberships bm
152
+ where bm.board_id = p_board_id
153
+ and bm.agent_id = v_actor
154
+ and bm.role = 'owner'
155
+ and bm.membership_state in ('joined', 'subscribed')
156
+ ) then
157
+ raise exception 'only a board owner may set the board authority ceiling' using errcode = '42501';
158
+ end if;
159
+
160
+ insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
161
+ values (p_board_id, p_max_grantable_role)
162
+ on conflict (board_id) do update
163
+ set max_grantable_role = excluded.max_grantable_role,
164
+ updated_at = now();
165
+
166
+ return query
167
+ select p_board_id, p_max_grantable_role;
168
+ end;
169
+ $$;
170
+
171
+ comment on function agent_board.set_board_authority_ceiling(uuid, text) is
172
+ 'Owner-gated setter for a board''s authority ceiling: takes the board-keyed _lock_board_authority_ceiling advisory lock, then upserts board_authority_ceilings only when current_agent_id() is an owner of the board (else raises 42501). Leaves _grant_within_ceiling semantics unchanged.';
173
+
174
+ revoke all on function agent_board.set_board_authority_ceiling(uuid, text) from public, anon;
175
+ grant execute on function agent_board.set_board_authority_ceiling(uuid, text) to authenticated, service_role;
176
+
177
+ create or replace function agent_board.onboard_federated_fleet_agent(
178
+ p_issuer text,
179
+ p_audience text,
180
+ p_subject text,
181
+ p_human_identity_claim jsonb,
182
+ p_slug text,
183
+ p_agent_public_key text,
184
+ p_agent_public_key_format text,
185
+ p_agent_public_key_fingerprint text,
186
+ p_pop_verified boolean,
187
+ p_actor_sub text default null,
188
+ p_agent_kind text default 'agent',
189
+ p_requested_scopes text[] default '{}',
190
+ p_requested_board_scopes text[] default '{}',
191
+ p_invite_code text default null,
192
+ p_board_id uuid default null,
193
+ p_machine_id uuid default null,
194
+ p_harness_kind text default null,
195
+ p_harness_label text default null,
196
+ p_repo_slug text default null,
197
+ p_repo_fingerprint text default null,
198
+ p_repo_scope_role text default null,
199
+ p_agent_role text default null,
200
+ p_expected_agent_handle text default null
201
+ )
202
+ returns table(
203
+ onboarding_contract_version text,
204
+ agent_id uuid,
205
+ auth_user_id uuid,
206
+ provider_id text,
207
+ signing_key_id uuid,
208
+ granted_scopes text[],
209
+ key_id text,
210
+ key_version int,
211
+ agent_handle text,
212
+ public_key_fingerprint text,
213
+ board_id uuid,
214
+ membership_role text,
215
+ membership_state text,
216
+ machine_id uuid,
217
+ harness_registration_id uuid,
218
+ repo_scope_id uuid,
219
+ fleet_machine_agent_id uuid,
220
+ binding_signing_key_id uuid,
221
+ harness_kind text,
222
+ harness_label text,
223
+ repo_slug text,
224
+ repo_fingerprint text,
225
+ repo_scope_role text,
226
+ agent_role text,
227
+ expected_agent_handle text,
228
+ identity_disposition text,
229
+ membership_disposition text,
230
+ harness_disposition text,
231
+ repo_scope_disposition text,
232
+ binding_disposition text
233
+ )
234
+ language plpgsql
235
+ security definer
236
+ set search_path = agent_board, public
237
+ as $$
238
+ #variable_conflict use_column
239
+ declare
240
+ v_inv agent_board.board_invite_tokens%rowtype;
241
+ v_bounds agent_board.fleet_onboarding_invite_bounds%rowtype;
242
+ v_board agent_board.boards%rowtype;
243
+ v_machine agent_board.fleet_machines%rowtype;
244
+ v_domain agent_board.fleet_trust_domains%rowtype;
245
+ v_enroll record;
246
+ v_membership record;
247
+ v_harness agent_board.fleet_harness_registrations%rowtype;
248
+ v_repo agent_board.fleet_repo_scopes%rowtype;
249
+ v_binding agent_board.fleet_machine_agents%rowtype;
250
+ v_kind text := lower(btrim(coalesce(p_harness_kind, '')));
251
+ v_label text := btrim(coalesce(p_harness_label, ''));
252
+ v_repo_slug text := btrim(coalesce(p_repo_slug, ''));
253
+ v_repo_fingerprint text := btrim(coalesce(p_repo_fingerprint, ''));
254
+ v_expected_handle text := btrim(coalesce(p_expected_agent_handle, ''));
255
+ v_existing_agent_id uuid;
256
+ v_owner_user_id uuid;
257
+ v_identity_disposition text;
258
+ v_membership_disposition text;
259
+ v_harness_disposition text;
260
+ v_repo_disposition text;
261
+ v_binding_disposition text;
262
+ v_completed_replay boolean := false;
263
+ begin
264
+ if not agent_board._is_service_role() then
265
+ raise exception 'service_role required for onboard_federated_fleet_agent' using errcode = '42501';
266
+ end if;
267
+ if nullif(btrim(coalesce(p_invite_code, '')), '') is null then
268
+ raise exception 'bounded invite code is required' using errcode = '22023';
269
+ end if;
270
+
271
+ select bit.* into v_inv
272
+ from agent_board.board_invite_tokens bit
273
+ where bit.code = btrim(p_invite_code)
274
+ for update;
275
+ if not found then
276
+ raise exception 'fleet onboarding invite is not usable' using errcode = '42501';
277
+ end if;
278
+ select * into v_bounds
279
+ from agent_board.fleet_onboarding_invite_bounds b
280
+ where b.invite_id = v_inv.id
281
+ for update;
282
+ if not found then
283
+ raise exception 'fleet onboarding requires a bounded invite' using errcode = '42501';
284
+ end if;
285
+
286
+ -- ADVISORY BEFORE BOARDS. set_board_authority_ceiling holds this lock and then
287
+ -- reaches this same boards row through board_authority_ceilings' FK (FOR KEY SHARE
288
+ -- on INSERT). Taking the boards lock first would invert the order against it and
289
+ -- deadlock -- it did, until this line was hoisted above the select below.
290
+ perform agent_board._lock_board_authority_ceiling(v_inv.board_id);
291
+
292
+ select * into v_board
293
+ from agent_board.boards b
294
+ where b.id = v_inv.board_id
295
+ for update;
296
+ if not found or v_board.status <> 'active' then
297
+ raise exception 'fleet onboarding board is not usable' using errcode = '42501';
298
+ end if;
299
+
300
+ if v_inv.board_id is distinct from p_board_id
301
+ or v_bounds.machine_id is distinct from p_machine_id
302
+ or v_bounds.harness_kind is distinct from v_kind
303
+ or v_bounds.harness_label is distinct from v_label
304
+ or v_bounds.repo_slug is distinct from v_repo_slug
305
+ or v_bounds.repo_fingerprint is distinct from v_repo_fingerprint
306
+ or v_bounds.repo_scope_role is distinct from p_repo_scope_role
307
+ or v_bounds.agent_role is distinct from p_agent_role
308
+ or v_bounds.expected_agent_handle is distinct from v_expected_handle then
309
+ raise exception 'fleet onboarding bounds mismatch' using errcode = '42501';
310
+ end if;
311
+ if not ((p_agent_role = 'phase_worker' and p_repo_scope_role = 'worker')
312
+ or (p_agent_role = 'reviewer' and p_repo_scope_role = 'reviewer')) then
313
+ raise exception 'fleet onboarding authority is elevated' using errcode = '42501';
314
+ end if;
315
+
316
+ select * into v_machine from agent_board.fleet_machines fm
317
+ where fm.id = p_machine_id for update;
318
+ if not found or v_machine.owner_agent_id <> v_inv.created_by_agent_id
319
+ or v_machine.lifecycle_state <> 'active' then
320
+ raise exception 'fleet onboarding machine is not usable' using errcode = '42501';
321
+ end if;
322
+ select * into v_domain from agent_board.fleet_trust_domains td
323
+ where td.id = v_machine.trust_domain_id for update;
324
+ if not found or v_domain.owner_agent_id <> v_inv.created_by_agent_id
325
+ or v_domain.lifecycle_state <> 'active' then
326
+ raise exception 'fleet onboarding trust domain is not usable' using errcode = '42501';
327
+ end if;
328
+
329
+ -- Serialize cross-route identity classification before locking principals. For
330
+ -- a replay, lock the creator and enrolled principal in UUID order so a human or
331
+ -- agent subtree revocation that visits the same pair cannot invert their order.
332
+ perform pg_advisory_xact_lock(hashtextextended(p_agent_public_key_fingerprint, 0));
333
+ select k.agent_id, a.owner_user_id into v_existing_agent_id, v_owner_user_id
334
+ from agent_board.agent_signing_keys k
335
+ join agent_board.agents a on a.id = k.agent_id
336
+ where k.fingerprint = p_agent_public_key_fingerprint;
337
+ v_owner_user_id := coalesce(v_owner_user_id, agent_board._ensure_agent_auth_user(
338
+ 'fed-human-' || substr(encode(extensions.digest(convert_to(p_issuer || '|' || p_subject, 'UTF8'), 'sha256'), 'hex'), 1, 32)
339
+ ));
340
+ perform 1 from auth.users u where u.id = v_owner_user_id for update;
341
+ if exists (
342
+ select 1 from agent_board.fleet_human_revocations r
343
+ where r.issuer = p_issuer and r.subject = p_subject
344
+ ) then
345
+ raise exception 'human owner has been revoked' using errcode = '42501';
346
+ end if;
347
+ perform 1 from agent_board.agents a
348
+ where a.id = any(array[v_inv.created_by_agent_id, v_existing_agent_id])
349
+ order by a.id for update;
350
+
351
+ -- Creator authority is part of the invite's live authorization, not merely a
352
+ -- snapshot from mint time. Lock every membership row and the current ceiling
353
+ -- before evaluating its role so revocation, eviction, or demotion must serialize
354
+ -- on one side of this transaction.
355
+ --
356
+ -- The ceiling ROW lock below covers demotion of an EXISTING ceiling only. No row
357
+ -- lock can cover the ABSENCE of a row, and absence is the normal default-owner
358
+ -- state, so a concurrent set_board_authority_ceiling inserting 'observer' used to
359
+ -- interleave: this transaction read "no ceiling", passed as owner, and committed a
360
+ -- 'member' membership after the ceiling committed. The board-keyed advisory lock
361
+ -- that serializes the ABSENCE is already held -- it is taken far above, BEFORE the
362
+ -- boards row lock, because the ceiling writer reaches boards through its FK and the
363
+ -- two must agree on order.
364
+ perform 1 from agent_board.board_memberships bm
365
+ where bm.board_id = v_inv.board_id
366
+ and bm.agent_id = v_inv.created_by_agent_id
367
+ order by bm.id for update;
368
+ perform 1 from agent_board.board_authority_ceilings c
369
+ where c.board_id = v_inv.board_id for update;
370
+
371
+ -- Concurrent legacy enrollment or onboarding on another machine has committed
372
+ -- before this snapshot decides whether the identity was created here.
373
+ if exists (select 1 from agent_board.agent_signing_keys k where k.fingerprint = p_agent_public_key_fingerprint) then
374
+ v_identity_disposition := 'reused';
375
+ else
376
+ v_identity_disposition := 'created';
377
+ end if;
378
+
379
+ select * into v_enroll from agent_board.enroll_federated_identity(
380
+ p_issuer, p_audience, p_subject, p_human_identity_claim, p_slug,
381
+ p_agent_public_key, p_agent_public_key_format, p_agent_public_key_fingerprint,
382
+ p_pop_verified, p_actor_sub, p_agent_kind, p_requested_scopes,
383
+ p_requested_board_scopes, null
384
+ );
385
+ if v_enroll.agent_handle is distinct from v_expected_handle then
386
+ raise exception 'server-assigned handle does not match the authorized handle' using errcode = '23505';
387
+ end if;
388
+
389
+ -- A complete replay may bypass expiry/exhaustion only while its redemption and
390
+ -- active membership remain true through commit. Lock both ledgers before the
391
+ -- proof; the invite row already serializes first redemption for an absent row.
392
+ perform 1 from agent_board.board_invite_redemptions red
393
+ where red.invite_id = v_inv.id and red.agent_id = v_enroll.agent_id
394
+ order by red.id for update;
395
+ perform 1 from agent_board.board_memberships bm
396
+ where bm.board_id = v_inv.board_id and bm.agent_id = v_enroll.agent_id
397
+ order by bm.id for update;
398
+
399
+ -- The released ROOMLINK core deliberately returns an existing redemption before
400
+ -- checking current token authority. Fleet onboarding has a narrower replay
401
+ -- contract: only an exact, fully active chain may ignore expiry/exhaustion. A
402
+ -- partial chain must pass every current invite gate before any missing suffix is
403
+ -- written, and revocation/current creator authority always apply.
404
+ select exists (
405
+ select 1
406
+ from agent_board.board_invite_redemptions red
407
+ join agent_board.board_memberships mem on mem.id = red.membership_id
408
+ join agent_board.fleet_harness_registrations h
409
+ on h.machine_id = p_machine_id
410
+ and h.harness_kind = v_kind
411
+ and h.harness_label = v_label
412
+ join agent_board.fleet_repo_scopes repo
413
+ on repo.harness_registration_id = h.id
414
+ and repo.repo_slug = v_repo_slug
415
+ and repo.repo_fingerprint = v_repo_fingerprint
416
+ and repo.scope_role = p_repo_scope_role
417
+ join agent_board.fleet_machine_agents binding
418
+ on binding.machine_id = p_machine_id
419
+ and binding.agent_id = v_enroll.agent_id
420
+ and binding.repo_scope_id = repo.id
421
+ and binding.signing_key_id = v_enroll.signing_key_id
422
+ and binding.agent_role = p_agent_role
423
+ join agent_board.agents principal on principal.id = v_enroll.agent_id
424
+ join agent_board.agent_signing_keys signing_key on signing_key.id = v_enroll.signing_key_id
425
+ where red.invite_id = v_inv.id
426
+ and red.agent_id = v_enroll.agent_id
427
+ and mem.board_id = v_inv.board_id
428
+ and mem.agent_id = v_enroll.agent_id
429
+ and mem.role = 'member'
430
+ and mem.membership_state in ('joined', 'subscribed')
431
+ and h.lifecycle_state = 'active'
432
+ and repo.lifecycle_state = 'active'
433
+ and binding.lifecycle_state = 'active'
434
+ and principal.status = 'active'
435
+ and signing_key.status = 'active'
436
+ and signing_key.proof_state = 'proven'
437
+ and signing_key.revoked_at is null
438
+ and not exists (
439
+ select 1 from agent_board.fleet_revocation_events e
440
+ where (e.subject_kind = 'agent' and e.subject_id = v_enroll.agent_id)
441
+ or (e.subject_kind = 'signing_key' and e.subject_id = v_enroll.signing_key_id)
442
+ or (e.subject_kind = 'harness' and e.subject_id = h.id)
443
+ or (e.subject_kind = 'repo_scope' and e.subject_id = repo.id)
444
+ )
445
+ ) into v_completed_replay;
446
+
447
+ if v_inv.revoked_at is not null
448
+ or not exists (
449
+ select 1 from agent_board.agents a
450
+ where a.id = v_inv.created_by_agent_id and a.status = 'active'
451
+ )
452
+ or not agent_board._grant_within_ceiling(
453
+ v_inv.created_by_agent_id, v_inv.board_id, v_inv.preset_role
454
+ )
455
+ or (
456
+ exists (
457
+ select 1 from agent_board.board_memberships bm
458
+ where bm.board_id = v_inv.board_id
459
+ and bm.agent_id = v_enroll.agent_id
460
+ and bm.membership_state = 'removed'
461
+ )
462
+ and not exists (
463
+ select 1 from agent_board.board_memberships bm
464
+ where bm.board_id = v_inv.board_id
465
+ and bm.agent_id = v_enroll.agent_id
466
+ and bm.membership_state in ('joined', 'subscribed')
467
+ )
468
+ ) then
469
+ raise exception 'fleet onboarding invite is not usable' using errcode = '42501';
470
+ end if;
471
+ if not v_completed_replay
472
+ and (
473
+ v_inv.expires_at <= now()
474
+ or (v_inv.max_uses is not null and v_inv.uses_count >= v_inv.max_uses)
475
+ ) then
476
+ raise exception 'fleet onboarding invite is not usable' using errcode = '42501';
477
+ end if;
478
+
479
+ if exists (
480
+ select 1 from agent_board.board_invite_redemptions r
481
+ where r.invite_id = v_inv.id and r.agent_id = v_enroll.agent_id
482
+ ) then v_membership_disposition := 'reused'; else v_membership_disposition := 'created'; end if;
483
+
484
+ select * into v_membership
485
+ from agent_board._redeem_fleet_invite_for_agent(btrim(p_invite_code), v_enroll.agent_id);
486
+ if v_membership.role <> 'member' or v_membership.membership_state not in ('joined', 'subscribed') then
487
+ raise exception 'fleet onboarding requires an active member membership' using errcode = '42501';
488
+ end if;
489
+
490
+ perform 1 from agent_board.fleet_harness_registrations h
491
+ where h.machine_id = p_machine_id and h.harness_kind = v_kind and h.harness_label = v_label
492
+ order by h.id for update;
493
+ if exists (
494
+ select 1 from agent_board.fleet_harness_registrations h
495
+ where h.machine_id = p_machine_id and h.harness_kind = v_kind and h.harness_label = v_label
496
+ and h.lifecycle_state <> 'active'
497
+ ) then
498
+ raise exception 'revoked harness registration cannot be replaced' using errcode = '42501';
499
+ end if;
500
+ select * into v_harness
501
+ from agent_board.fleet_harness_registrations h
502
+ where h.machine_id = p_machine_id and h.harness_kind = v_kind and h.harness_label = v_label
503
+ and h.lifecycle_state = 'active'
504
+ order by h.id;
505
+ if found then
506
+ v_harness_disposition := 'reused';
507
+ else
508
+ insert into agent_board.fleet_harness_registrations(machine_id, harness_kind, harness_label)
509
+ values (p_machine_id, v_kind, v_label) returning * into v_harness;
510
+ v_harness_disposition := case when v_identity_disposition = 'reused' then 'recovered' else 'created' end;
511
+ end if;
512
+
513
+ perform 1 from agent_board.fleet_repo_scopes r
514
+ where r.harness_registration_id = v_harness.id
515
+ and r.repo_fingerprint = v_repo_fingerprint and r.scope_role = p_repo_scope_role
516
+ order by r.id for update;
517
+ if exists (
518
+ select 1 from agent_board.fleet_repo_scopes r
519
+ where r.harness_registration_id = v_harness.id
520
+ and r.repo_fingerprint = v_repo_fingerprint and r.scope_role = p_repo_scope_role
521
+ and (r.lifecycle_state <> 'active' or r.repo_slug <> v_repo_slug)
522
+ ) then
523
+ raise exception 'conflicting or revoked repo scope cannot be replaced' using errcode = '42501';
524
+ end if;
525
+ select * into v_repo
526
+ from agent_board.fleet_repo_scopes r
527
+ where r.harness_registration_id = v_harness.id
528
+ and r.repo_fingerprint = v_repo_fingerprint and r.scope_role = p_repo_scope_role
529
+ and r.lifecycle_state = 'active' and r.repo_slug = v_repo_slug
530
+ order by r.id;
531
+ if found then
532
+ v_repo_disposition := 'reused';
533
+ else
534
+ insert into agent_board.fleet_repo_scopes(
535
+ harness_registration_id, repo_slug, repo_fingerprint, scope_role
536
+ ) values (v_harness.id, v_repo_slug, v_repo_fingerprint, p_repo_scope_role)
537
+ returning * into v_repo;
538
+ v_repo_disposition := case when v_identity_disposition = 'reused' then 'recovered' else 'created' end;
539
+ end if;
540
+
541
+ -- Machine -> agent/key/binding lock order. Any tombstone for this natural key
542
+ -- blocks replacement authority even though the active unique index would allow it.
543
+ perform 1 from agent_board.agents a where a.id = v_enroll.agent_id for update;
544
+ perform 1 from agent_board.agent_signing_keys k where k.id = v_enroll.signing_key_id for update;
545
+ perform 1 from agent_board.fleet_machine_agents b
546
+ where b.machine_id = p_machine_id and b.agent_id = v_enroll.agent_id
547
+ and b.repo_scope_id = v_repo.id
548
+ order by b.id for update;
549
+ if exists (
550
+ select 1 from agent_board.fleet_machine_agents b
551
+ where b.machine_id = p_machine_id and b.agent_id = v_enroll.agent_id
552
+ and b.repo_scope_id = v_repo.id
553
+ and (
554
+ b.lifecycle_state <> 'active'
555
+ or b.signing_key_id is distinct from v_enroll.signing_key_id
556
+ or b.agent_role is distinct from p_agent_role
557
+ )
558
+ ) then
559
+ raise exception 'conflicting or revoked fleet binding cannot be replaced' using errcode = '42501';
560
+ end if;
561
+ select * into v_binding
562
+ from agent_board.fleet_machine_agents b
563
+ where b.machine_id = p_machine_id and b.agent_id = v_enroll.agent_id
564
+ and b.repo_scope_id = v_repo.id and b.lifecycle_state = 'active'
565
+ and b.signing_key_id = v_enroll.signing_key_id and b.agent_role = p_agent_role
566
+ order by b.id;
567
+ if found then
568
+ v_binding_disposition := 'reused';
569
+ else
570
+ insert into agent_board.fleet_machine_agents(
571
+ machine_id, agent_id, repo_scope_id, signing_key_id, agent_role
572
+ ) values (
573
+ p_machine_id, v_enroll.agent_id, v_repo.id, v_enroll.signing_key_id, p_agent_role
574
+ ) returning * into v_binding;
575
+ v_binding_disposition := case when v_identity_disposition = 'reused' then 'recovered' else 'created' end;
576
+ end if;
577
+
578
+ if exists (
579
+ select 1 from agent_board.fleet_revocation_events e
580
+ where (e.subject_kind = 'agent' and e.subject_id = v_enroll.agent_id)
581
+ or (e.subject_kind = 'signing_key' and e.subject_id = v_enroll.signing_key_id)
582
+ or (e.subject_kind = 'harness' and e.subject_id = v_harness.id)
583
+ or (e.subject_kind = 'repo_scope' and e.subject_id = v_repo.id)
584
+ ) then
585
+ raise exception 'fleet onboarding authority has effective revocation evidence' using errcode = '42501';
586
+ end if;
587
+
588
+ return query select
589
+ 'fleet_onboarding.v1'::text, v_enroll.agent_id, v_enroll.auth_user_id,
590
+ v_enroll.provider_id, v_enroll.signing_key_id, v_enroll.granted_scopes,
591
+ v_enroll.key_id, v_enroll.key_version, v_enroll.agent_handle,
592
+ p_agent_public_key_fingerprint, v_membership.board_id, v_membership.role,
593
+ v_membership.membership_state, p_machine_id, v_harness.id, v_repo.id,
594
+ v_binding.id, v_binding.signing_key_id, v_kind, v_label, v_repo_slug, v_repo_fingerprint,
595
+ p_repo_scope_role, p_agent_role, v_expected_handle,
596
+ v_identity_disposition, v_membership_disposition, v_harness_disposition,
597
+ v_repo_disposition, v_binding_disposition;
598
+ end;
599
+ $$;
600
+
601
+ comment on function agent_board.onboard_federated_fleet_agent(text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[], text, uuid, uuid, text, text, text, text, text, text, text) is
602
+ 'Service-role-only atomic fleet onboarding. Locks invite, machine, domain, principal, key, and binding in canonical order; takes the board-keyed _lock_board_authority_ceiling advisory lock so the ceiling''s ABSENCE serializes against a concurrent set_board_authority_ceiling before creator authority is evaluated; validates immutable creator bounds; enrolls/replays identity; redeems member ROOMLINK; and creates/reuses the exact harness, repo, and key-bound machine-agent chain in one transaction.';
603
+ revoke all on function agent_board.onboard_federated_fleet_agent(text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[], text, uuid, uuid, text, text, text, text, text, text, text) from public, anon, authenticated;
604
+ grant execute on function agent_board.onboard_federated_fleet_agent(text, text, text, jsonb, text, text, text, text, boolean, text, text, text[], text[], text, uuid, uuid, text, text, text, text, text, text, text) to service_role;
@@ -0,0 +1,78 @@
1
+ -- Consiliency/message-board#162 follow-up: total-human revocation must have stable
2
+ -- evidence for EVERY federation-linked principal before it mutates the subtree.
3
+ -- Requiring only one evidenced principal for the owner lets a mixed owner pass the
4
+ -- gate while an unaudited sibling receives no durable issuer/subject marker.
5
+
6
+ create or replace function agent_board.revoke_human_subtree(
7
+ p_owner_user_id uuid,
8
+ p_revoked_by_agent_id uuid,
9
+ p_reason text default 'human revocation'
10
+ )
11
+ returns table(revoked_agent_count integer, revoked_key_count integer)
12
+ language plpgsql
13
+ security definer
14
+ set search_path = agent_board, public
15
+ as $$
16
+ declare
17
+ v_agent uuid;
18
+ v_agent_count integer := 0;
19
+ v_key_count integer := 0;
20
+ begin
21
+ if not agent_board._is_service_role() then
22
+ raise exception 'revoke_human_subtree is service_role-only' using errcode = '42501';
23
+ end if;
24
+
25
+ perform 1 from auth.users u where u.id = p_owner_user_id for update;
26
+ if not found then
27
+ raise exception 'human owner not found' using errcode = 'P0002';
28
+ end if;
29
+
30
+ if exists (
31
+ select 1
32
+ from agent_board.agents a
33
+ where a.owner_user_id = p_owner_user_id
34
+ and jsonb_typeof(a.metadata -> 'federation') = 'object'
35
+ and not exists (
36
+ select 1
37
+ from agent_board.enrollment_federation_audit e
38
+ where e.agent_id = a.id
39
+ and nullif(btrim(e.issuer), '') is not null
40
+ and nullif(btrim(e.subject), '') is not null
41
+ )
42
+ ) then
43
+ raise exception 'stable federation evidence not found for every linked agent' using errcode = 'P0002';
44
+ end if;
45
+
46
+ insert into agent_board.fleet_human_revocations(
47
+ issuer, subject, owner_user_id, revoked_by_agent_id, revocation_reason
48
+ )
49
+ select distinct
50
+ e.issuer,
51
+ e.subject,
52
+ p_owner_user_id, p_revoked_by_agent_id, p_reason
53
+ from agent_board.enrollment_federation_audit e
54
+ join agent_board.agents a on a.id = e.agent_id
55
+ where a.owner_user_id = p_owner_user_id
56
+ and nullif(btrim(e.issuer), '') is not null
57
+ and nullif(btrim(e.subject), '') is not null
58
+ on conflict (issuer, subject) do nothing;
59
+
60
+ for v_agent in
61
+ select id
62
+ from agent_board.agents
63
+ where owner_user_id = p_owner_user_id
64
+ order by id
65
+ loop
66
+ v_key_count := v_key_count
67
+ + agent_board._revoke_agent_principal(v_agent, p_revoked_by_agent_id, p_reason);
68
+ v_agent_count := v_agent_count + 1;
69
+ end loop;
70
+
71
+ return query select v_agent_count, v_key_count;
72
+ end;
73
+ $$;
74
+
75
+ comment on function agent_board.revoke_human_subtree(uuid, uuid, text) is
76
+ 'TOTAL human revocation. Locks the human owner; requires append-only stable federation evidence for every federation-linked owned agent before any mutation; derives durable issuer/subject markers only from that audit evidence; then visits every owned agent in UUID order before the agent-before-key primitive. service_role-only.';
77
+ revoke all on function agent_board.revoke_human_subtree(uuid, uuid, text) from public, anon, authenticated;
78
+ grant execute on function agent_board.revoke_human_subtree(uuid, uuid, text) to service_role;