@consiliency/agent-board-schema 1.12.0 → 1.22.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 (104) hide show
  1. package/fixtures/already-applied.json +124 -0
  2. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  3. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  4. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  5. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  6. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  7. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  8. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  9. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  10. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  11. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  12. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  13. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  14. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  15. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  16. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  17. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  18. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  19. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  20. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  21. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  22. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  23. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  24. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  25. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  26. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  27. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  28. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  29. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  30. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  31. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  32. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  33. package/package.json +1 -1
  34. package/schema-manifest.json +282 -3
  35. package/src/tooling.test.mjs +113 -2
  36. package/tests/agent_board_actorspoof.test.sql +349 -0
  37. package/tests/agent_board_agentsdir.test.sql +154 -0
  38. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  39. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  40. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  41. package/tests/agent_board_claimkey.test.sql +229 -0
  42. package/tests/agent_board_consent_backfill.test.sql +48 -0
  43. package/tests/agent_board_consent_emission.test.sql +601 -0
  44. package/tests/agent_board_consent_gating.test.sql +489 -0
  45. package/tests/agent_board_consent_substrate.test.sql +720 -0
  46. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  47. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  48. package/tests/agent_board_delegation_policies.test.sql +107 -1
  49. package/tests/agent_board_fedsession.test.sql +6 -4
  50. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  51. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  52. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  53. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  54. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  55. package/tests/agent_board_fleet_policy.test.sql +2 -2
  56. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  57. package/tests/agent_board_identity_providers.test.sql +9 -0
  58. package/tests/agent_board_inbox_reads.test.sql +29 -12
  59. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  60. package/tests/agent_board_invitepolicy.test.sql +205 -0
  61. package/tests/agent_board_leaveidem.test.sql +194 -0
  62. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  63. package/tests/agent_board_mint_harden.test.sql +303 -0
  64. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  65. package/tests/agent_board_obsgate.test.sql +178 -0
  66. package/tests/agent_board_obsgate2.test.sql +153 -0
  67. package/tests/agent_board_obsgate3.test.sql +170 -0
  68. package/tests/agent_board_obsgate4.test.sql +196 -0
  69. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  70. package/tests/agent_board_pubexec_class.test.sql +53 -0
  71. package/tests/agent_board_rejectsem.test.sql +271 -0
  72. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  73. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  74. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  75. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  76. package/tests/agent_board_rolereset.test.sql +351 -0
  77. package/tests/agent_board_roomdeleg.test.sql +5 -3
  78. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  79. package/tests/agent_board_roomlink.test.sql +25 -9
  80. package/tests/agent_board_rowid.test.sql +1274 -0
  81. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  82. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  83. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  84. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  85. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  86. package/tests/agent_board_signing_store.test.sql +6 -6
  87. package/tests/fixtures/isolation-substrate.sql +93 -0
  88. package/tests/fixtures/test-prereqs.sql +33 -0
  89. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  90. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  91. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  92. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  93. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  94. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  95. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  96. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  97. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  98. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  99. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  100. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  101. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  102. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  103. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  104. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -0,0 +1,660 @@
