@consiliency/agent-board-schema 1.44.0 → 1.48.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.
@@ -0,0 +1,551 @@
1
+ -- #388 — the ownerless quota must CONVERGE an agent already over it, not merely bound new
2
+ -- acquisitions. The renew-side guard: while the holder is over quota, a renew may not POSTPONE a
3
+ -- deadline. It still succeeds, the lease already held is preserved to the microsecond, and the only
4
+ -- thing withheld is more time — so each claim lapses at its own expiry, the count falls, and the
5
+ -- guard releases at the ceiling.
6
+ --
7
+ -- SCOPED, AND THE SCOPE IS ASSERTED HERE RATHER THAN PROMISED IN PROSE. That convergence holds
8
+ -- against an agent whose boards do not opt out. It does NOT hold against one whose board is flipped
9
+ -- to `claim_policy = 'advisory'` and back, which an owner may do at will and which an agent that
10
+ -- created its own board may do to itself. ORQ-11/12 perform that flip-flop and pin both halves: the
11
+ -- horizon survives the revert, and the census cannot see that it was taken.
12
+ --
13
+ -- ORQ-1 IS THE ASSERTION THIS FILE EXISTS FOR, and it is written as the attack #388 describes: an
14
+ -- agent that is already over the ceiling asks for +400 days, which before this migration it got,
15
+ -- and could get again forever. It asserts an EXACT resulting timestamp rather than an error,
16
+ -- because a raise is the design this deliberately did not choose (see the migration header; #387's
17
+ -- ORT-3 records the same reasoning for the TTL half and is likewise unchanged by this file).
18
+ --
19
+ -- ORQ-5 IS THE DISCRIMINATING TWIN AND THE HARDEST ONE TO GET RIGHT. A guard that froze EVERY
20
+ -- ownerless renew would satisfy ORQ-1 and destroy the feature, and the obvious twin — an agent well
21
+ -- under the quota renews normally, ORQ-6 — cannot see the mutation that actually threatens this
22
+ -- design: `> quota` weakened to `>= quota`, which is ACQUIRE's refusal predicate and reads as a
23
+ -- harmless unification. Under `>=` an agent sitting legitimately AT the ceiling is frozen and
24
+ -- walked down to 49 — a legal population shrunk rather than an illegal one converged. Only an agent
25
+ -- holding EXACTLY the quota can catch it, so this file builds one.
26
+ --
27
+ -- THE NEAR-MISS ROWS ARE ORQ-5's OTHER HALF. The AT agent is given four rows that differ from a
28
+ -- counted row in exactly one conjunct of the shared count predicate — a set released_at, a
29
+ -- non-active status, a lapsed lease, and an owned resource kind. Dropping any conjunct from
30
+ -- `_claimgate_active_ownerless_claims()` puts the AT agent at 51, freezes it, and fails ORQ-5 by
31
+ -- name. Without them the helper's predicate would be pinned only by the census's own fixture, in a
32
+ -- different file, against a different surface.
33
+ --
34
+ -- ORQ-13 IS THE FIXED POINT, and it is the assertion that separates this design from a guard that
35
+ -- latches. Release one claim, and the SAME claim that was frozen a moment ago extends normally.
36
+ -- A guard reading a constant, or reading a count captured once, passes everything above and fails
37
+ -- here.
38
+ --
39
+ -- WHY A NEW FILE RATHER THAN MORE OF agent_board_ownerless_renew_ttl.test.sql. That file's fixture
40
+ -- is one agent holding a handful of claims, and it flips the guard flag and the board policy
41
+ -- mid-file as its own assertions require. This property needs three agents, three boards, ~104
42
+ -- claim rows and an exact-51 population held stable across a dozen assertions; interleaving the
43
+ -- two fixtures would make
44
+ -- each file's flag state a function of the other's line order. The runner discovers `*.test.sql` by
45
+ -- scanning the directory (scripts/run-agent-board-schema-tests.sh), so a new file needs no
46
+ -- registration.
47
+ --
48
+ -- SCOPING. The guard flag is fleet-wide state shared with every other test, so this file SETS it
49
+ -- explicitly rather than inheriting whatever ran before — an inherited `false` would make every
50
+ -- assertion here pass vacuously against a completely unguarded function. Every count is keyed to
51
+ -- this file's agent ids for the same reason.
52
+ create extension if not exists pgtap with schema extensions;
53
+ set search_path = extensions, public, agent_board;
54
+
55
+ begin;
56
+
57
+ select plan(31);
58
+
59
+ -- ---------------------------------------------------------------------------
60
+ -- Fixture.
61
+ -- over — quota + 1 counted claims: filler (sized off the quota helper) + one per assertion
62
+ -- below, one of them on an ADVISORY board, one with NO thread, and one on a board the
63
+ -- over agent OWNS. Over by exactly 1, so ORQ-13's single release lands it exactly ON the
64
+ -- ceiling.
65
+ -- at — exactly quota counted claims, plus four NEAR-MISS rows (ORQ-5's other half).
66
+ -- under — three counted claims, one per ownerless kind.
67
+ -- Every counted row is inserted with an expiry expressed against now(), which is transaction-stable
68
+ -- — so every assertion below can demand an EXACT timestamp rather than a window.
69
+ -- ---------------------------------------------------------------------------
70
+ create temp table _orq (
71
+ sfx text,
72
+ over_agent uuid, at_agent uuid, under_agent uuid,
73
+ over_user uuid, at_user uuid, under_user uuid,
74
+ board uuid, thread uuid, adv_board uuid, adv_thread uuid,
75
+ esc_board uuid, esc_thread uuid
76
+ ) on commit drop;
77
+
78
+ do $$
79
+ declare
80
+ v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
81
+ v_over uuid := gen_random_uuid();
82
+ v_at uuid := gen_random_uuid();
83
+ v_under uuid := gen_random_uuid();
84
+ v_over_user uuid;
85
+ v_at_user uuid;
86
+ v_under_user uuid;
87
+ v_board uuid := gen_random_uuid();
88
+ v_thread uuid := gen_random_uuid();
89
+ v_adv_board uuid := gen_random_uuid();
90
+ v_adv_thread uuid := gen_random_uuid();
91
+ v_esc_board uuid;
92
+ v_esc_thread uuid := gen_random_uuid();
93
+ begin
94
+ -- All three agents renew AS THEMSELVES, so all three need an auth identity: renew_claim resolves
95
+ -- the actor from the session and refuses a token it does not own (42501).
96
+ v_over_user := public.ensure_github_user('orq-over-' || v_sfx, 'orq-over-' || v_sfx || '@example.test', 'orq-over-' || v_sfx, 'ORQ Over');
97
+ v_at_user := public.ensure_github_user('orq-at-' || v_sfx, 'orq-at-' || v_sfx || '@example.test', 'orq-at-' || v_sfx, 'ORQ At Quota');
98
+ v_under_user := public.ensure_github_user('orq-under-' || v_sfx, 'orq-under-' || v_sfx || '@example.test', 'orq-under-' || v_sfx, 'ORQ Under');
99
+
100
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
101
+ values (v_over, 'agent', 'orq-over-' || v_sfx, 'ORQ Over', 'active', v_over_user, v_over_user),
102
+ (v_at, 'agent', 'orq-at-' || v_sfx, 'ORQ At Quota', 'active', v_at_user, v_at_user),
103
+ (v_under, 'agent', 'orq-under-' || v_sfx, 'ORQ Under', 'active', v_under_user, v_under_user);
104
+
105
+ -- An INHERIT board (the default) and a permanently ADVISORY one, both inserted directly: these
106
+ -- two only need to EXIST in a given policy state, and BCP owns the subject of who may set one.
107
+ -- The third board below is different and is created through the RPC on purpose — ORQ-11 is about
108
+ -- an owner exercising that gate, so a direct insert would prove nothing there.
109
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, claim_policy)
110
+ values (v_board, 'orq-' || v_sfx, 'ORQ Board', v_over, 'inherit'),
111
+ (v_adv_board, 'orq-adv-' || v_sfx, 'ORQ Advisory Board', v_over, 'advisory');
112
+
113
+ -- A THIRD board, created through create_board BY THE OVER AGENT ITSELF rather than inserted.
114
+ -- That is load-bearing for ORQ-11/12: create_board seeds the creator's OWNER membership, and
115
+ -- set_board_claim_policy is gated on exactly that membership. So the escape hatch those
116
+ -- assertions document needs no accomplice — the agent that is over quota can be the owner who
117
+ -- opts its own board out. Inserting this board directly would seed no membership and would make
118
+ -- the escape look like it required a cooperating third party, which is a weaker and false claim.
119
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
120
+ perform set_config('request.jwt.claim.sub', v_over_user::text, true);
121
+ select board_id into v_esc_board
122
+ from agent_board.create_board(v_over, 'orq-esc-' || v_sfx, 'ORQ Escape Board', 'owner-toggled opt-out', '{}'::jsonb, 'private');
123
+
124
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
125
+ values (v_thread, 'coordination', v_over, 'ORQ thread', v_board),
126
+ (v_adv_thread, 'coordination', v_over, 'ORQ advisory thread', v_adv_board),
127
+ (v_esc_thread, 'coordination', v_over, 'ORQ escape thread', v_esc_board);
128
+
129
+ insert into _orq values (v_sfx, v_over, v_at, v_under, v_over_user, v_at_user, v_under_user,
130
+ v_board, v_thread, v_adv_board, v_adv_thread, v_esc_board, v_esc_thread);
131
+ end $$;
132
+
133
+ -- The guard must be ON or every assertion below passes against an unguarded function.
134
+ insert into agent_board.claimgate_enforcement_flags(scope_key, enabled)
135
+ values ('ownerless_guards', true)
136
+ on conflict (scope_key) do update set enabled = true;
137
+
138
+ -- OVER: quota + 1 counted claims. Six are named because an assertion drives each; the rest are
139
+ -- filler sized off the quota helper so this file does not carry a second copy of the number.
140
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
141
+ select (select over_agent from _orq), (select thread from _orq), 'file',
142
+ 'orq-over-filler-' || (select sfx from _orq) || '-' || g, now() + interval '10 days'
143
+ from generate_series(1, agent_board._claimgate_ownerless_quota() - 6) g;
144
+
145
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
146
+ values
147
+ -- ORQ-1: the headline freeze.
148
+ ((select over_agent from _orq), (select thread from _orq), 'file', 'orq-over-file-' || (select sfx from _orq), now() + interval '10 days'),
149
+ -- ORQ-4: a SHORTER ask must still be honoured, and ORQ-8 re-uses it once the flag is off.
150
+ ((select over_agent from _orq), (select thread from _orq), 'file', 'orq-over-shorten-' || (select sfx from _orq), now() + interval '10 days'),
151
+ -- ORQ-3: NO thread — must fall back to 'inherit' and stay governed, not escape by omission.
152
+ ((select over_agent from _orq), null, 'file', 'orq-over-nothread-' || (select sfx from _orq), now() + interval '10 days'),
153
+ -- ORQ-2: the other two kinds. ORQ-13 later re-uses the tool claim for the fixed point.
154
+ ((select over_agent from _orq), (select thread from _orq), 'tool', 'orq-over-tool-' || (select sfx from _orq), now() + interval '10 days'),
155
+ ((select over_agent from _orq), (select thread from _orq), 'custom', 'orq-over-custom-' || (select sfx from _orq), now() + interval '10 days'),
156
+ -- ORQ-9: on the ADVISORY board. COUNTED all the same — the opt-out gates whether a guard RUNS,
157
+ -- never whether the row counts — but not converged.
158
+ ((select over_agent from _orq), (select adv_thread from _orq), 'file', 'orq-over-adv-' || (select sfx from _orq), now() + interval '10 days'),
159
+ -- ORQ-11/12: on the board the OVER AGENT OWNS, so it can toggle the opt-out itself.
160
+ ((select over_agent from _orq), (select esc_thread from _orq), 'file', 'orq-over-escape-' || (select sfx from _orq), now() + interval '10 days');
161
+
162
+ -- AT: exactly quota counted claims, at a SHORTER horizon than the over agent's so ORQ-5 can ask
163
+ -- for MORE time and observe that it is granted. Asking for less would be satisfied by a frozen
164
+ -- function too, and would prove nothing.
165
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
166
+ select (select at_agent from _orq), (select thread from _orq), 'file',
167
+ 'orq-at-filler-' || (select sfx from _orq) || '-' || g, now() + interval '1 day'
168
+ from generate_series(1, agent_board._claimgate_ownerless_quota() - 1) g;
169
+
170
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
171
+ values ((select at_agent from _orq), (select thread from _orq), 'file', 'orq-at-probe-' || (select sfx from _orq), now() + interval '1 day');
172
+
173
+ -- AT: four NEAR-MISS rows. Each differs from a counted row in EXACTLY ONE conjunct of
174
+ -- _claimgate_active_ownerless_claims(), so dropping that conjunct puts this agent at quota + 1,
175
+ -- freezes it, and fails ORQ-5 naming itself. A near-miss differing in two dimensions would survive
176
+ -- the mutation and prove nothing.
177
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, status, released_at, lease_expires_at)
178
+ values
179
+ -- released_at is null
180
+ ((select at_agent from _orq), (select thread from _orq), 'file', 'orq-at-nm-released-' || (select sfx from _orq), 'active', now(), now() + interval '1 day'),
181
+ -- status = 'active'
182
+ ((select at_agent from _orq), (select thread from _orq), 'file', 'orq-at-nm-status-' || (select sfx from _orq), 'expired', null, now() + interval '1 day'),
183
+ -- lease_expires_at > now()
184
+ ((select at_agent from _orq), (select thread from _orq), 'file', 'orq-at-nm-lease-' || (select sfx from _orq), 'active', null, now() - interval '1 hour'),
185
+ -- resource_kind in ('file','tool','custom') — an OWNED kind, governed by the fleet gate and not
186
+ -- by this quota at all.
187
+ ((select at_agent from _orq), (select thread from _orq), 'machine', 'orq-at-nm-kind-' || (select sfx from _orq), 'active', null, now() + interval '1 day');
188
+
189
+ -- UNDER: three counted claims, one per ownerless kind.
190
+ insert into agent_board.claims(agent_id, thread_id, resource_kind, resource_key, lease_expires_at)
191
+ values
192
+ ((select under_agent from _orq), (select thread from _orq), 'file', 'orq-under-probe-' || (select sfx from _orq), now() + interval '1 day'),
193
+ ((select under_agent from _orq), (select thread from _orq), 'tool', 'orq-under-tool-' || (select sfx from _orq), now() + interval '1 day'),
194
+ ((select under_agent from _orq), (select thread from _orq), 'custom', 'orq-under-custom-' || (select sfx from _orq), now() + interval '1 day');
195
+
196
+ -- The lease tokens, by role. Captured once so every assertion below names a claim rather than
197
+ -- re-deriving it from a key that a canonicalising renew is allowed to rewrite.
198
+ create temp table _tok (name text primary key, tok uuid) on commit drop;
199
+ insert into _tok
200
+ select replace(split_part(c.resource_key, '-' || (select sfx from _orq), 1), 'orq-', ''), c.lease_token
201
+ from agent_board.claims c
202
+ where c.resource_key in (
203
+ 'orq-over-file-' || (select sfx from _orq),
204
+ 'orq-over-shorten-' || (select sfx from _orq),
205
+ 'orq-over-nothread-' || (select sfx from _orq),
206
+ 'orq-over-tool-' || (select sfx from _orq),
207
+ 'orq-over-custom-' || (select sfx from _orq),
208
+ 'orq-over-adv-' || (select sfx from _orq),
209
+ 'orq-over-escape-' || (select sfx from _orq),
210
+ 'orq-at-probe-' || (select sfx from _orq),
211
+ 'orq-under-probe-' || (select sfx from _orq));
212
+
213
+ -- Act as the OVER agent for ORQ-1..4 and ORQ-9.
214
+ do $$
215
+ begin
216
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
217
+ perform set_config('request.jwt.claim.sub', (select over_user from _orq)::text, true);
218
+ end $$;
219
+
220
+ -- ---------------------------------------------------------------------------
221
+ -- ORQ-1: THE POINT. An agent already over the ceiling asks for +400 days. Before this migration it
222
+ -- got +400 days and could repeat that forever; #387's TTL cap alone would give it +30 days, which
223
+ -- still never converges. It gets back exactly the lease it already held.
224
+ --
225
+ -- lives_ok, not a `do $$ perform … $$` block: the mutation this assertion most needs to catch is a
226
+ -- "hardening" into a raise, and a raise inside a do-block ABORTS the transaction and truncates the
227
+ -- run instead of failing here by name (#113).
228
+ -- ---------------------------------------------------------------------------
229
+ select lives_ok(
230
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
231
+ (select tok from _tok where name = 'over-file')::text, (now() + interval '400 days')::text),
232
+ 'ORQ-1a: an over-quota renew still SUCCEEDS — convergence by refusal would kill live work, which is the design #387 rejected and this file does not resurrect');
233
+
234
+ select is(
235
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-file')),
236
+ now() + interval '10 days',
237
+ 'ORQ-1 THE POINT: the over-quota renew did not POSTPONE anything — the lease it already held comes back to the microsecond, so the claim lapses on its own schedule and the agent converges');
238
+
239
+ -- ---------------------------------------------------------------------------
240
+ -- ORQ-2: THE OTHER TWO KINDS. Every other assertion here drives `file`; in the TTL half, dropping
241
+ -- 'tool' or 'custom' from the kind list passed the entire battery (ORT-9). Exercised behaviourally,
242
+ -- because the defect is "this kind is not converged" and only a timestamp observation sees it.
243
+ -- ---------------------------------------------------------------------------
244
+ select lives_ok(
245
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
246
+ (select tok from _tok where name = 'over-tool')::text, (now() + interval '400 days')::text),
247
+ 'ORQ-2a: a tool renew by an over-quota agent succeeds');
248
+
249
+ select lives_ok(
250
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
251
+ (select tok from _tok where name = 'over-custom')::text, (now() + interval '400 days')::text),
252
+ 'ORQ-2b: a custom renew by an over-quota agent succeeds');
253
+
254
+ select is(
255
+ (select string_agg(c.resource_kind || '=' || (c.lease_expires_at = now() + interval '10 days')::text, ', ' order by c.resource_kind)
256
+ from agent_board.claims c
257
+ where c.lease_token in (select tok from _tok where name in ('over-tool', 'over-custom'))),
258
+ 'custom=true, tool=true',
259
+ 'ORQ-2: tool and custom are frozen at renew too — dropping either from the kind list leaves two thirds of the class unconverged and passes every file-driven assertion here');
260
+
261
+ -- ---------------------------------------------------------------------------
262
+ -- ORQ-3: A CLAIM WITH NO THREAD IS STILL CONVERGED. The board opt-out is resolved from the claim's
263
+ -- thread, so a claim with none must fall back to 'inherit' rather than escaping by omission —
264
+ -- otherwise `and v_thread_id is not null` in the guard buys an agent an unbounded renewal.
265
+ -- ---------------------------------------------------------------------------
266
+ select lives_ok(
267
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
268
+ (select tok from _tok where name = 'over-nothread')::text, (now() + interval '400 days')::text),
269
+ 'ORQ-3a: a threadless renew by an over-quota agent succeeds');
270
+
271
+ select is(
272
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-nothread')),
273
+ now() + interval '10 days',
274
+ 'ORQ-3: a claim with NO thread is still frozen — the board opt-out cannot be obtained by omitting the thread');
275
+
276
+ -- ---------------------------------------------------------------------------
277
+ -- ORQ-4: SHORTENING IS STILL ALLOWED. The clamp is least(), not an assignment: an over-quota agent
278
+ -- asking for LESS time is converging voluntarily and must be taken at its word. An assignment of
279
+ -- the current expiry passes ORQ-1 and silently lengthens this lease from one hour back to ten days.
280
+ -- ---------------------------------------------------------------------------
281
+ select lives_ok(
282
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
283
+ (select tok from _tok where name = 'over-shorten')::text, (now() + interval '1 hour')::text),
284
+ 'ORQ-4a: an over-quota agent may still renew for a SHORTER term');
285
+
286
+ select is(
287
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-shorten')),
288
+ now() + interval '1 hour',
289
+ 'ORQ-4: a shorter ask is honoured exactly — the guard is least(requested, held), not a rewrite to the held value, which would hand back time the agent tried to give up');
290
+
291
+ -- ---------------------------------------------------------------------------
292
+ -- ORQ-5: THE DISCRIMINATING TWIN. An agent at EXACTLY the quota is at the ceiling, not over it, and
293
+ -- renews normally, and it fails when `> quota` is weakened to `>= quota` — acquire's own refusal
294
+ -- predicate, which reads as a harmless unification and would walk a legal population down to 49.
295
+ --
296
+ -- NOT the only assertion that catches that, and an earlier version of this comment said it was.
297
+ -- The mutation was measured: `>` -> `>=` fails ORQ-5 AND ORQ-13, because ORQ-13 releases one claim
298
+ -- to put the agent exactly at the ceiling and so builds the same boundary fixture a second time.
299
+ -- Being pinned twice is stronger than the claim was; naming the wrong count still sends the next
300
+ -- reader to the wrong test.
301
+ --
302
+ -- It is also where the four near-miss rows pay off: drop any conjunct from the shared count and
303
+ -- this agent is at 51 and frozen.
304
+ -- ---------------------------------------------------------------------------
305
+ do $$
306
+ begin
307
+ perform set_config('request.jwt.claim.sub', (select at_user from _orq)::text, true);
308
+ end $$;
309
+
310
+ select lives_ok(
311
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
312
+ (select tok from _tok where name = 'at-probe')::text, (now() + interval '5 days')::text),
313
+ 'ORQ-5a: an agent holding exactly the quota can renew');
314
+
315
+ select is(
316
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'at-probe')),
317
+ now() + interval '5 days',
318
+ 'ORQ-5 THE TWIN: an agent at EXACTLY the quota is EXTENDED in full (+1d becomes +5d) — >= instead of > freezes it, and every other assertion here still passes');
319
+
320
+ -- ---------------------------------------------------------------------------
321
+ -- ORQ-6: the ordinary twin. An agent well under the quota is untouched, so a guard that converged
322
+ -- by freezing everything fails here as well as at ORQ-5.
323
+ -- ---------------------------------------------------------------------------
324
+ do $$
325
+ begin
326
+ perform set_config('request.jwt.claim.sub', (select under_user from _orq)::text, true);
327
+ end $$;
328
+
329
+ select lives_ok(
330
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
331
+ (select tok from _tok where name = 'under-probe')::text, (now() + interval '5 days')::text),
332
+ 'ORQ-6a: an under-quota renew succeeds');
333
+
334
+ select is(
335
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'under-probe')),
336
+ now() + interval '5 days',
337
+ 'ORQ-6: an agent well under the quota renews with its horizon untouched — a guard that froze every ownerless renew would satisfy ORQ-1 and destroy the feature');
338
+
339
+ -- ---------------------------------------------------------------------------
340
+ -- ORQ-7: the operator carve-out. service_role is exempt here exactly as it is on the TTL cap and at
341
+ -- acquire, so an operator can still extend an over-quota agent's lease deliberately — and, since
342
+ -- service_role skips the TTL cap too, lands the full +400 days rather than the 30-day horizon.
343
+ -- ---------------------------------------------------------------------------
344
+ do $$
345
+ begin
346
+ perform set_config('request.jwt.claims', '', true);
347
+ perform set_config('request.jwt.claim.sub', '', true);
348
+ perform set_config('request.jwt.claim.role', '', true);
349
+ end $$;
350
+
351
+ select lives_ok(
352
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
353
+ (select tok from _tok where name = 'over-file')::text, (now() + interval '400 days')::text),
354
+ 'ORQ-7a: service_role can renew an over-quota agent''s claim');
355
+
356
+ select is(
357
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-file')),
358
+ now() + interval '400 days',
359
+ 'ORQ-7: service_role is EXEMPT from the convergence clamp, the same carve-out it has at acquire and on the TTL cap — an operator extension is not frozen');
360
+
361
+ -- ---------------------------------------------------------------------------
362
+ -- ORQ-8: one flag governs every ownerless guard. With ownerless_guards off, convergence stops —
363
+ -- so the operator switch means the same thing on both write paths and for both the cap and the
364
+ -- quota, rather than half-latching.
365
+ -- ---------------------------------------------------------------------------
366
+ do $$
367
+ begin
368
+ update agent_board.claimgate_enforcement_flags set enabled = false where scope_key = 'ownerless_guards';
369
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
370
+ perform set_config('request.jwt.claim.sub', (select over_user from _orq)::text, true);
371
+ end $$;
372
+
373
+ select lives_ok(
374
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
375
+ (select tok from _tok where name = 'over-shorten')::text, (now() + interval '400 days')::text),
376
+ 'ORQ-8a: with the guards off an over-quota renew succeeds');
377
+
378
+ select is(
379
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-shorten')),
380
+ now() + interval '400 days',
381
+ 'ORQ-8: with ownerless_guards OFF the renew is NOT frozen (and not TTL-clamped) — one flag governs the quota clamp exactly as it governs the cap');
382
+
383
+ -- ---------------------------------------------------------------------------
384
+ -- ORQ-9: the board opt-out reaches the quota clamp too. A board coordinating its own locking
385
+ -- (claim_policy='advisory') asked us not to enforce, and converging its claims would break that
386
+ -- promise — refusing to extend a lease someone else's mutex already granted. The row still COUNTS
387
+ -- toward the ceiling, which is why the census reports claims_on_advisory_boards separately: it is
388
+ -- exactly the share this guard cannot reach.
389
+ -- ---------------------------------------------------------------------------
390
+ do $$
391
+ begin
392
+ update agent_board.claimgate_enforcement_flags set enabled = true where scope_key = 'ownerless_guards';
393
+ end $$;
394
+
395
+ select lives_ok(
396
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
397
+ (select tok from _tok where name = 'over-adv')::text, (now() + interval '400 days')::text),
398
+ 'ORQ-9a: an advisory-board renew by an over-quota agent succeeds');
399
+
400
+ select is(
401
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-adv')),
402
+ now() + interval '400 days',
403
+ 'ORQ-9: a claim on a claim_policy=advisory board is NOT frozen — the opt-out reaches the quota clamp exactly as it reaches the TTL cap (both are skipped, hence the full +400 days), which is the same bug one layer down had it been acquire-only');
404
+
405
+ -- ---------------------------------------------------------------------------
406
+ -- ORQ-10: THE CENSUS AND THE ENFORCER AGREE. #393's view keeps its own copy of the count predicate
407
+ -- deliberately — its four near-miss rows are what make each conjunct of the READER individually
408
+ -- mutation-visible, and routing its headline through the helper would erase that. So the two are
409
+ -- pinned EQUAL here instead, which is the property that actually matters: a census whose arithmetic
410
+ -- differs from the enforcer's is silently answering a different question.
411
+ -- ---------------------------------------------------------------------------
412
+ select is(
413
+ (select string_agg(l.label || '=' || v.active_ownerless_claims || '/' || agent_board._claimgate_active_ownerless_claims(v.agent_id), ', ' order by l.label)
414
+ from agent_board.v_ownerless_claim_quota v
415
+ join (values ((select over_agent from _orq), 'over'),
416
+ ((select at_agent from _orq), 'at'),
417
+ ((select under_agent from _orq), 'under')) as l(id, label) on l.id = v.agent_id),
418
+ format('at=%s/%s, over=%s/%s, under=3/3',
419
+ agent_board._claimgate_ownerless_quota(), agent_board._claimgate_ownerless_quota(),
420
+ agent_board._claimgate_ownerless_quota() + 1, agent_board._claimgate_ownerless_quota() + 1),
421
+ 'ORQ-10: the #393 census view and the shared enforcement count agree on all three fixture agents — the two predicates are separate text and this is what stops them drifting');
422
+
423
+ -- ---------------------------------------------------------------------------
424
+ -- ORQ-11/12: THE ESCAPE HATCH. THIS ASSERTION DOCUMENTS A LIMITATION ON PURPOSE — it is not an
425
+ -- accident and it is not a defect being hidden. Read the migration header first; this is the
426
+ -- executable half of the paragraph that scopes the convergence claim.
427
+ --
428
+ -- The advisory carve-out is resolved at RENEW TIME against the board's CURRENT policy, and
429
+ -- set_board_claim_policy is owner-settable. So an owner can flip to 'advisory', let the over-quota
430
+ -- agent renew every claim to the full horizon, and flip back — repeatedly, before expiry. The
431
+ -- monotone clamp guarantees convergence against an agent whose boards do not opt out; it guarantees
432
+ -- NOTHING against one whose board owner cooperates, and the flip-flop can be repeated forever.
433
+ --
434
+ -- AND THE AGENT CAN BE ITS OWN OWNER, which is why the fixture builds this board through
435
+ -- create_board rather than inserting it. create_board seeds the creator's owner membership and
436
+ -- set_board_claim_policy is gated on exactly that, so no accomplice is required — an agent that
437
+ -- creates a board holds the switch that exempts its own claims. That is the strong form of the
438
+ -- limitation, and asserting the weak form (a cooperating third party) would understate it.
439
+ --
440
+ -- The carve-out is NOT removed in response. A board coordinating through someone else's mutex asked
441
+ -- us not to enforce, and refusing to extend a lease that mutex granted would break the promise the
442
+ -- opt-out makes. The exemption is deliberate; what was wrong was the CLAIM made about it, which is
443
+ -- corrected in prose and pinned here so the correction cannot silently rot (rule 5).
444
+ -- ---------------------------------------------------------------------------
445
+ select lives_ok(
446
+ format($$ select * from agent_board.set_board_claim_policy(%L::uuid, 'advisory') $$,
447
+ (select esc_board from _orq)::text),
448
+ 'ORQ-11a: the OVER-QUOTA AGENT ITSELF may opt its own board out — create_board made it the owner, and set_board_claim_policy is gated on owner membership, so the escape needs no accomplice');
449
+
450
+ select lives_ok(
451
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
452
+ (select tok from _tok where name = 'over-escape')::text, (now() + interval '400 days')::text),
453
+ 'ORQ-11b: and during that window the over-quota renew takes the FULL horizon');
454
+
455
+ select lives_ok(
456
+ format($$ select * from agent_board.set_board_claim_policy(%L::uuid, 'inherit') $$,
457
+ (select esc_board from _orq)::text),
458
+ 'ORQ-11c: the board is flipped straight back to inherit');
459
+
460
+ select is(
461
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-escape')),
462
+ now() + interval '400 days',
463
+ 'ORQ-11 THE ESCAPE HATCH: reverting to inherit does NOT take the horizon back — the +400 day lease survives the revert, so an owner who flips advisory/inherit before each expiry keeps an over-quota agent over quota forever. Convergence is guaranteed only against an agent whose boards do not opt out');
464
+
465
+ -- The second half, kept HERE rather than in the census file. The escape and its invisibility are
466
+ -- one defect: the census resolves board policy at QUERY time (OQ-29's subject), so once the board
467
+ -- is back to 'inherit' this claim contributes ZERO to claims_on_advisory_boards while the overage
468
+ -- it produced is still counted. An operator reading the census sees an agent over quota with no
469
+ -- advisory share — the exact shape that says "this one is a squatter, act on it" — and the column
470
+ -- built to explain the exemption reports none. Splitting the two halves across two files would put
471
+ -- the escape and the reason it is invisible where a future edit to one cannot see the other.
472
+ select is(
473
+ (select 'advisory=' || v.claims_on_advisory_boards || ', overage=' || v.overage || ', blocked=' || v.acquire_blocked::text
474
+ from agent_board.v_ownerless_claim_quota v where v.agent_id = (select over_agent from _orq)),
475
+ 'advisory=1, overage=1, blocked=true',
476
+ 'ORQ-12: after the revert the escaped claim is INVISIBLE to the census — claims_on_advisory_boards counts only the one still on an advisory board, not the one that used the exemption and gave it back, so the overage it bought reads as unexplained squatting');
477
+
478
+ -- ---------------------------------------------------------------------------
479
+ -- ORQ-13: THE FIXED POINT. Release ONE claim, and the agent is at the ceiling rather than over it —
480
+ -- the same tool claim that was frozen at ORQ-2 now extends in full. A guard that latched, read a
481
+ -- constant, or captured the count once passes everything above and fails here.
482
+ -- ---------------------------------------------------------------------------
483
+ update agent_board.claims
484
+ set status = 'released', released_at = now()
485
+ where resource_key = 'orq-over-filler-' || (select sfx from _orq) || '-1';
486
+
487
+ select lives_ok(
488
+ format($$ select * from agent_board.renew_claim(%L::uuid, %L::timestamptz) $$,
489
+ (select tok from _tok where name = 'over-tool')::text, (now() + interval '20 days')::text),
490
+ 'ORQ-13a: after releasing one claim the renew succeeds');
491
+
492
+ select is(
493
+ (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-tool')),
494
+ now() + interval '20 days',
495
+ 'ORQ-13 THE FIXED POINT: one release puts the agent AT the ceiling and the previously frozen claim extends in full (+10d becomes +20d) — convergence stops at the quota, it does not run to zero');
496
+
497
+ -- ---------------------------------------------------------------------------
498
+ -- ORQ-14: CLASS, catalog-derived. Which functions can move a claim's lease_expires_at? #388's own
499
+ -- text asserts "acquire_claim/renew_claim are the only SQL writers" — a recorded fact, which rule 5
500
+ -- says is a claim about the moment it was written. Derived here instead, so a third writer that
501
+ -- needs this guard fails a test and NAMES ITSELF rather than relying on someone remembering.
502
+ -- ---------------------------------------------------------------------------
503
+ select is(
504
+ (select string_agg(p.proname, ', ' order by p.proname)
505
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
506
+ where n.nspname = 'agent_board'
507
+ and agent_board_test.stripped_body(p.oid, true) ~ '(lease_expires_at\s*=[^=]|insert into agent_board\.claims\s*\([^)]*lease_expires_at)'),
508
+ 'acquire_claim, renew_claim',
509
+ 'ORQ-14 CLASS: acquire_claim and renew_claim are the ONLY functions that write a claim lease — a new one names itself here instead of quietly inheriting an unconverged renewal path');
510
+
511
+ -- ---------------------------------------------------------------------------
512
+ -- ORQ-15/14: CLASS, one definition of the count. Three hand-copies of a predicate across two
513
+ -- enforcers and a census is rule 1's own failure shape, so acquire's inline count was extracted
514
+ -- into _claimgate_active_ownerless_claims() and both paths now ask it. ORQ-15 is the exact-set scan
515
+ -- (a fourth site that re-derives the count names itself); ORQ-16 is the positive pair, scored on
516
+ -- BOTH functions because asserting only that renew uses the helper would pass if acquire silently
517
+ -- stopped using it.
518
+ -- ---------------------------------------------------------------------------
519
+ select is(
520
+ (select string_agg(p.proname, ', ' order by p.proname)
521
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
522
+ where n.nspname = 'agent_board'
523
+ and agent_board_test.stripped_body(p.oid, true) ~ 'count\(\*\)[^;]*from agent_board\.claims'),
524
+ '_claimgate_active_ownerless_claims',
525
+ 'ORQ-15 CLASS: exactly ONE function counts rows of agent_board.claims — an enforcement path that re-derives the ownerless count instead of asking the helper fails here and names itself');
526
+
527
+ select is(
528
+ (select string_agg(p.proname || '=' || (agent_board_test.stripped_body(p.oid, true) ~ '_claimgate_active_ownerless_claims')::text, ', ' order by p.proname)
529
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
530
+ where n.nspname = 'agent_board' and p.proname in ('acquire_claim', 'renew_claim')),
531
+ 'acquire_claim=true, renew_claim=true',
532
+ 'ORQ-16 CLASS: BOTH enforcement paths ask the shared count — scored on the pair, because pinning renew alone would pass if acquire reverted to its own copy');
533
+
534
+ -- ---------------------------------------------------------------------------
535
+ -- ORQ-17: the helper is service_role-only, and DELIBERATELY narrower than its sibling
536
+ -- _claimgate_ownerless_quota() (granted to authenticated). That one returns a constant; this one
537
+ -- reads agent_board.claims under security invoker, so an authenticated caller would receive its own
538
+ -- RLS-filtered count wearing the name of the fleet-wide quota count — a confident, plausible, wrong
539
+ -- number, which is the #367 shape the census header describes one layer down. Asserted positively
540
+ -- on all three roles rather than by the absence of a grant.
541
+ -- ---------------------------------------------------------------------------
542
+ select is(
543
+ format('anon=%s, authenticated=%s, service_role=%s',
544
+ has_function_privilege('anon', 'agent_board._claimgate_active_ownerless_claims(uuid)', 'execute')::text,
545
+ has_function_privilege('authenticated', 'agent_board._claimgate_active_ownerless_claims(uuid)', 'execute')::text,
546
+ has_function_privilege('service_role', 'agent_board._claimgate_active_ownerless_claims(uuid)', 'execute')::text),
547
+ 'anon=false, authenticated=false, service_role=true',
548
+ 'ORQ-17: the shared count is service_role only — it reads claims under security invoker, so an authenticated grant would hand a caller its own RLS-filtered count under a fleet-wide name');
549
+
550
+ select * from finish();
551
+ rollback;