@consiliency/agent-board-schema 1.45.0 → 1.50.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 (35) hide show
  1. package/fixtures/already-applied.json +24 -0
  2. package/migrations/20260926000100_agent_board_ownerless_quota_census.sql +268 -0
  3. package/migrations/20260927000100_agent_board_fleet_domain_no_downgrade.sql +393 -0
  4. package/migrations/20260928000100_agent_board_ownerless_quota_renew_convergence.sql +562 -0
  5. package/migrations/20260929000100_agent_board_advisory_revert_cap_reset.sql +371 -0
  6. package/migrations/20260930000100_agent_board_claim_policy_race.sql +922 -0
  7. package/migrations/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql +158 -0
  8. package/package.json +1 -1
  9. package/schema-manifest.json +57 -3
  10. package/src/tooling.test.mjs +52 -2
  11. package/tests/agent_board_board_claim_policy.test.sql +261 -1
  12. package/tests/agent_board_claim_subject_gate.test.sql +11 -2
  13. package/tests/agent_board_claimkey.test.sql +17 -2
  14. package/tests/agent_board_claimpolicy_race.test.sql +1374 -0
  15. package/tests/agent_board_fleet_machine_hijack.test.sql +270 -1
  16. package/tests/agent_board_fleet_machine_reclaim.test.sql +28 -2
  17. package/tests/agent_board_ownerless_quota_census.test.sql +406 -0
  18. package/tests/agent_board_ownerless_renew_quota.test.sql +580 -0
  19. package/tests/catalog/p_metadata_parameters.json +28 -0
  20. package/tests/fixtures/test-prereqs.sql +186 -0
  21. package/tests/isolation/expected/claimpolicy_acquire_revert_race.out +66 -0
  22. package/tests/isolation/expected/claimpolicy_board_scope.out +73 -0
  23. package/tests/isolation/expected/claimpolicy_claimrow_lock_order.out +80 -0
  24. package/tests/isolation/expected/claimpolicy_isolation_refusal.out +114 -0
  25. package/tests/isolation/expected/claimpolicy_renew_revert_race.out +66 -0
  26. package/tests/isolation/expected/claimpolicy_writer_keyshare.out +49 -0
  27. package/tests/isolation/expected/fleet_domain_no_downgrade_demote_race.out +447 -0
  28. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +118 -0
  29. package/tests/isolation/specs/claimpolicy_board_scope.spec +115 -0
  30. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +200 -0
  31. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +162 -0
  32. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +150 -0
  33. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +107 -0
  34. package/tests/isolation/specs/fleet_domain_no_downgrade_demote_race.spec +292 -0
  35. package/tests/seeds/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql.pre.sql +99 -0
