@consiliency/agent-board-schema 1.45.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 +12 -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 +30 -3
- package/src/tooling.test.mjs +29 -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/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
|
@@ -572,6 +572,18 @@
|
|
|
572
572
|
{
|
|
573
573
|
"id": "20260925000100_agent_board_ownerless_renew_ttl_cap",
|
|
574
574
|
"sha256": "da312f43cabf07c6dfd10dbe2a119fb46f49f24155e21adce4f38e291a38103c"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
"id": "20260926000100_agent_board_ownerless_quota_census",
|
|
578
|
+
"sha256": "bbfabcb2d45cbcebd1e6e121032a1045e8406ea8763a784762a92d210e1e1043"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
"id": "20260928000100_agent_board_ownerless_quota_renew_convergence",
|
|
582
|
+
"sha256": "9982efff25378703a744187b15081b5cd53ab38fa6ac7d2950092a471780169e"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"id": "20260927000100_agent_board_fleet_domain_no_downgrade",
|
|
586
|
+
"sha256": "ec5c4d89aff2d6d77d44cb3d00c1096b39010c6064c23d72eb3c5c354f8a5e74"
|
|
575
587
|
}
|
|
576
588
|
]
|
|
577
589
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
-- #388 — the ownerless quota bounds NEW ACQUISITIONS but never CONVERGES an agent already over it.
|
|
2
|
+
--
|
|
3
|
+
-- THE PROPERTY, stated as the issue states it. `acquire_claim` enforces a per-agent ceiling of
|
|
4
|
+
-- `_claimgate_ownerless_quota()` (50) concurrent active file/tool/custom claims. `renew_claim`
|
|
5
|
+
-- does not, and #387 deliberately did not add it — that PR closed the TTL half only. So the quota
|
|
6
|
+
-- is effective against GROWTH and not against PERSISTENCE: an agent already over it renews every
|
|
7
|
+
-- one of its claims indefinitely and stays over, forever.
|
|
8
|
+
--
|
|
9
|
+
-- THREE WAYS AN AGENT IS ALREADY OVER, all real and none hypothetical:
|
|
10
|
+
-- 1. claims acquired while `ownerless_guards` was OFF — the flag defaults disabled and was
|
|
11
|
+
-- flipped in production only recently (#369), so everything accumulated before the flip is
|
|
12
|
+
-- grandfathered;
|
|
13
|
+
-- 2. claims acquired under a board that was `claim_policy = 'advisory'` and later set back to
|
|
14
|
+
-- `inherit` — the acquire-time guard is skipped for an advisory board (20260922000100), but
|
|
15
|
+
-- the resulting rows are counted by every LATER check;
|
|
16
|
+
-- 3. a quota value lowered after the fact.
|
|
17
|
+
--
|
|
18
|
+
-- ---------------------------------------------------------------------------
|
|
19
|
+
-- WHAT THIS MIGRATION IS, AND THE THREE THINGS IT DELIBERATELY IS NOT
|
|
20
|
+
-- ---------------------------------------------------------------------------
|
|
21
|
+
--
|
|
22
|
+
-- IT IS A READ-ONLY CENSUS. #388 step 3: "which agents are currently over quota on the live
|
|
23
|
+
-- board". Nothing here writes, blocks, clamps or refuses.
|
|
24
|
+
--
|
|
25
|
+
-- IT IS NOT ENFORCEMENT, and that is a decision rather than an omission. Acquire refusing costs
|
|
26
|
+
-- the caller nothing — it asked for a new claim and did not get it. Renew refusing would KILL A
|
|
27
|
+
-- LIVE LEASE the agent is currently relying on, and if an agent is 10 over, the reasonable
|
|
28
|
+
-- behaviour is not "fail 10 renews at random", it is a decision about WHICH claims lose (oldest
|
|
29
|
+
-- first? least recently touched? notify the agent?). That is a product question nobody has
|
|
30
|
+
-- answered. This is the instrument that informs it. Enforcement, if it is chosen, additionally
|
|
31
|
+
-- needs the agent-scoped `pg_advisory_xact_lock` acquire already takes plus a two-session
|
|
32
|
+
-- isolationtester case (#137's harness), because two concurrent renews must not both observe room.
|
|
33
|
+
--
|
|
34
|
+
-- IT IS NOT A SCRIPT, and the alternative was considered rather than skipped. A `scripts/*.mjs`
|
|
35
|
+
-- check with the house 0/1/2 exit contract answers a question about whatever database it is
|
|
36
|
+
-- pointed at; the population that matters is PRODUCTION, and the operator path to production is
|
|
37
|
+
-- `service_role` SQL through the Management API, where a view and a function are directly
|
|
38
|
+
-- readable and a Node script is not runnable at all. A CI invocation would examine an empty
|
|
39
|
+
-- freshly-built database and report "0 of 0" — true, and worthless. And an exit code is a GATE:
|
|
40
|
+
-- 1 means "fail the build", which presumes a policy about what an over-quota agent should cause
|
|
41
|
+
-- to happen. #388 explicitly defers that policy, so there is nothing yet for a non-zero exit to
|
|
42
|
+
-- mean. When the convergence policy exists, a script that gates on this view is the natural next
|
|
43
|
+
-- artifact; it is not this one.
|
|
44
|
+
--
|
|
45
|
+
-- ---------------------------------------------------------------------------
|
|
46
|
+
-- THE NUMERATOR IS ACQUIRE'S OWN COUNT PREDICATE, VERBATIM
|
|
47
|
+
-- ---------------------------------------------------------------------------
|
|
48
|
+
-- A census whose arithmetic differs from the enforcer's is a census about a different question,
|
|
49
|
+
-- and it would be silently wrong rather than visibly wrong. The count below is copied from the
|
|
50
|
+
-- quota block of the CURRENT `acquire_claim` (20260922000100 — note that migration re-creates it
|
|
51
|
+
-- from a `pg_get_functiondef` dump in UPPERCASE, so a lowercase grep for the definer finds
|
|
52
|
+
-- 20260914000100 and reads the wrong body):
|
|
53
|
+
--
|
|
54
|
+
-- where c.agent_id = v_actor_agent_id
|
|
55
|
+
-- and c.resource_kind in ('file', 'tool', 'custom')
|
|
56
|
+
-- and c.status = 'active'
|
|
57
|
+
-- and c.released_at is null
|
|
58
|
+
-- and c.lease_expires_at > now()
|
|
59
|
+
--
|
|
60
|
+
-- NO BOARD-POLICY FILTER, and this is the subtle one. The advisory board opt-out gates WHETHER
|
|
61
|
+
-- the guard runs for the acquisition being attempted; it does not exclude the resulting rows from
|
|
62
|
+
-- the count. So an agent's 60 claims count as 60 even if 20 of them were taken on advisory
|
|
63
|
+
-- boards. Filtering them out here would produce a smaller, friendlier, WRONG number that does not
|
|
64
|
+
-- predict what acquire does. Stated operationally, a row in this view with `acquire_blocked` true
|
|
65
|
+
-- means: this agent's next ownerless acquire, on an `inherit` board, with the guards on, raises
|
|
66
|
+
-- 54000.
|
|
67
|
+
--
|
|
68
|
+
-- `claims_on_advisory_boards` is reported as a SEPARATE column precisely because it is the thing
|
|
69
|
+
-- the convergence decision needs and the thing the headline count must not absorb — refusing a
|
|
70
|
+
-- claim that another board's own lock already granted is us breaking a promise, so an operator
|
|
71
|
+
-- weighing "which claims lose" needs to see that share without it distorting the ceiling test.
|
|
72
|
+
--
|
|
73
|
+
-- OVERAGE AND ACQUIRE_BLOCKED ARE DIFFERENT NUMBERS AND BOTH ARE REPORTED. Acquire refuses at
|
|
74
|
+
-- `count >= quota`, so an agent holding exactly 50 is already frozen while being over by nothing.
|
|
75
|
+
-- * `overage` = greatest(count - quota, 0) — how many claims exceed the ceiling. This is
|
|
76
|
+
-- the convergence question: what would have to be given up.
|
|
77
|
+
-- * `acquire_blocked` = count >= quota — acquire's refusal predicate, verbatim. This is the
|
|
78
|
+
-- "why can my agent not take a lock" question.
|
|
79
|
+
-- Collapsing them into one boolean would answer one of those two questions wrongly.
|
|
80
|
+
--
|
|
81
|
+
-- ---------------------------------------------------------------------------
|
|
82
|
+
-- THE DENOMINATOR IS NOT OPTIONAL
|
|
83
|
+
-- ---------------------------------------------------------------------------
|
|
84
|
+
-- The live answer today is expected to be ZERO agents over quota. A reader that emits "0
|
|
85
|
+
-- offenders" and nothing else is byte-identical whether it examined 400 agents or examined
|
|
86
|
+
-- nothing at all — which is exactly the defect #367 shipped a migration to close, where
|
|
87
|
+
-- `v_ops_presence_heartbeats` returned zero rows about an entirely unmonitored fleet and read as
|
|
88
|
+
-- perfect health. So `ownerless_quota_census()` always emits its population alongside its finding:
|
|
89
|
+
--
|
|
90
|
+
-- 0 over quota, of 12 agents holding ownerless claims, across 47 active agents
|
|
91
|
+
--
|
|
92
|
+
-- and it additionally emits `guards_enabled` and `quota`, so the output states the REGIME it
|
|
93
|
+
-- measured under. "0 over quota" with `guards_enabled = false` is a materially different fact
|
|
94
|
+
-- from the same number with the flag on, and a reader that reports the count without the flag
|
|
95
|
+
-- invites exactly the rule-5 misreading — a true statement about a moment, still asserting itself
|
|
96
|
+
-- after the moment moved.
|
|
97
|
+
--
|
|
98
|
+
-- ---------------------------------------------------------------------------
|
|
99
|
+
-- The view. `security_invoker` matching every other view in agent_board; the grant below is what
|
|
100
|
+
-- actually confines it, and the two are not redundant (see the grant comment).
|
|
101
|
+
-- ---------------------------------------------------------------------------
|
|
102
|
+
create or replace view agent_board.v_ownerless_claim_quota
|
|
103
|
+
with (security_invoker = true)
|
|
104
|
+
as
|
|
105
|
+
with holders as (
|
|
106
|
+
select
|
|
107
|
+
c.agent_id as agent_id,
|
|
108
|
+
count(*)::integer as active_ownerless_claims,
|
|
109
|
+
-- The advisory share, resolved per claim through its OWN thread. A claim with no thread, or a
|
|
110
|
+
-- thread with no board, resolves to 'inherit' by the helper's own contract — so absence never
|
|
111
|
+
-- reads as an opt-out here either.
|
|
112
|
+
count(*) filter (
|
|
113
|
+
where agent_board._claimgate_board_claim_policy(c.thread_id) = 'advisory'
|
|
114
|
+
)::integer as claims_on_advisory_boards,
|
|
115
|
+
count(*) filter (where c.resource_kind = 'file')::integer as file_claims,
|
|
116
|
+
count(*) filter (where c.resource_kind = 'tool')::integer as tool_claims,
|
|
117
|
+
count(*) filter (where c.resource_kind = 'custom')::integer as custom_claims,
|
|
118
|
+
min(c.created_at) as oldest_claim_at,
|
|
119
|
+
max(c.created_at) as newest_claim_at,
|
|
120
|
+
max(c.lease_expires_at) as latest_lease_expires_at
|
|
121
|
+
from agent_board.claims c
|
|
122
|
+
-- ACQUIRE'S PREDICATE, VERBATIM. Every conjunct is load-bearing and each has a near-miss row in
|
|
123
|
+
-- the pgTAP fixture: a released claim, an expired-status claim, a lapsed-lease claim, and an
|
|
124
|
+
-- owned-kind claim all sit next to the counted ones so that dropping any conjunct here fails a
|
|
125
|
+
-- named assertion rather than passing quietly.
|
|
126
|
+
where c.resource_kind in ('file', 'tool', 'custom')
|
|
127
|
+
and c.status = 'active'
|
|
128
|
+
and c.released_at is null
|
|
129
|
+
and c.lease_expires_at > now()
|
|
130
|
+
group by c.agent_id
|
|
131
|
+
)
|
|
132
|
+
select
|
|
133
|
+
h.agent_id,
|
|
134
|
+
-- Identity sufficient to ACT on the row without a second query: who to contact, and whether the
|
|
135
|
+
-- agent is even still in service. `status` is REPORTED, never filtered on — a suspended agent
|
|
136
|
+
-- holding 60 live leases is precisely the row an operator most needs to see, and a
|
|
137
|
+
-- `status = 'active'` filter here would hide it.
|
|
138
|
+
a.handle as agent_handle,
|
|
139
|
+
a.display_name as agent_display_name,
|
|
140
|
+
a.status as agent_status,
|
|
141
|
+
h.active_ownerless_claims,
|
|
142
|
+
agent_board._claimgate_ownerless_quota() as quota,
|
|
143
|
+
greatest(h.active_ownerless_claims - agent_board._claimgate_ownerless_quota(), 0)::integer
|
|
144
|
+
as overage,
|
|
145
|
+
(h.active_ownerless_claims >= agent_board._claimgate_ownerless_quota())
|
|
146
|
+
as acquire_blocked,
|
|
147
|
+
h.claims_on_advisory_boards,
|
|
148
|
+
h.file_claims,
|
|
149
|
+
h.tool_claims,
|
|
150
|
+
h.custom_claims,
|
|
151
|
+
h.oldest_claim_at,
|
|
152
|
+
h.newest_claim_at,
|
|
153
|
+
h.latest_lease_expires_at
|
|
154
|
+
from holders h
|
|
155
|
+
join agent_board.agents a on a.id = h.agent_id;
|
|
156
|
+
|
|
157
|
+
comment on view agent_board.v_ownerless_claim_quota is
|
|
158
|
+
'#388 read-only census: one row per agent HOLDING at least one active ownerless (file/tool/'
|
|
159
|
+
'custom) claim, with its count, the quota, and the overage. The count is acquire_claim''s own '
|
|
160
|
+
'quota predicate verbatim (kind in file/tool/custom, status active, released_at null, lease '
|
|
161
|
+
'unexpired) with NO board-policy filter, because the advisory opt-out gates whether the guard '
|
|
162
|
+
'RUNS for an acquisition, not whether the resulting rows COUNT — so acquire_blocked (count >= '
|
|
163
|
+
'quota) predicts acquire''s 54000 exactly. overage (count - quota, floored at 0) is the '
|
|
164
|
+
'different question of how many claims exceed the ceiling; an agent at exactly the quota is '
|
|
165
|
+
'blocked while over by nothing. claims_on_advisory_boards is reported separately because it '
|
|
166
|
+
'informs the convergence decision without distorting the ceiling test. agent_status is reported '
|
|
167
|
+
'and never filtered: a suspended agent holding live leases is the row that matters most. '
|
|
168
|
+
'REPORT-ONLY — nothing here refuses, clamps or converges anything; see #388 for why refusing a '
|
|
169
|
+
'renew is a product decision and not a clamp. Read agent_board.ownerless_quota_census() FIRST '
|
|
170
|
+
'for the denominator. security_invoker, service_role only.';
|
|
171
|
+
|
|
172
|
+
-- ---------------------------------------------------------------------------
|
|
173
|
+
-- The summary. Its only job is to make an empty result impossible to misread.
|
|
174
|
+
-- ---------------------------------------------------------------------------
|
|
175
|
+
create or replace function agent_board.ownerless_quota_census()
|
|
176
|
+
returns table(
|
|
177
|
+
guards_enabled boolean,
|
|
178
|
+
quota integer,
|
|
179
|
+
agents_active integer,
|
|
180
|
+
agents_holding_ownerless_claims integer,
|
|
181
|
+
agents_over_quota integer,
|
|
182
|
+
agents_acquire_blocked integer,
|
|
183
|
+
total_active_ownerless_claims integer,
|
|
184
|
+
total_overage integer
|
|
185
|
+
)
|
|
186
|
+
language sql
|
|
187
|
+
stable
|
|
188
|
+
security invoker
|
|
189
|
+
set search_path = agent_board, public
|
|
190
|
+
as $$
|
|
191
|
+
select
|
|
192
|
+
-- The REGIME first: a count without the flag that governs it is a rule-5 fact with no
|
|
193
|
+
-- expiry. "0 over quota" means something different when the guards are off.
|
|
194
|
+
agent_board._claimgate_ownerless_guards_enabled() as guards_enabled,
|
|
195
|
+
agent_board._claimgate_ownerless_quota() as quota,
|
|
196
|
+
-- The widest denominator: the fleet. Non-zero even when NOBODY holds an ownerless claim, so
|
|
197
|
+
-- "0 of 0" can still be distinguished from "0 of 0, of 0" — a database this reader cannot
|
|
198
|
+
-- see into at all.
|
|
199
|
+
(select count(*)::integer from agent_board.agents where status = 'active')
|
|
200
|
+
as agents_active,
|
|
201
|
+
-- The population actually examined. THIS is the number that makes a zero finding meaningful.
|
|
202
|
+
-- It is counted from the view directly and NOT derived from the offender set — deriving it
|
|
203
|
+
-- would make it 0 exactly when the finding is 0, which is the vacuous shape this exists to
|
|
204
|
+
-- prevent.
|
|
205
|
+
(select count(*)::integer from agent_board.v_ownerless_claim_quota)
|
|
206
|
+
as agents_holding_ownerless_claims,
|
|
207
|
+
(select count(*) filter (where overage > 0)::integer
|
|
208
|
+
from agent_board.v_ownerless_claim_quota) as agents_over_quota,
|
|
209
|
+
(select count(*) filter (where acquire_blocked)::integer
|
|
210
|
+
from agent_board.v_ownerless_claim_quota) as agents_acquire_blocked,
|
|
211
|
+
(select coalesce(sum(active_ownerless_claims), 0)::integer
|
|
212
|
+
from agent_board.v_ownerless_claim_quota) as total_active_ownerless_claims,
|
|
213
|
+
(select coalesce(sum(overage), 0)::integer
|
|
214
|
+
from agent_board.v_ownerless_claim_quota) as total_overage;
|
|
215
|
+
$$;
|
|
216
|
+
|
|
217
|
+
comment on function agent_board.ownerless_quota_census() is
|
|
218
|
+
'#388 denominator. Returns agents_holding_ownerless_claims and agents_active ALONGSIDE '
|
|
219
|
+
'agents_over_quota, so "0 over quota" can never be rendered as "all clear" — 0 over, of 0 '
|
|
220
|
+
'holders, across 47 active agents, is a reader that examined nothing, and it reads as one '
|
|
221
|
+
'(the #367 defect shape). Also emits guards_enabled and quota so the output states the regime '
|
|
222
|
+
'it measured under. agents_holding_ownerless_claims is counted from the view, NOT derived from '
|
|
223
|
+
'the offender set: a denominator that collapses to 0 exactly when the finding is 0 proves '
|
|
224
|
+
'nothing. agents_over_quota counts overage > 0; agents_acquire_blocked counts count >= quota, '
|
|
225
|
+
'which is acquire_claim''s refusal predicate and is the strictly larger set. '
|
|
226
|
+
'security invoker + stable: a projection of v_ownerless_claim_quota, inheriting the caller''s '
|
|
227
|
+
'RLS.';
|
|
228
|
+
|
|
229
|
+
-- ---------------------------------------------------------------------------
|
|
230
|
+
-- SERVICE_ROLE ONLY, and this is the most important statement in the file.
|
|
231
|
+
--
|
|
232
|
+
-- `security_invoker` does not make an `authenticated` grant safe here; it makes it dangerous, and
|
|
233
|
+
-- for the mirror-image reason #367 found. The RLS SELECT policy on `agent_board.claims` is:
|
|
234
|
+
--
|
|
235
|
+
-- agent_id = current_agent_id()
|
|
236
|
+
-- or exists (select 1 from tasks t where t.id = task_id
|
|
237
|
+
-- and (t.created_by_agent_id = current_agent_id()
|
|
238
|
+
-- or t.assigned_agent_id = current_agent_id()))
|
|
239
|
+
--
|
|
240
|
+
-- so an authenticated caller can see essentially ITS OWN claims. Read by that caller this view
|
|
241
|
+
-- returns one row — itself — and the census reports `agents_holding_ownerless_claims = 1`,
|
|
242
|
+
-- `agents_over_quota = 0`. Not an error, not an empty result: a confident, plausible, fleet-shaped
|
|
243
|
+
-- answer that is wrong, and wrong in the reassuring direction. Two agents would get two different
|
|
244
|
+
-- censuses with no way to tell which was right, and the one that mattered — the agent sitting at
|
|
245
|
+
-- 60 claims — would be invisible to everyone except itself.
|
|
246
|
+
--
|
|
247
|
+
-- #367 hit this as OVER-reporting (a LEFT JOIN turned "hidden from you" into an alarm). Here the
|
|
248
|
+
-- join is an inner aggregate, so it is UNDER-reporting, which is worse for a census whose whole
|
|
249
|
+
-- purpose is to find the outliers: over-reporting is noisy and gets investigated, under-reporting
|
|
250
|
+
-- is silence with a denominator attached to make it look diligent.
|
|
251
|
+
--
|
|
252
|
+
-- An authenticated agent cannot answer "who in the fleet is over quota" at all, because it cannot
|
|
253
|
+
-- see anyone else's claims. That is the RLS model working, not a gap here, and the honest response
|
|
254
|
+
-- is to withhold the surface rather than serve a per-caller answer that reads fleet-wide. Quota
|
|
255
|
+
-- convergence is an operations question and this is an operations surface.
|
|
256
|
+
--
|
|
257
|
+
-- `security_invoker` is kept anyway as defence in depth: if a future migration widens the grant,
|
|
258
|
+
-- rows are still RLS-filtered rather than leaked wholesale. OQ-8/OQ-9 pin both the view option and
|
|
259
|
+
-- the grant so widening either fails a test that says why.
|
|
260
|
+
-- ---------------------------------------------------------------------------
|
|
261
|
+
grant select on agent_board.v_ownerless_claim_quota to service_role;
|
|
262
|
+
|
|
263
|
+
-- NOT REDUNDANT — this is the AL-9b trap from #367, which arrives on a plain
|
|
264
|
+
-- `create or replace function` and not only on a DROP. A newly created function is granted
|
|
265
|
+
-- EXECUTE to PUBLIC by default, so `authenticated` inherits it through PUBLIC and granting only
|
|
266
|
+
-- service_role above would change nothing on its own.
|
|
267
|
+
revoke all on function agent_board.ownerless_quota_census() from public;
|
|
268
|
+
grant execute on function agent_board.ownerless_quota_census() to service_role;
|