@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.
- package/fixtures/already-applied.json +16 -0
- package/migrations/20260925000100_agent_board_ownerless_renew_ttl_cap.sql +166 -0
- package/migrations/20260926000100_agent_board_ownerless_quota_census.sql +268 -0
- package/migrations/20260927000100_agent_board_fleet_domain_no_downgrade.sql +393 -0
- package/migrations/20260928000100_agent_board_ownerless_quota_renew_convergence.sql +562 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/tooling.test.mjs +35 -2
- package/tests/agent_board_fleet_machine_hijack.test.sql +270 -1
- package/tests/agent_board_fleet_machine_reclaim.test.sql +28 -2
- package/tests/agent_board_ownerless_quota_census.test.sql +400 -0
- package/tests/agent_board_ownerless_renew_quota.test.sql +551 -0
- package/tests/agent_board_ownerless_renew_ttl.test.sql +226 -0
- package/tests/catalog/p_metadata_parameters.json +12 -0
- package/tests/isolation/expected/fleet_domain_no_downgrade_demote_race.out +447 -0
- package/tests/isolation/specs/fleet_domain_no_downgrade_demote_race.spec +292 -0
|
@@ -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;
|