@@ -0,0 +1,371 @@
1
+ -- #400 — a board owner can defeat ownerless-quota convergence indefinitely, and leave no trace.
2
+ --
3
+ -- THE DEFECT, AND IT NEEDS NO ACCOMPLICE. `create_board` seeds the creator's owner membership, and
4
+ -- that membership is exactly what `set_board_claim_policy`'s gate tests. So an agent that creates a
5
+ -- board holds the switch that exempts its own claims:
6
+ --
7
+ -- 1. flip the board to claim_policy = 'advisory';
8
+ -- 2. renew every ownerless claim to the full horizon — the advisory carve-out skips BOTH the
9
+ -- #373 TTL cap and the #388 convergence clamp;
10
+ -- 3. flip back to 'inherit';
11
+ -- 4. repeat before expiry.
12
+ --
13
+ -- And it is invisible. #393's census resolves board policy at QUERY time (OQ-29), so after step 3
14
+ -- `claims_on_advisory_boards` reports ZERO for those claims while the overage they bought is still
15
+ -- counted. An operator sees an agent over quota with no advisory share — the shape that reads as
16
+ -- plain squatting — and the column built to explain the exemption reports none of it. #399 shipped
17
+ -- with this pinned as a test (ORQ-11/12) rather than described in prose, precisely so that closing
18
+ -- it would have to change an assertion instead of a paragraph.
19
+ --
20
+ -- ===========================================================================
21
+ -- THE FIX: A CAP RESET ON REVERT. On an 'advisory' -> 'inherit' transition, re-apply the ownerless
22
+ -- TTL cap to that board's active ownerless claims. Clamp only — least(lease_expires_at, now() +
23
+ -- _claimgate_ownerless_ttl_cap()) — never extend.
24
+ -- ===========================================================================
25
+ --
26
+ -- THE FAIRNESS ARGUMENT, which is the whole justification: reverting to 'inherit' means accepting
27
+ -- inherit's rules, and the clamp only ever SHORTENS. A board that wants the long horizon its own
28
+ -- mutex granted may keep it — by staying advisory. What it may not do is bank a long lease under
29
+ -- the opt-out and then hand the opt-out back. The claim keeps exactly what an ordinary claim on an
30
+ -- ordinary inherit board is allowed to keep, which is what "inherit" means.
31
+ --
32
+ -- WHY NOT THE AUDIT LEDGER #400 ORIGINALLY PROPOSED. A ledger makes the escape VISIBLE; this
33
+ -- removes its BENEFIT, and the two are not equal. To hold a lease longer than the cap the board
34
+ -- must STAY advisory — and while it is advisory those claims are counted by the census's
35
+ -- `claims_on_advisory_boards`, which is exactly the column an operator reads. So above the cap the
36
+ -- escape becomes either ineffective or visible, with no new mechanism and no policy history to
37
+ -- maintain. The owner chose this over the ledger.
38
+ --
39
+ -- ===========================================================================
40
+ -- WHAT THIS DOES NOT CLOSE. BOUNDED, NOT CLOSED — and the difference is stated here rather than
41
+ -- discovered later, because a header that overclaims is the #399 failure being repeated.
42
+ -- ===========================================================================
43
+ --
44
+ -- 1. THE FLIP-FLOP STILL WORKS, up to the cap. Each advisory window still buys a lease of up to
45
+ -- `_claimgate_ownerless_ttl_cap()` (30 days) that survives the revert, so an owner who flips
46
+ -- every 29 days keeps an over-quota agent over quota forever. What is gone is the UNBOUNDED
47
+ -- horizon: the ceiling on what a cycle can bank drops from "whatever was asked for" to the
48
+ -- same cap every non-advisory ownerless lease already lives under. #388's convergence clamp
49
+ -- is therefore still defeatable by a board owner; it is no longer defeatable by ONE flip.
50
+ -- ORQ-11 asserts the bound (the +400 day lease does not survive the revert) and says so.
51
+ --
52
+ -- 2. IT IS STILL INVISIBLE BETWEEN FLIPS. Nothing here records that the exemption was used, and
53
+ -- the census still resolves policy at query time. ORQ-12 is unchanged and still pins that.
54
+ -- Recording acquire-time policy remains #393's own documented gap and is not invented here.
55
+ --
56
+ -- 3. AN IN-FLIGHT RENEW CAN STILL WIN THE RACE, AND THIS IS UNPINNED. `_claimgate_board_claim_
57
+ -- policy` takes no lock on `boards`, so a `renew_claim` whose snapshot predates the revert's
58
+ -- commit resolves 'advisory' and, under READ COMMITTED, re-evaluates and applies its full
59
+ -- horizon AFTER this function's row lock is released at commit. The attacker owns both
60
+ -- sessions, so a deliberate interleave banks a full-horizon lease past the revert. Closing it
61
+ -- means taking a `boards` lock on the renew path, which is a LOCK-ORDER decision (renew
62
+ -- already touches claims and, through the resolver, threads/boards) and belongs with the
63
+ -- two-session isolationtester harness, not with this change. Recorded as open rather than
64
+ -- quietly excluded from the claim.
65
+ --
66
+ -- 4. AN OPERATOR EXTENSION IS CLAMPED TOO. `service_role` is exempt from the guards at acquire
67
+ -- and at renew, so an operator may deliberately hand an agent a 400-day ownerless lease. If
68
+ -- that claim's board is later reverted from advisory to inherit, this reset shortens it —
69
+ -- nothing records who last extended a lease, and inventing provenance to protect it would be
70
+ -- a second unreviewed mechanism. The operator can re-extend; service_role's renew is exempt.
71
+ --
72
+ -- ===========================================================================
73
+ -- THE DECISIONS, EACH PINNED
74
+ -- ===========================================================================
75
+ --
76
+ -- TRIGGER CONDITION: 'advisory' -> 'inherit' ONLY. Not 'inherit' -> 'inherit' (a no-op re-set must
77
+ -- clamp nothing; BCP-15), not 'inherit' -> 'advisory' (BCP-16), not 'advisory' -> 'advisory'
78
+ -- (BCP-17 — the one the other three cannot see: dropping the `= 'inherit'` conjunct leaves
79
+ -- "previously advisory", which still passes the two inherit-first cases). The OUTGOING policy is
80
+ -- read under `for no key update` on the board row, so two concurrent flips serialize and only the
81
+ -- one that actually observed 'advisory' resets. `for no key update` is deliberate over `for
82
+ -- update`: it does not conflict with the FOR KEY SHARE that an FK reference to `boards` takes, so
83
+ -- inserting a thread on this board does not queue behind a policy flip.
84
+ --
85
+ -- CARVE-OUTS, and where they DIFFER from the guards this mirrors — stated because "consistency" is
86
+ -- the default expectation and one of the two is deliberately absent:
87
+ --
88
+ -- * `_claimgate_ownerless_guards_enabled()` GOVERNS THIS, exactly as it governs every other
89
+ -- ownerless guard on both write paths. With the flag off nothing enforces the TTL cap at
90
+ -- acquire or at renew, so re-applying it on a revert would enforce a cap that no other path
91
+ -- enforces — one flag, one meaning. BCP-18 pins it.
92
+ --
93
+ -- * THERE IS NO service_role CARVE-OUT, because there is nothing for it to carve out.
94
+ -- `set_board_claim_policy` REFUSES service_role outright (current_agent_id() is null, so the
95
+ -- owner gate raises 42501 — BCP-8) and the reset sits below that gate. An `and not
96
+ -- _is_service_role()` conjunct here would be dead code that reads like a policy. The carve-out
97
+ -- that DOES bite is the one recorded above as limitation 4: the reset governs the BOARD's
98
+ -- claims regardless of who last extended them.
99
+ --
100
+ -- SCOPE: active, unreleased, unexpired claims of the ownerless kinds whose thread resolves to this
101
+ -- board. The kind list is NOT hand-written a fourth time — `_claimgate_ownerless_kinds()` is
102
+ -- introduced below and `_claimgate_active_ownerless_claims()` (the helper both enforcement paths
103
+ -- count through) is re-created to consume it, so the reset and the guards share one definition. A
104
+ -- claim on ANOTHER board, or with no thread at all, is out of scope and untouched (BCP-14): this
105
+ -- board's revert is a statement about this board. So are the DEAD rows on this board — a released
106
+ -- claim and a lapsed one (BCP-21), scored on the lease for the released row and on `updated_at` for
107
+ -- the lapsed one, whose lease `least()` already protects on its own.
108
+ --
109
+ -- `least()`, AND IT IS THE ONLY CLAMP MECHANISM — deliberately not paired with a
110
+ -- `lease_expires_at > now() + cap` conjunct in the WHERE. The two would be the same guard written
111
+ -- twice, and only one of them can be mutation-visible: with the WHERE conjunct present, deleting
112
+ -- `least()` changes nothing observable. So `least()` stands alone and BCP-12 fails when it is
113
+ -- replaced by an assignment. The cost is that a claim already WITHIN the cap has its `updated_at`
114
+ -- bumped by a revert while its lease is preserved to the microsecond; that is a documented
115
+ -- consequence of keeping one mechanism rather than two, and BCP-12 asserts the lease only.
116
+ --
117
+ -- CLASS (rule 1): `set claim_policy` appears in exactly one function in `agent_board`, and no role
118
+ -- holds UPDATE on `agent_board.boards` — so this RPC is the only writer of the column and the only
119
+ -- place the hook can live. Both halves are asserted from the catalog in BCP-19/20, the second as a
120
+ -- privilege bound rather than as a longer regex.
121
+
122
+ -- ---------------------------------------------------------------------------
123
+ -- The ownerless kind list, once. Every other site still spells it inline; those are the two
124
+ -- enforcement paths (acquire_claim, renew_claim) and they are pinned BEHAVIOURALLY (ORT-9, ORQ-2)
125
+ -- because a kind that stops being clamped is only visible as a clamp that did not happen. They are
126
+ -- not re-emitted here: transcribing two 150-line function bodies for a cosmetic dedup is a
127
+ -- transcription risk taken for no behaviour change.
128
+ -- ---------------------------------------------------------------------------
129
+ create or replace function agent_board._claimgate_ownerless_kinds()
130
+ returns text[]
131
+ language sql
132
+ immutable
133
+ parallel safe
134
+ set search_path = ''
135
+ as $$
136
+ select array['file', 'tool', 'custom']::text[];
137
+ $$;
138
+
139
+ revoke all on function agent_board._claimgate_ownerless_kinds() from public, anon;
140
+ grant execute on function agent_board._claimgate_ownerless_kinds() to authenticated, service_role;
141
+
142
+ comment on function agent_board._claimgate_ownerless_kinds() is
143
+ 'The OWNERLESS claim kinds — the ones governed by the per-agent quota and the TTL cap rather '
144
+ 'than by the fleet-authority gate. Single definition shared by '
145
+ '_claimgate_active_ownerless_claims() (which both enforcement paths count through) and by '
146
+ 'set_board_claim_policy''s advisory-revert cap reset (#400). Granted to authenticated like its '
147
+ 'sibling _claimgate_ownerless_quota(): it returns a constant and tells a caller nothing about '
148
+ 'any row.';
149
+
150
+ -- ---------------------------------------------------------------------------
151
+ -- The count, re-created to consume the kind helper. Body otherwise unchanged from 20260928000100.
152
+ -- ---------------------------------------------------------------------------
153
+ create or replace function agent_board._claimgate_active_ownerless_claims(p_agent_id uuid)
154
+ returns integer
155
+ language sql
156
+ stable
157
+ set search_path = ''
158
+ as $$
159
+ select count(*)::integer
160
+ from agent_board.claims c
161
+ where c.agent_id = p_agent_id
162
+ and c.resource_kind = any(agent_board._claimgate_ownerless_kinds())
163
+ and c.status = 'active'
164
+ and c.released_at is null
165
+ and c.lease_expires_at > now();
166
+ $$;
167
+
168
+ revoke all on function agent_board._claimgate_active_ownerless_claims(uuid) from public, anon;
169
+ grant execute on function agent_board._claimgate_active_ownerless_claims(uuid) to service_role;
170
+
171
+ -- Rule 5: the previous wording said a board owner may flip "indefinitely", which #400 falsifies in
172
+ -- its unbounded form. Corrected in the migration that changes the behaviour, not left for the next
173
+ -- reader to disbelieve.
174
+ comment on function agent_board._claimgate_active_ownerless_claims(uuid) is
175
+ '#388: THE single definition of an agent''s active ownerless claim count — acquire_claim''s quota '
176
+ 'predicate (kind in _claimgate_ownerless_kinds(), status active, released_at null, lease '
177
+ 'unexpired). Called by acquire_claim (which refuses at >= _claimgate_ownerless_quota()) and by '
178
+ 'renew_claim (which stops EXTENDING at > quota, so an over-quota agent converges to the ceiling '
179
+ 'UNLESS its board opts out). The opt-out escape is now BOUNDED rather than unbounded (#400): a '
180
+ 'board owner may still flip claim_policy to advisory and back, but reverting to inherit '
181
+ 're-applies the ownerless TTL cap, so a cycle banks at most _claimgate_ownerless_ttl_cap() '
182
+ 'instead of an arbitrary horizon. NO board-policy filter here: the advisory opt-out gates whether '
183
+ 'a guard RUNS, not whether the resulting rows COUNT. service_role only, and deliberately narrower '
184
+ 'than _claimgate_ownerless_quota(): this reads claims under security invoker, so an authenticated '
185
+ 'caller would receive its own RLS-filtered count under a fleet-wide name.';
186
+
187
+ -- ---------------------------------------------------------------------------
188
+ -- set_board_claim_policy. The 20260922000100 body with the outgoing-policy read and the cap reset
189
+ -- added; the validation, the owner gate, the boards UPDATE, the P0002 check and the fleet-revocation
190
+ -- assertion are unchanged and in the same order.
191
+ --
192
+ -- ORDER: boards -> claims -> the agents-side revocation assertion. The canonical boards-before-
193
+ -- agents order (#194) is preserved and the reset sits between them; the assertion still raises in
194
+ -- the same transaction, so a fleet-revoked owner's reset is rolled back with their policy change.
195
+ -- ---------------------------------------------------------------------------
196
+ create or replace function agent_board.set_board_claim_policy(
197
+ p_board_id uuid,
198
+ p_claim_policy text
199
+ )
200
+ returns table(
201
+ board_id uuid,
202
+ claim_policy text,
203
+ updated_at timestamptz
204
+ )
205
+ language plpgsql
206
+ security definer
207
+ set search_path = agent_board, public
208
+ as $$
209
+ declare
210
+ v_actor uuid := agent_board.current_agent_id();
211
+ v_row agent_board.boards%rowtype;
212
+ v_prev_policy text;
213
+ begin
214
+ if p_claim_policy is null then
215
+ raise exception 'p_claim_policy must not be null' using errcode = '22023';
216
+ end if;
217
+ -- Validated HERE as well as by the column CHECK. The CHECK would reject an unknown value
218
+ -- anyway, but with a constraint-violation error naming an internal constraint; a caller asking
219
+ -- for 'off' deserves to be told the vocabulary, not handed 23514.
220
+ if p_claim_policy not in ('inherit', 'advisory') then
221
+ raise exception 'claim policy must be one of inherit, advisory (received %)', p_claim_policy
222
+ using errcode = '22023';
223
+ end if;
224
+
225
+ -- Owner gate: only an active owner-role member may set this. service_role
226
+ -- (current_agent_id() = null) is not an owner and is rejected here.
227
+ if v_actor is null
228
+ or not exists (
229
+ select 1
230
+ from agent_board.board_memberships bm
231
+ where bm.board_id = p_board_id
232
+ and bm.agent_id = v_actor
233
+ and bm.role = 'owner'
234
+ and bm.membership_state in ('joined', 'subscribed')
235
+ ) then
236
+ raise exception 'only a board owner may set the claim policy' using errcode = '42501';
237
+ end if;
238
+
239
+ -- #400: the OUTGOING policy, read under the row lock the UPDATE below will take anyway. Placed
240
+ -- AFTER the owner gate so an unauthorized caller never locks the row. `for no key update` rather
241
+ -- than `for update`: it does not conflict with the FOR KEY SHARE an FK reference to boards takes,
242
+ -- so a concurrent thread insert on this board is not queued behind a policy flip. Two concurrent
243
+ -- flips serialize here, so only the one that genuinely observed 'advisory' resets.
244
+ --
245
+ -- A missing board leaves this null and the UPDATE below matches nothing, so the P0002 answer and
246
+ -- its ordering are unchanged.
247
+ select b.claim_policy into v_prev_policy
248
+ from agent_board.boards b
249
+ where b.id = p_board_id
250
+ for no key update;
251
+
252
+ update agent_board.boards b
253
+ set claim_policy = p_claim_policy,
254
+ updated_at = now()
255
+ where b.id = p_board_id
256
+ returning * into v_row;
257
+
258
+ if v_row.id is null then
259
+ raise exception 'board not found' using errcode = 'P0002';
260
+ end if;
261
+
262
+ -- #400 THE CAP RESET ON REVERT. Reverting to 'inherit' means accepting inherit's rules, so this
263
+ -- board's active ownerless claims get the ownerless TTL cap re-applied. The escape it closes:
264
+ -- flip to advisory, renew every claim to an arbitrary horizon (the advisory carve-out skips both
265
+ -- the cap and #388's convergence clamp), flip back, repeat. An agent that created the board is
266
+ -- its owner, so no accomplice is needed. Nothing took the granted horizon back — the convergence
267
+ -- clamp only refuses to EXTEND — so the horizon simply survived the revert.
268
+ --
269
+ -- CLAMP ONLY, and least() is the ONLY thing doing it. There is deliberately no `lease_expires_at
270
+ -- > now() + cap` conjunct in the WHERE: it would be the same guard written twice and would make
271
+ -- least() unmutatable. So a claim already within the cap keeps its lease to the microsecond
272
+ -- (BCP-12) while its updated_at is bumped — the documented cost of one mechanism over two.
273
+ --
274
+ -- 'advisory' -> 'inherit' ONLY. A no-op re-set (inherit -> inherit) must clamp nothing, and
275
+ -- neither may inherit -> advisory or advisory -> advisory (BCP-15/16/17).
276
+ --
277
+ -- Governed by the same operator flag as every other ownerless guard: with ownerless_guards off
278
+ -- nothing enforces this cap at acquire or renew, so re-applying it here would enforce a bound no
279
+ -- other path enforces (BCP-18). There is no service_role conjunct because service_role cannot
280
+ -- reach this line — the owner gate above refuses it (BCP-8).
281
+ --
282
+ -- Scoped to THIS board through the claim's thread, the same path _claimgate_board_claim_policy
283
+ -- resolves. A claim on another board, or with no thread, is untouched (BCP-14).
284
+ if v_prev_policy = 'advisory'
285
+ and p_claim_policy = 'inherit'
286
+ and agent_board._claimgate_ownerless_guards_enabled() then
287
+ update agent_board.claims c
288
+ set lease_expires_at = least(c.lease_expires_at,
289
+ now() + agent_board._claimgate_ownerless_ttl_cap()),
290
+ updated_at = now()
291
+ where c.resource_kind = any(agent_board._claimgate_ownerless_kinds())
292
+ and c.status = 'active'
293
+ and c.released_at is null
294
+ and c.lease_expires_at > now()
295
+ and c.thread_id in (select t.id
296
+ from agent_board.threads t
297
+ where t.board_id = p_board_id);
298
+ end if;
299
+
300
+ -- #276, AFTER the boards UPDATE so this function's order is boards -> agents, the canonical one
301
+ -- (#194). Fleet revocation deliberately LEAVES agents.status = 'active' -- the ledger row is the
302
+ -- only authority signal -- so the owner-membership check above passes for a revoked owner and
303
+ -- cannot stand alone. Disabling this board's claim enforcement is an authority write in the same
304
+ -- class as flipping the role gate, which is why the mirror has to include this call and not just
305
+ -- the membership test. Same transaction: the raise rolls the UPDATE back -- and, since #400, the
306
+ -- cap reset with it.
307
+ --
308
+ -- This was MISSING in the first version of this function, which claimed in its own commit
309
+ -- message to mirror set_board_role_gate "exactly". A review seat found it by reading both
310
+ -- functions side by side rather than taking that claim.
311
+ perform agent_board._assert_actor_not_fleet_revoked(v_actor, 'set_board_claim_policy');
312
+
313
+ return query select v_row.id, v_row.claim_policy, v_row.updated_at;
314
+ end;
315
+ $$;
316
+
317
+ revoke all on function agent_board.set_board_claim_policy(uuid, text) from public, anon;
318
+ grant execute on function agent_board.set_board_claim_policy(uuid, text) to authenticated, service_role;
319
+
320
+ comment on function agent_board.set_board_claim_policy(uuid, text) is
321
+ 'Board-owner-only. Sets boards.claim_policy to ''inherit'' (follow the fleet-wide claimgate flags) '
322
+ 'or ''advisory'' (this board coordinates through another locking mechanism, so our ownerless '
323
+ 'file/tool/custom TTL cap and quota are recorded but not enforced). Never weakens the '
324
+ 'fleet-authority gate on machine/repository/repo_file, the subject gate, or claim uniqueness. '
325
+ 'Owner gate and service_role refusal mirror set_board_role_gate. #400: an advisory -> inherit '
326
+ 'transition RE-APPLIES the ownerless TTL cap to this board''s active ownerless claims — '
327
+ 'least(lease_expires_at, now() + _claimgate_ownerless_ttl_cap()), clamp only, never an extension '
328
+ '— so a long lease banked under the opt-out cannot be kept by handing the opt-out back. Governed '
329
+ 'by ownerless_guards; no other transition resets anything.';
330
+
331
+ -- ---------------------------------------------------------------------------
332
+ -- Rule 5: three recorded facts that were true when written and are not now. #399 stated the escape
333
+ -- as UNBOUNDED ("indefinitely", "forever"); it is now bounded by the TTL cap per cycle. Corrected
334
+ -- in the migration that falsifies them. The 20260928000100 file itself cannot be edited — its
335
+ -- bytes are hashed in the manifest and in fixtures/already-applied.json — so the comments it set
336
+ -- are restated here, which is the pattern that migration used for the same reason.
337
+ -- ---------------------------------------------------------------------------
338
+ comment on column agent_board.boards.claim_policy is
339
+ 'Board-settable claim enforcement. ''inherit'' follows the fleet-wide claimgate flags. ''advisory'' '
340
+ 'means this board coordinates through some OTHER locking mechanism, so our ownerless '
341
+ '(file/tool/custom) TTL cap and per-agent quota are recorded but not enforced for its claims. It '
342
+ 'does NOT weaken the fleet-authority gate on machine/repository/repo_file, the subject gate, or '
343
+ 'claim uniqueness — a board may decline our lock, never grant itself another owner''s resource. '
344
+ 'Set via agent_board.set_board_claim_policy (board owner only), which since #400 re-applies the '
345
+ 'ownerless TTL cap to this board''s active ownerless claims when the policy reverts from '
346
+ '''advisory'' to ''inherit'': the opt-out may be given back, but not after banking a lease longer '
347
+ 'than inherit allows.';
348
+
349
+ comment on function agent_board._claimgate_ownerless_quota() is
350
+ 'CLAIMGATE/SHADOW (#133): max concurrent ACTIVE ownerless (file/tool/custom) claims per agent. '
351
+ 'Single source for the migration logic and its tests. GENEROUS placeholder (50) pending prod '
352
+ 'census. Enforced on BOTH write paths: acquire_claim REFUSES at count >= quota (54000), and '
353
+ 'renew_claim stops EXTENDING at count > quota (#388) so an agent already over the ceiling '
354
+ 'converges back to it instead of renewing its overage forever — SCOPED to agents whose boards do '
355
+ 'not opt out. A board owner may flip claim_policy to advisory, let the agent renew, and flip '
356
+ 'back; since #400 the revert re-applies the ownerless TTL cap, so each such cycle banks at most '
357
+ '_claimgate_ownerless_ttl_cap() rather than an arbitrary horizon. The escape is bounded, not '
358
+ 'closed (ORQ-11/12). The two predicates differ deliberately — >= is what makes the quota the '
359
+ 'largest reachable population, > is what makes the ceiling the convergence fixed point — and both '
360
+ 'count through _claimgate_active_ownerless_claims(). Governed by '
361
+ '_claimgate_ownerless_guards_enabled() on both paths, service_role exempt, and skipped for a board '
362
+ 'whose claim_policy is advisory.';
363
+
364
+ comment on table agent_board.claimgate_enforcement_flags is
365
+ 'CLAIMGATE per-scope enforcement flags. Absent row => disabled (SHADOW default: blocks nothing). '
366
+ 'scope_key ''ownerless_guards'' gates EVERY ownerless (file/tool/custom) guard on ALL THREE write '
367
+ 'paths: the TTL cap + per-agent quota in acquire_claim, in renew_claim both the TTL clamp (#373) '
368
+ 'and the quota-convergence clamp (#388), and in set_board_claim_policy the advisory -> inherit cap '
369
+ 'reset (#400). The advisory opt-out is owner-toggleable and re-toggleable, so it remains an escape '
370
+ 'from convergence — bounded since #400 by the TTL cap per cycle rather than unbounded. '
371
+ 'Operator-managed (service_role); enabled only after the prod lease-length census.';