1
+ -- CLAIMGATE Phase 3 — ENFORCE (#133): per-kind GATED fleet-authority DENIAL + renew re-auth
2
+ -- + max-lease horizon. Ships the full enforcement machinery, but every fleet-backed kind's
3
+ -- DENIAL is behind a per-kind flag that DEFAULTS OFF — nothing denies at merge.
4
+ --
5
+ -- =====================================================================================
6
+ -- ROLLOUT — GATED PER-KIND, DEFAULT OFF (maintainer decision 2026-07-31, locked)
7
+ -- =====================================================================================
8
+ -- Reuses the SHADOW claimgate_enforcement_flags table (20260824000100:118) with per-kind
9
+ -- scope_keys 'enforce_machine' / 'enforce_repository' / 'enforce_repo_file', resolved by
10
+ -- _claimgate_kind_enforced(kind) which COALESCES to FALSE when the row is absent (mirrors the
11
+ -- send-authz config-flag pattern 20260718000100 — but note that pattern DEFAULTS FAIL-OPEN;
12
+ -- here absent => not-enforced => SHADOW behavior, i.e. shadow-stamp only). When a kind's flag
13
+ -- is OFF: acquire/renew still just SHADOW-stamp (current behavior, blocks nothing). When ON:
14
+ -- DENY (42501) an unauthorized claim/renew of that kind. Machine first (smallest population,
15
+ -- cleanest identity), then repository/repo_file once each shadow census is clean.
16
+ --
17
+ -- =====================================================================================
18
+ -- MAX-LEASE HORIZON — 30d DEFAULT, operator-configurable; CLAMP (not reject)
19
+ -- =====================================================================================
20
+ -- "Bounded by one lease term" is meaningless without a maximum (a gated kind could meet the
21
+ -- acquire TTL and then renew for years). ENFORCE imposes a frozen maximum horizon on acquire
22
+ -- AND every renew for the fleet-gated kinds (machine/repository/repo_file), ONLY when that
23
+ -- kind's enforcement flag is ON. Stored as _claimgate_max_lease_horizon() reading an optional
24
+ -- claimgate_settings('max_lease_horizon') override row, defaulting 30d (#118 discipline: a
25
+ -- single-source resolver, not a literal sprinkled through the logic).
26
+ --
27
+ -- CLAMP, not reject: the horizon CLAMPS an over-long lease to now()+horizon rather than raising.
28
+ -- This is FORCED by the reconciliation strategy — existing overlong active leases are NOT
29
+ -- bulk-migrated (they are gated OFF until promotion); they reconcile at their NEXT renew, which
30
+ -- only works if renew CLAMPS (a reject would leave the old long expiry in place and just error).
31
+ -- Acquire clamps too, for consistency (a fresh 60d request lands the 30d bound). service_role
32
+ -- is exempt (operator carve-out, consistent with the SHADOW ownerless cap).
33
+ -- NOTE (reviewer-visible divergence): the SHADOW ownerless TTL cap (20260824000100:369)
34
+ -- REJECTS an over-long ownerless lease (22023); this fleet horizon CLAMPS. Different mechanism,
35
+ -- different intent (ownerless quota-squat prevention vs. fleet lease-term bound) — deliberate.
36
+ --
37
+ -- =====================================================================================
38
+ -- ACTOR-PARAMETERIZED PREDICATE (shadow evidence must cover EXISTING claims)
39
+ -- =====================================================================================
40
+ -- SHADOW's session-derived _can_claim_resource(kind,key) can only evaluate the CURRENT actor,
41
+ -- so it cannot census a claim already held by some OTHER agent. ENFORCE splits it:
42
+ -- * _can_claim_resource_for(p_actor, kind, key) — pure authority for an ARBITRARY actor
43
+ -- (NO service_role carve-out — that is a property of the CALLER, not the subject).
44
+ -- * _can_claim_resource(kind, key) — the frozen IF-0-SHADOW-1 wrapper, unchanged
45
+ -- in behavior: service_role short-circuits true, else delegates to _for(current_agent_id()).
46
+ -- Behavior of the frozen signature is IDENTICAL (the 36-assertion predicate suite regression-pins
47
+ -- the refactor). The operator censuses existing rows with _can_claim_resource_for(claim.agent_id,…).
48
+ --
49
+ -- FAIL-CLOSED ON UNKNOWN: automatic. An unknown/unresolvable fleet resource (fingerprint not in
50
+ -- the graph) makes the predicate return FALSE, so an enforced acquire of it DENIES — no special arm.
51
+ --
52
+ -- Renew re-auth CONSULTS THE SAME per-kind flag acquire does (a mutation splitting the flags fails
53
+ -- a test). Task-kind renew AND all release_claim stay OWNERSHIP-ONLY — this SUPERSEDES
54
+ -- 20260817000100:27-28 (maintainer-ratified 2026-07-31); only the 3 predicate-gated kinds re-auth.
55
+ --
56
+ -- Depends on the register_fleet_machine cross-owner-hijack fix (#141 / 20260629000101 hardening,
57
+ -- merged as #145) — fingerprint-ownership must not be stealable once it equals lock authority.
58
+ --
59
+ -- #118 class (CLAUDE.md schema-mechanics): every NEWLY-DEFINED SECURITY DEFINER function below
60
+ -- restates revoke/grant/comment; the new claimgate_settings table restates grant/RLS/policy/
61
+ -- comment (mirroring claimgate_enforcement_flags). acquire_claim / renew_claim / _can_claim_resource
62
+ -- are create-or-replace of existing functions (preserve prior ACL/comment) — they restate only
63
+ -- revoke+grant. Additive, forward-only. schema_version stays 8.
64
+
65
+ -- 0. Per-kind enforcement resolver — reuse claimgate_enforcement_flags, DEFAULT OFF. ---------
66
+ create or replace function agent_board._claimgate_kind_enforced(p_resource_kind text)
67
+ returns boolean
68
+ language sql
69
+ stable
70
+ security definer
71
+ set search_path = ''
72
+ as $$
73
+ select coalesce(
74
+ (select f.enabled
75
+ from agent_board.claimgate_enforcement_flags f
76
+ where f.scope_key = case p_resource_kind
77
+ when 'machine' then 'enforce_machine'
78
+ when 'repository' then 'enforce_repository'
79
+ when 'repo_file' then 'enforce_repo_file'
80
+ else null
81
+ end),
82
+ false)
83
+ $$;
84
+ revoke all on function agent_board._claimgate_kind_enforced(text) from public, anon;
85
+ grant execute on function agent_board._claimgate_kind_enforced(text) to authenticated, service_role;
86
+ comment on function agent_board._claimgate_kind_enforced(text) is
87
+ 'CLAIMGATE/ENFORCE (#133): is DENIAL enforced for this fleet-backed kind? Reads '
88
+ 'claimgate_enforcement_flags scope_key enforce_machine/enforce_repository/enforce_repo_file; '
89
+ 'DEFAULTS FALSE (absent row) so a kind stays SHADOW (stamp-only) until an operator flips it. '
90
+ 'Non-fleet kinds (task/file/tool/custom) always resolve FALSE (no scope_key). SECURITY DEFINER; '
91
+ 'PUBLIC EXECUTE revoked. acquire AND renew consult THIS resolver (one flag per kind).';
92
+
93
+ -- 1. Max-lease horizon setting + resolver (operator-configurable, default 30d). --------------
94
+ create table if not exists agent_board.claimgate_settings (
95
+ key text primary key,
96
+ value text not null,
97
+ updated_at timestamptz not null default now(),
98
+ -- Validate the max_lease_horizon override at WRITE time, not in the hot path: a malformed or
99
+ -- non-positive value must fail the operator's INSERT/UPDATE rather than raise (22007) or clamp
100
+ -- into the past on EVERY enforced fleet acquire/renew where _claimgate_max_lease_horizon() casts
101
+ -- it (panel should-fix). CASE guards the cast so it only runs for the horizon key (a non-horizon
102
+ -- setting may hold arbitrary text). A malformed literal raises on the cast here; a <= 0 interval
103
+ -- fails the CHECK — both at write time.
104
+ constraint claimgate_settings_max_lease_horizon_valid check (
105
+ case when key = 'max_lease_horizon' then (value::interval) > interval '0' else true end
106
+ )
107
+ );
108
+ comment on table agent_board.claimgate_settings is
109
+ 'CLAIMGATE operator-configurable settings (key/value text). key ''max_lease_horizon'' overrides '
110
+ 'the fleet max-lease horizon (an interval literal, e.g. ''14 days''); absent => 30d default. '
111
+ 'Operator-managed (service_role); read through _claimgate_max_lease_horizon().';
112
+ alter table agent_board.claimgate_settings enable row level security;
113
+ grant select, insert, update, delete on agent_board.claimgate_settings to service_role;
114
+ drop policy if exists agent_board_service_all_claimgate_settings on agent_board.claimgate_settings;
115
+ create policy agent_board_service_all_claimgate_settings
116
+ on agent_board.claimgate_settings
117
+ as permissive for all to service_role using (true) with check (true);
118
+
119
+ create or replace function agent_board._claimgate_max_lease_horizon()
120
+ returns interval
121
+ language sql
122
+ stable
123
+ security definer
124
+ set search_path = ''
125
+ as $$
126
+ select coalesce(
127
+ (select s.value::interval
128
+ from agent_board.claimgate_settings s
129
+ where s.key = 'max_lease_horizon'),
130
+ interval '30 days')
131
+ $$;
132
+ revoke all on function agent_board._claimgate_max_lease_horizon() from public, anon;
133
+ grant execute on function agent_board._claimgate_max_lease_horizon() to authenticated, service_role;
134
+ comment on function agent_board._claimgate_max_lease_horizon() is
135
+ 'CLAIMGATE/ENFORCE (#133): max lease horizon for an ENFORCED fleet-backed (machine/repository/'
136
+ 'repo_file) claim, applied on acquire AND every renew (CLAMPS an over-long lease, does not reject). '
137
+ 'Reads claimgate_settings(''max_lease_horizon'') override; DEFAULTS 30d. SECURITY DEFINER (reads a '
138
+ 'service_role-only table); PUBLIC EXECUTE revoked.';
139
+
140
+ -- 2. _can_claim_resource_for — the ACTOR-PARAMETERIZED authority twin. ----------------------
141
+ -- Body reproduced from _can_claim_resource (20260824000100) EXCEPT: (a) authority is computed
142
+ -- for the PASSED p_actor, not current_agent_id(); (b) NO service_role short-circuit (that is a
143
+ -- caller property, kept in the wrapper below). Lets the operator census EXISTING held claims
144
+ -- for an arbitrary owner. SECURITY DEFINER to break fleet-table RLS recursion (#118).
145
+ create or replace function agent_board._can_claim_resource_for(
146
+ p_actor uuid,
147
+ p_resource_kind text,
148
+ p_resource_key text
149
+ )
150
+ returns boolean
151
+ language plpgsql
152
+ stable
153
+ security definer
154
+ set search_path = 'agent_board', 'public'
155
+ as $function$
156
+ declare
157
+ v_fp text;
158
+ v_sep int;
159
+ begin
160
+ -- The ownerless tail + task are not fleet-gated, so they remain authorized even for a null
161
+ -- actor (governed elsewhere: task gate in acquire_claim; ownerless TTL/quota in acquire_claim).
162
+ if p_resource_kind = 'task' then
163
+ return true;
164
+ elsif p_resource_kind in ('file', 'tool', 'custom') then
165
+ return true;
166
+ end if;
167
+
168
+ -- A null principal is honestly UNAUTHORIZED on any fleet-backed kind (fail-closed-on-unknown).
169
+ if p_actor is null then
170
+ return false;
171
+ end if;
172
+
173
+ if p_resource_kind = 'machine' then
174
+ v_fp := btrim(p_resource_key);
175
+ if v_fp is null or v_fp = '' then
176
+ return false;
177
+ end if;
178
+ -- machine_fingerprint is globally unique; the machine AND its trust domain must be active,
179
+ -- and the actor must own it OR hold an active binding on it.
180
+ return exists (
181
+ select 1
182
+ from agent_board.fleet_machines fm
183
+ join agent_board.fleet_trust_domains ftd
184
+ on ftd.id = fm.trust_domain_id
185
+ where fm.machine_fingerprint = v_fp
186
+ and fm.lifecycle_state = 'active'
187
+ and ftd.lifecycle_state = 'active'
188
+ and (
189
+ fm.owner_agent_id = p_actor
190
+ or exists (
191
+ select 1
192
+ from agent_board.fleet_machine_agents fma
193
+ where fma.machine_id = fm.id
194
+ and fma.agent_id = p_actor
195
+ and fma.lifecycle_state = 'active'
196
+ )
197
+ )
198
+ );
199
+
200
+ elsif p_resource_kind = 'repository' or p_resource_kind = 'repo_file' then
201
+ if p_resource_kind = 'repo_file' then
202
+ -- FIRST ':' splits fp | path (the exact position CLAIMKEY froze). No colon / empty fp =
203
+ -- not resolvable -> deny (honest verdict; fail-closed).
204
+ v_sep := position(':' in coalesce(p_resource_key, ''));
205
+ if v_sep <= 1 then
206
+ return false;
207
+ end if;
208
+ v_fp := btrim(substring(p_resource_key from 1 for v_sep - 1));
209
+ else
210
+ v_fp := btrim(p_resource_key);
211
+ end if;
212
+ if v_fp is null or v_fp = '' then
213
+ return false;
214
+ end if;
215
+ -- FULL chain, EVERY edge lifecycle-active (PANEL AMENDMENT: harness revoke does not cascade
216
+ -- to child scopes, so fhr.lifecycle_state='active' is load-bearing here). Anchored on the
217
+ -- actor's active binding + its repo_scope; UNION across trust domains (shared-lock model).
218
+ return exists (
219
+ select 1
220
+ from agent_board.fleet_machine_agents fma
221
+ join agent_board.fleet_repo_scopes frs
222
+ on frs.id = fma.repo_scope_id
223
+ join agent_board.fleet_harness_registrations fhr
224
+ on fhr.id = frs.harness_registration_id
225
+ join agent_board.fleet_machines fm
226
+ on fm.id = fhr.machine_id
227
+ join agent_board.fleet_trust_domains ftd
228
+ on ftd.id = fm.trust_domain_id
229
+ where fma.agent_id = p_actor
230
+ and fma.lifecycle_state = 'active'
231
+ and frs.repo_fingerprint = v_fp
232
+ and frs.scope_role <> 'observer'
233
+ and frs.lifecycle_state = 'active'
234
+ and fhr.lifecycle_state = 'active'
235
+ and fm.lifecycle_state = 'active'
236
+ and ftd.lifecycle_state = 'active'
237
+ );
238
+
239
+ else
240
+ -- Defensive: an unenumerated future kind. The completeness test enumerates the 7 catalog
241
+ -- kinds and asserts each is handled, so this is unreachable for today's constraint.
242
+ return true;
243
+ end if;
244
+ end;
245
+ $function$;
246
+ -- LEAST-PRIVILEGE (panel must-fix): grant to service_role ONLY — NOT authenticated. This
247
+ -- SECURITY DEFINER function is an RLS-bypassing authority ORACLE: fleet_machine_agents' SELECT
248
+ -- policy (20260629000100:341-355) hides a binding from everyone but its subject/machine-owner,
249
+ -- but _for would let ANY authenticated caller probe an arbitrary (actor, fingerprint) pair and
250
+ -- learn whether that actor is fleet-authorized. The only call-site is the session-bound wrapper
251
+ -- _can_claim_resource (which binds current_agent_id() and is itself SECURITY DEFINER, so it
252
+ -- invokes _for as OWNER regardless of this grant) — no RPC passes caller input as p_actor. So the
253
+ -- authenticated grant buys nothing and only exposes the oracle; revoke it. The operator census
254
+ -- path runs as service_role.
255
+ revoke all on function agent_board._can_claim_resource_for(uuid, text, text) from public, anon;
256
+ grant execute on function agent_board._can_claim_resource_for(uuid, text, text) to service_role;
257
+ comment on function agent_board._can_claim_resource_for(uuid, text, text) is
258
+ 'CLAIMGATE/ENFORCE (#133): actor-parameterized fleet-graph authority — computes authority for an '
259
+ 'ARBITRARY p_actor (NO service_role carve-out; that is a caller property, kept in the session '
260
+ 'wrapper _can_claim_resource). Same fleet chains as SHADOW. Lets the operator (service_role) census '
261
+ 'EXISTING held claims (_can_claim_resource_for(claim.agent_id, kind, key)). SECURITY DEFINER; granted '
262
+ 'to service_role ONLY (an RLS-bypass authority oracle — NOT exposed to authenticated); PUBLIC revoked.';
263
+
264
+ -- 3. _can_claim_resource — the frozen IF-0-SHADOW-1 wrapper, now delegating to the twin. ----
265
+ -- BEHAVIOR UNCHANGED: service_role short-circuits true; otherwise authority for the SESSION
266
+ -- actor (current_agent_id()). The 36-assertion predicate suite regression-pins this refactor.
267
+ create or replace function agent_board._can_claim_resource(
268
+ p_resource_kind text,
269
+ p_resource_key text
270
+ )
271
+ returns boolean
272
+ language plpgsql
273
+ stable
274
+ security definer
275
+ set search_path = 'agent_board', 'public'
276
+ as $function$
277
+ begin
278
+ -- Operator carve-out: service_role short-circuits authorized (matches every other gate).
279
+ if agent_board._is_service_role() then
280
+ return true;
281
+ end if;
282
+ return agent_board._can_claim_resource_for(
283
+ agent_board.current_agent_id(), p_resource_kind, p_resource_key);
284
+ end;
285
+ $function$;
286
+ revoke all on function agent_board._can_claim_resource(text, text) from public, anon;
287
+ grant execute on function agent_board._can_claim_resource(text, text) to authenticated, service_role;
288
+ comment on function agent_board._can_claim_resource(text, text) is
289
+ 'CLAIMGATE (#133, IF-0-SHADOW-1): session-derived fleet-graph authority predicate ENFORCE consumes. '
290
+ 'service_role short-circuits true; otherwise delegates to _can_claim_resource_for(current_agent_id()). '
291
+ 'Behavior FROZEN and unchanged from SHADOW (the actor-parameterized split is behavior-preserving). '
292
+ 'SECURITY DEFINER to break fleet RLS recursion; PUBLIC EXECUTE revoked.';
293
+
294
+ -- 4. acquire_claim — per-kind gated DENIAL + horizon clamp. ---------------------------------
295
+ -- Reproduced VERBATIM from 20260824000100 (SHADOW) EXCEPT the single ENFORCE block inserted
296
+ -- right after the shadow verdict is computed and BEFORE the sweep (predicate-before-side-
297
+ -- effects: a denied caller observes no state change — no swept-expired rows leak the deny).
298
+ create or replace function agent_board.acquire_claim(
299
+ p_agent_id uuid,
300
+ p_resource_kind text,
301
+ p_resource_key text,
302
+ p_lease_expires_at timestamp with time zone,
303
+ p_task_id uuid default null,
304
+ p_thread_id uuid default null,
305
+ p_metadata jsonb default '{}'::jsonb
306
+ )
307
+ returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamp with time zone, released_at timestamp with time zone, created_at timestamp with time zone, updated_at timestamp with time zone)
308
+ language plpgsql
309
+ security definer
310
+ set search_path to 'agent_board', 'public'
311
+ as $function$
312
+ declare
313
+ v_thread_id uuid;
314
+ v_claim_id uuid;
315
+ v_actor_agent_id uuid;
316
+ v_shadow_authorized boolean;
317
+ v_ownerless boolean;
318
+ v_active_ownerless int;
319
+ begin
320
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
321
+
322
+ if nullif(trim(p_resource_key), '') is null then
323
+ raise exception 'resource key is required' using errcode = '22023';
324
+ end if;
325
+
326
+ -- CLAIMKEY (#133): normalize the resource key to its canonical, fleet-fingerprint-carrying
327
+ -- form BEFORE any lookup/insert, so the row stored (and the uniqueness index contended)
328
+ -- carry canonical identity. NORMALIZE-ONLY — this never rejects (rejection is ENFORCE).
329
+ p_resource_key := agent_board._canonical_claim_key(p_resource_kind, p_resource_key);
330
+
331
+ if p_task_id is not null then
332
+ select tasks.thread_id into v_thread_id from agent_board.tasks where id = p_task_id;
333
+ if v_thread_id is null then
334
+ raise exception 'task not found' using errcode = 'P0002';
335
+ end if;
336
+ if not agent_board._current_agent_can_access_task(p_task_id) then
337
+ raise exception 'task not accessible to current agent' using errcode = '42501';
338
+ end if;
339
+ else
340
+ v_thread_id := p_thread_id;
341
+ end if;
342
+
343
+ if v_thread_id is not null and not agent_board._current_agent_can_access_thread(v_thread_id) then
344
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
345
+ end if;
346
+
347
+ -- OBSGATE-4 (#127): a claim on a TASK resource is authorized on the LOCKED task = the task
348
+ -- named by RESOURCE_KEY (resource_kind='task'). The active-resource uniqueness is on
349
+ -- (resource_kind, resource_key), so that is the task a squat contends for — gating resource_key
350
+ -- (NOT p_task_id, which only associates the claim/event) prevents locking task B's key while
351
+ -- authorizing on an unrelated task A passed as p_task_id. A non-uuid 'task' key cannot collide
352
+ -- with a uuid-keyed legitimate claim.
353
+ --
354
+ -- The composition MIRRORS append_task_event's: ACCESS then MUTATE. mutate alone is
355
+ -- insufficient — _current_agent_can_mutate_task admits any non-observer board member via
356
+ -- _current_agent_can_post_to_thread (board send-role), which is NOT thread access; on a
357
+ -- RESTRICTED thread a board member lacking thread access would pass mutate but must still be
358
+ -- denied (they cannot access the task). Requiring access too closes that gap and matches the
359
+ -- authorization a task-BOUND acquire already gets through append_task_event.
360
+ if p_resource_kind = 'task'
361
+ and p_resource_key ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' then
362
+ if not agent_board._current_agent_can_access_task(p_resource_key::uuid) then
363
+ raise exception 'task not accessible to current agent' using errcode = '42501';
364
+ end if;
365
+ if not agent_board._current_agent_can_mutate_task(p_resource_key::uuid) then
366
+ raise exception 'claiming a task resource requires a non-observer board role, or task assignment/creation'
367
+ using errcode = '42501';
368
+ end if;
369
+ end if;
370
+
371
+ -- CLAIMGATE/SHADOW (#133): compute the fleet-graph authority verdict BEFORE the sweep (so
372
+ -- ENFORCE can flip this same point fail-closed with no observable side effect leaking to an
373
+ -- unauthorized caller). The verdict is stamped below regardless of enforcement.
374
+ v_shadow_authorized := agent_board._can_claim_resource(p_resource_kind, p_resource_key);
375
+
376
+ -- CLAIMGATE/ENFORCE (#133): per-kind GATED DENIAL, BEFORE the sweep (predicate-before-side-
377
+ -- effects). When this fleet-backed kind's flag is ON and the actor is UNAUTHORIZED, DENY —
378
+ -- no state has changed yet (sweep/insert are below). When OFF, fall through to the shadow-
379
+ -- stamp only (SHADOW behavior; blocks nothing). Non-fleet kinds never kind-enforce (resolver
380
+ -- returns FALSE). service_role is authorized by the predicate (wrapper short-circuit), so the
381
+ -- operator carve-out holds. Fail-closed-on-unknown is automatic: an unresolvable fleet resource
382
+ -- makes v_shadow_authorized FALSE, so an enforced acquire of it denies.
383
+ if p_resource_kind in ('machine', 'repository', 'repo_file')
384
+ and agent_board._claimgate_kind_enforced(p_resource_kind)
385
+ and not v_shadow_authorized then
386
+ raise exception 'not authorized to claim this % resource (fleet authority required)', p_resource_kind
387
+ using errcode = '42501';
388
+ end if;
389
+
390
+ -- CLAIMGATE/ENFORCE (#133): max-lease horizon. For an ENFORCED fleet-backed kind, CLAMP the
391
+ -- lease to now()+horizon (default 30d, operator-configurable) — a bound on lease term, NOT an
392
+ -- authority block. Clamp (not reject) so an over-long lease reconciles here / at its next renew.
393
+ -- service_role exempt (operator carve-out). Only reached by an AUTHORIZED acquire (unauthorized
394
+ -- was denied above), or when the flag is OFF (then the guard below is skipped entirely).
395
+ if p_resource_kind in ('machine', 'repository', 'repo_file')
396
+ and not agent_board._is_service_role()
397
+ and agent_board._claimgate_kind_enforced(p_resource_kind)
398
+ and p_lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() then
399
+ p_lease_expires_at := now() + agent_board._claimgate_max_lease_horizon();
400
+ end if;
401
+
402
+ -- Ownerless tail (file/tool/custom): the ONE guard SHADOW carries — GATED OFF BY DEFAULT.
403
+ -- _claimgate_ownerless_guards_enabled() is false unless an operator flips it post-census, so in
404
+ -- SHADOW this block is skipped entirely and NOTHING is blocked (only the stamp above lands).
405
+ -- service_role is also exempt (operator carve-out, consistent with the predicate short-circuit).
406
+ v_ownerless := p_resource_kind in ('file', 'tool', 'custom');
407
+ if v_ownerless
408
+ and not agent_board._is_service_role()
409
+ and agent_board._claimgate_ownerless_guards_enabled() then
410
+ -- TTL CAP: an ownerless lease may not exceed the max horizon. Rejects an over-long acquire;
411
+ -- this is a lease-length bound, NOT an authority block.
412
+ if p_lease_expires_at > now() + agent_board._claimgate_ownerless_ttl_cap() then
413
+ raise exception 'ownerless claim lease horizon exceeds cap (%)', agent_board._claimgate_ownerless_ttl_cap()
414
+ using errcode = '22023';
415
+ end if;
416
+
417
+ -- QUOTA (transactionally serialized): an actor-scoped advisory XACT lock serializes this
418
+ -- actor's concurrent acquisitions (on DIFFERENT keys, which the row-level uniqueness index
419
+ -- does NOT), so a count-before-insert cannot race two claims past the cap. The lock releases
420
+ -- at commit; it does not block other actors (the key mixes in the actor id).
421
+ perform pg_advisory_xact_lock(hashtext('agent_board.claimgate_ownerless_quota'), hashtext(v_actor_agent_id::text));
422
+ select count(*) into v_active_ownerless
423
+ from agent_board.claims c
424
+ where c.agent_id = v_actor_agent_id
425
+ and c.resource_kind in ('file', 'tool', 'custom')
426
+ and c.status = 'active'
427
+ and c.released_at is null
428
+ and c.lease_expires_at > now();
429
+ if v_active_ownerless >= agent_board._claimgate_ownerless_quota() then
430
+ raise exception 'ownerless active-claim quota reached (% per agent)', agent_board._claimgate_ownerless_quota()
431
+ using errcode = '54000';
432
+ end if;
433
+ end if;
434
+
435
+ -- CLAIMKEY (#133) MUST-FIX 3: the sweep AND the conflict check are ALIAS-AWARE. p_resource_key
436
+ -- is already canonical (above); comparing against the CANONICAL form of each existing row's key
437
+ -- matches both a canonical row AND an un-healed LEGACY row for the same physical resource. Without
438
+ -- this, acquire('machine','machine:fpDH') would normalize to 'fpDH', see no exact-'fpDH' conflict
439
+ -- against a still-active legacy 'machine:fpDH' row, and INSERT a second live hold — breaking mutual
440
+ -- exclusion during the legacy window. Closing it here in code does NOT depend on the operator-owned
441
+ -- live census.
442
+ update agent_board.claims c
443
+ set status = 'expired',
444
+ updated_at = now()
445
+ where c.resource_kind = p_resource_kind
446
+ and agent_board._canonical_claim_key(c.resource_kind, c.resource_key) = p_resource_key
447
+ and c.status = 'active'
448
+ and c.released_at is null
449
+ and c.lease_expires_at <= now();
450
+
451
+ if exists (
452
+ select 1 from agent_board.claims
453
+ where claims.resource_kind = p_resource_kind
454
+ and agent_board._canonical_claim_key(claims.resource_kind, claims.resource_key) = p_resource_key
455
+ and claims.status = 'active'
456
+ and claims.released_at is null
457
+ and claims.lease_expires_at > now()
458
+ ) then
459
+ raise exception 'conflicting active claim' using errcode = '23505';
460
+ end if;
461
+
462
+ insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at, metadata)
463
+ values (
464
+ p_task_id, v_thread_id, v_actor_agent_id, p_resource_kind, p_resource_key, p_lease_expires_at,
465
+ -- SHADOW stamp: record the authority verdict alongside any caller metadata. Merged last so it
466
+ -- is authoritative over a caller-supplied claimgate_shadow key.
467
+ coalesce(p_metadata, '{}'::jsonb)
468
+ || jsonb_build_object(
469
+ 'claimgate_shadow',
470
+ jsonb_build_object('kind', p_resource_kind, 'authorized', v_shadow_authorized)
471
+ )
472
+ )
473
+ returning id into v_claim_id;
474
+
475
+ if p_task_id is not null then
476
+ perform agent_board.append_task_event(p_task_id, v_actor_agent_id, 'claimed', jsonb_build_object('claim_id', v_claim_id, 'resource_kind', p_resource_kind, 'resource_key', p_resource_key));
477
+ end if;
478
+
479
+ return query
480
+ select c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at
481
+ from agent_board.claims c
482
+ where c.id = v_claim_id;
483
+ end;
484
+ $function$;
485
+ revoke all on function agent_board.acquire_claim(uuid, text, text, timestamp with time zone, uuid, uuid, jsonb) from public, anon;
486
+ grant execute on function agent_board.acquire_claim(uuid, text, text, timestamp with time zone, uuid, uuid, jsonb) to authenticated, service_role;
487
+
488
+ -- 5. renew_claim — RE-AUTHORIZE fleet-gated kinds + horizon clamp. --------------------------
489
+ -- Reproduced VERBATIM from 20260821000100 (CLAIMKEY) EXCEPT the ENFORCE block after the
490
+ -- ownership check: (a) re-run the SAME session predicate acquire consults for the 3 predicate-
491
+ -- gated kinds when the kind's flag is ON, so a revoked binding cannot perpetually extend a
492
+ -- lease; (b) clamp the renewed lease to the max horizon. Task-kind renew stays OWNERSHIP-ONLY
493
+ -- (this SUPERSEDES 20260817000100:27-28, maintainer-ratified 2026-07-31): only machine/
494
+ -- repository/repo_file re-auth; task/file/tool/custom renew unchanged, release_claim untouched.
495
+ create or replace function agent_board.renew_claim(p_lease_token uuid, p_lease_expires_at timestamptz)
496
+ returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamptz, released_at timestamptz, created_at timestamptz, updated_at timestamptz)
497
+ language plpgsql
498
+ security definer
499
+ set search_path = agent_board, public
500
+ as $$
501
+ declare
502
+ v_actor_agent_id uuid;
503
+ v_kind text;
504
+ v_key text;
505
+ v_effective_expires timestamptz;
506
+ begin
507
+ if not agent_board._is_service_role() then
508
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
509
+ if not exists (
510
+ select 1
511
+ from agent_board.claims c
512
+ where c.lease_token = p_lease_token
513
+ and c.agent_id = v_actor_agent_id
514
+ ) then
515
+ raise exception 'claim not accessible to current agent' using errcode = '42501';
516
+ end if;
517
+ end if;
518
+
519
+ -- CLAIMGATE/ENFORCE (#133): re-authorize + horizon for the predicate-gated kinds. Read the
520
+ -- live lease's resource identity (SECURITY DEFINER, so RLS does not hide it). A dead/expired
521
+ -- lease yields a null kind and the UPDATE below matches nothing (preserves prior behavior).
522
+ select c.resource_kind, c.resource_key
523
+ into v_kind, v_key
524
+ from agent_board.claims c
525
+ where c.lease_token = p_lease_token
526
+ and c.status = 'active'
527
+ and c.released_at is null
528
+ and c.lease_expires_at > now();
529
+
530
+ -- RE-AUTH: bound revocation latency to one lease term. Uses the SAME session wrapper acquire
531
+ -- consults (_can_claim_resource -> _can_claim_resource_for(current_agent_id())), so service_role
532
+ -- keeps its carve-out and acquire+renew share ONE per-kind flag (a mutation splitting the flags
533
+ -- fails a test). Task-renew is EXCLUDED (ownership-only, ratified supersession).
534
+ if v_kind in ('machine', 'repository', 'repo_file')
535
+ and agent_board._claimgate_kind_enforced(v_kind) then
536
+ if not agent_board._can_claim_resource(v_kind, agent_board._canonical_claim_key(v_kind, v_key)) then
537
+ raise exception 'not authorized to renew this % claim (fleet authority revoked)', v_kind
538
+ using errcode = '42501';
539
+ end if;
540
+ end if;
541
+
542
+ -- HORIZON: clamp the renewed lease to now()+horizon for an ENFORCED fleet-gated kind — this is
543
+ -- where an existing overlong lease reconciles (no bulk-clamp migration; the horizon clamps it
544
+ -- at its next renew). service_role exempt. Same per-kind flag as acquire.
545
+ v_effective_expires := p_lease_expires_at;
546
+ if v_kind in ('machine', 'repository', 'repo_file')
547
+ and not agent_board._is_service_role()
548
+ and agent_board._claimgate_kind_enforced(v_kind)
549
+ and p_lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() then
550
+ v_effective_expires := now() + agent_board._claimgate_max_lease_horizon();
551
+ end if;
552
+
553
+ -- CLAIMKEY (#133) MUST-FIX 1: re-canonicalize the STORED key as the lease is renewed, but
554
+ -- NEVER propagate a uniqueness collision. A check-then-act guard is NOT race-safe: two aliased
555
+ -- legacy rows renewed concurrently at READ COMMITTED can both observe the canonical key free
556
+ -- and one loses on claims_active_resource_unique (23505). So attempt the canonicalization and,
557
+ -- on unique_violation (deterministic alias OR concurrent race), retry ONCE keeping the CURRENT
558
+ -- key. That makes renew non-raising UNCONDITIONALLY (true normalize-only); the surviving legacy
559
+ -- key is the deferred dual-key alias the ENFORCE phase closes.
560
+ begin
561
+ return query
562
+ update agent_board.claims c
563
+ set lease_expires_at = v_effective_expires,
564
+ resource_key = agent_board._canonical_claim_key(c.resource_kind, c.resource_key),
565
+ updated_at = now()
566
+ where c.lease_token = p_lease_token
567
+ and c.status = 'active'
568
+ and c.released_at is null
569
+ and c.lease_expires_at > now()
570
+ returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
571
+ exception when unique_violation then
572
+ -- the canonical key is already held (by a committed twin or a concurrent renew/acquire):
573
+ -- renew the lease keeping the current key, aliasing deferred to ENFORCE. Only the key change
574
+ -- can raise unique_violation (the lease/timestamp columns are unconstrained), so the retry
575
+ -- cannot itself collide.
576
+ return query
577
+ update agent_board.claims c
578
+ set lease_expires_at = v_effective_expires,
579
+ updated_at = now()
580
+ where c.lease_token = p_lease_token
581
+ and c.status = 'active'
582
+ and c.released_at is null
583
+ and c.lease_expires_at > now()
584
+ returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
585
+ end;
586
+ end;
587
+ $$;
588
+ revoke all on function agent_board.renew_claim(uuid, timestamptz) from public, anon;
589
+ grant execute on function agent_board.renew_claim(uuid, timestamptz) to authenticated, service_role;
590
+
591
+ -- 6. _claimgate_promotion_blockers — PROMOTION-READINESS gate (panel must-fix, roadmap :346/:352).
592
+ -- Enforcement flags stay OPERATOR-GATED and DEFAULT OFF (there is NO auto-clamp / auto-migrate).
593
+ -- This is the VERIFICATION the roadmap's "reconcile before enforcement" (:346) and "zero
594
+ -- non-canonical fleet claims at flip" (:352) exit criteria require, made checkable WITHOUT
595
+ -- mutating any lease. For a fleet kind it returns every ACTIVE (status='active', unreleased,
596
+ -- unexpired) claim that would BREAK the instant enforce_<kind> flips ON:
597
+ -- (a) unauthorized — the CURRENT holder is no longer fleet-authorized for it, computed with
598
+ -- the actor-parameterized _can_claim_resource_for(agent_id, kind, key) (this is exactly
599
+ -- why that private twin exists — it evaluates an EXISTING holder, not the session actor);
600
+ -- (b) over_horizon — lease_expires_at exceeds now()+_claimgate_max_lease_horizon(), i.e. a
601
+ -- lease that would be clamped (and, for a lease that will never renew, would otherwise run
602
+ -- past the bound forever — the years-long-lease case);
603
+ -- (c) non_canonical — resource_key <> _canonical_claim_key(...), the dual-key aliasing window.
604
+ -- An EMPTY result for a kind == SAFE to flip that kind's flag.
605
+ --
606
+ -- =====================================================================================
607
+ -- PROMOTION RUNBOOK (operator, per fleet kind — machine, then repository, then repo_file)
608
+ -- =====================================================================================
609
+ -- 1. select * from agent_board._claimgate_promotion_blockers('<kind>'); -- as service_role
610
+ -- 2. For EACH row: reconcile it — release_claim / let it expire (unauthorized or over-horizon),
611
+ -- or renew_claim once to self-heal a non_canonical key (CLAIMKEY re-canonicalizes on renew).
612
+ -- 3. Re-run step 1; when it returns ZERO rows the kind is safe.
613
+ -- 4. Only then: insert into agent_board.claimgate_enforcement_flags(scope_key, enabled)
614
+ -- values ('enforce_<kind>', true) on conflict (scope_key) do update set enabled = true;
615
+ -- (The ownerless file/tool/custom guards remain INDEPENDENTLY flag-gated behind
616
+ -- claimgate_enforcement_flags('ownerless_guards') — OFF until the prod lease-length census;
617
+ -- they are NOT co-timed with fleet enforcement, per the maintainer decision, and this gate does
618
+ -- not cover them.)
619
+ --
620
+ -- #118: SECURITY DEFINER (reads all claims + the RLS-recursive fleet graph via _for) — service_role
621
+ -- ONLY (an operator census/oracle, same posture as _can_claim_resource_for); PUBLIC revoked.
622
+ create or replace function agent_board._claimgate_promotion_blockers(p_resource_kind text)
623
+ returns table(
624
+ claim_id uuid,
625
+ agent_id uuid,
626
+ resource_kind text,
627
+ resource_key text,
628
+ lease_expires_at timestamptz,
629
+ unauthorized boolean,
630
+ over_horizon boolean,
631
+ non_canonical boolean
632
+ )
633
+ language sql
634
+ stable
635
+ security definer
636
+ set search_path = ''
637
+ as $$
638
+ select c.id, c.agent_id, c.resource_kind, c.resource_key, c.lease_expires_at,
639
+ not agent_board._can_claim_resource_for(c.agent_id, c.resource_kind, c.resource_key) as unauthorized,
640
+ c.lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() as over_horizon,
641
+ c.resource_key is distinct from agent_board._canonical_claim_key(c.resource_kind, c.resource_key) as non_canonical
642
+ from agent_board.claims c
643
+ where c.resource_kind = p_resource_kind
644
+ and c.status = 'active'
645
+ and c.released_at is null
646
+ and c.lease_expires_at > now()
647
+ and (
648
+ not agent_board._can_claim_resource_for(c.agent_id, c.resource_kind, c.resource_key)
649
+ or c.lease_expires_at > now() + agent_board._claimgate_max_lease_horizon()
650
+ or c.resource_key is distinct from agent_board._canonical_claim_key(c.resource_kind, c.resource_key)
651
+ )
652
+ $$;
653
+ revoke all on function agent_board._claimgate_promotion_blockers(text) from public, anon;
654
+ grant execute on function agent_board._claimgate_promotion_blockers(text) to service_role;
655
+ comment on function agent_board._claimgate_promotion_blockers(text) is
656
+ 'CLAIMGATE/ENFORCE (#133, roadmap :346/:352): promotion-readiness gate for a fleet kind. Returns the '
657
+ 'ACTIVE claims that would BREAK at flip — unauthorized holder (via _can_claim_resource_for), '
658
+ 'over the max-lease horizon, or a non-canonical key. EMPTY == safe to set enforce_<kind>=true. '
659
+ 'Verification only (no lease is mutated; enforcement stays operator-gated, default OFF). '
660
+ 'SECURITY DEFINER; service_role ONLY (operator census); PUBLIC revoked.';