@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
|
@@ -568,6 +568,22 @@
|
|
|
568
568
|
{
|
|
569
569
|
"id": "20260924000100_agent_board_agent_liveness_denominator",
|
|
570
570
|
"sha256": "deb58ead6c4d7bbda686b85191d5eb2f2771ae2a405933ffbfbaabd7ad0117d9"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
"id": "20260925000100_agent_board_ownerless_renew_ttl_cap",
|
|
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"
|
|
571
587
|
}
|
|
572
588
|
]
|
|
573
589
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
-- #373 — the ownerless TTL cap was ACQUIRE-ONLY, so the bound it advertises did not exist.
|
|
2
|
+
--
|
|
3
|
+
-- THE GAP, re-derived from the catalog rather than taken from the issue:
|
|
4
|
+
--
|
|
5
|
+
-- acquire_claim guard=true ttl_cap=true quota=true
|
|
6
|
+
-- renew_claim guard=false ttl_cap=false quota=false
|
|
7
|
+
-- release_claim guard=false ttl_cap=false quota=false
|
|
8
|
+
--
|
|
9
|
+
-- `_claimgate_ownerless_ttl_cap()` (30 days) bounded an ownerless `file`/`tool`/`custom` lease at
|
|
10
|
+
-- ACQUIRE and nowhere else. An agent could take a 30-day lease and renew it indefinitely, past the
|
|
11
|
+
-- cap, forever. The duration bound was advertised and unenforced.
|
|
12
|
+
--
|
|
13
|
+
-- A COMMENT THAT DESCRIBED BEHAVIOUR THE CODE DID NOT HAVE. `_claimgate_ownerless_ttl_cap()` says
|
|
14
|
+
-- ENFORCE "extends the horizon to renew for all non-task kinds". For the ownerless kinds it
|
|
15
|
+
-- demonstrably did not — the ENFORCE rewrite (20260825000100) re-authorises and clamps only
|
|
16
|
+
-- machine/repository/repo_file and says so in its own text. That is the shape AGENTS.md rule 5
|
|
17
|
+
-- names: an intention written as a fact, which reads as true and is never re-checked.
|
|
18
|
+
--
|
|
19
|
+
-- CLAMP, NOT REJECT — and this differs from acquire deliberately. `acquire_claim` RAISES on an
|
|
20
|
+
-- over-cap lease (22023): nothing is lost, the caller simply asks for less. A renew that raised
|
|
21
|
+
-- would instead KILL A LIVE LEASE the agent is currently relying on, converting a length problem
|
|
22
|
+
-- into an availability incident. Renew already resolves this the same way for the fleet-gated
|
|
23
|
+
-- kinds — clamp to the horizon, and let an over-long legacy lease reconcile at its next renew --
|
|
24
|
+
-- so the ownerless clamp mirrors renew's own idiom rather than acquire's.
|
|
25
|
+
--
|
|
26
|
+
-- SAME FOUR CONDITIONS AS ACQUIRE, so one flag governs both paths and a board that opted out of
|
|
27
|
+
-- our locking is not clamped by it either:
|
|
28
|
+
-- the kind is ownerless · not service_role · _claimgate_ownerless_guards_enabled()
|
|
29
|
+
-- · the claim's board is not claim_policy='advisory'
|
|
30
|
+
--
|
|
31
|
+
-- WHAT THIS DOES NOT DO. The per-agent quota (50 concurrent ownerless claims) is untouched. It was
|
|
32
|
+
-- never defeated by this gap — renewing does not increase an agent's claim count — and all three
|
|
33
|
+
-- board seats on #369 converged on that independently. This closes the TTL half only.
|
|
34
|
+
create or replace function agent_board.renew_claim(p_lease_token uuid, p_lease_expires_at timestamptz)
|
|
35
|
+
returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamptz, released_at timestamptz, created_at timestamptz, updated_at timestamptz)
|
|
36
|
+
language plpgsql
|
|
37
|
+
security definer
|
|
38
|
+
set search_path = agent_board, public
|
|
39
|
+
as $$
|
|
40
|
+
declare
|
|
41
|
+
v_actor_agent_id uuid;
|
|
42
|
+
v_kind text;
|
|
43
|
+
v_key text;
|
|
44
|
+
v_thread_id uuid;
|
|
45
|
+
v_effective_expires timestamptz;
|
|
46
|
+
begin
|
|
47
|
+
if not agent_board._is_service_role() then
|
|
48
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
|
|
49
|
+
if not exists (
|
|
50
|
+
select 1
|
|
51
|
+
from agent_board.claims c
|
|
52
|
+
where c.lease_token = p_lease_token
|
|
53
|
+
and c.agent_id = v_actor_agent_id
|
|
54
|
+
) then
|
|
55
|
+
raise exception 'claim not accessible to current agent' using errcode = '42501';
|
|
56
|
+
end if;
|
|
57
|
+
end if;
|
|
58
|
+
|
|
59
|
+
-- CLAIMGATE/ENFORCE (#133): re-authorize + horizon for the predicate-gated kinds. Read the
|
|
60
|
+
-- live lease's resource identity (SECURITY DEFINER, so RLS does not hide it). A dead/expired
|
|
61
|
+
-- lease yields a null kind and the UPDATE below matches nothing (preserves prior behavior).
|
|
62
|
+
select c.resource_kind, c.resource_key, c.thread_id
|
|
63
|
+
into v_kind, v_key, v_thread_id
|
|
64
|
+
from agent_board.claims c
|
|
65
|
+
where c.lease_token = p_lease_token
|
|
66
|
+
and c.status = 'active'
|
|
67
|
+
and c.released_at is null
|
|
68
|
+
and c.lease_expires_at > now();
|
|
69
|
+
|
|
70
|
+
-- RE-AUTH: bound revocation latency to one lease term. Uses the SAME session wrapper acquire
|
|
71
|
+
-- consults (_can_claim_resource -> _can_claim_resource_for(current_agent_id())), so service_role
|
|
72
|
+
-- keeps its carve-out and acquire+renew share ONE per-kind flag (a mutation splitting the flags
|
|
73
|
+
-- fails a test). Task-renew is EXCLUDED (ownership-only, ratified supersession).
|
|
74
|
+
if v_kind in ('machine', 'repository', 'repo_file')
|
|
75
|
+
and agent_board._claimgate_kind_enforced(v_kind) then
|
|
76
|
+
if not agent_board._can_claim_resource(v_kind, agent_board._canonical_claim_key(v_kind, v_key)) then
|
|
77
|
+
raise exception 'not authorized to renew this % claim (fleet authority revoked)', v_kind
|
|
78
|
+
using errcode = '42501';
|
|
79
|
+
end if;
|
|
80
|
+
end if;
|
|
81
|
+
|
|
82
|
+
-- HORIZON: clamp the renewed lease to now()+horizon for an ENFORCED fleet-gated kind — this is
|
|
83
|
+
-- where an existing overlong lease reconciles (no bulk-clamp migration; the horizon clamps it
|
|
84
|
+
-- at its next renew). service_role exempt. Same per-kind flag as acquire.
|
|
85
|
+
v_effective_expires := p_lease_expires_at;
|
|
86
|
+
if v_kind in ('machine', 'repository', 'repo_file')
|
|
87
|
+
and not agent_board._is_service_role()
|
|
88
|
+
and agent_board._claimgate_kind_enforced(v_kind)
|
|
89
|
+
and p_lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() then
|
|
90
|
+
v_effective_expires := now() + agent_board._claimgate_max_lease_horizon();
|
|
91
|
+
end if;
|
|
92
|
+
|
|
93
|
+
-- #373 OWNERLESS TTL CAP AT RENEW. The half that was missing: without this an agent renews a
|
|
94
|
+
-- file/tool/custom lease past the 30-day cap indefinitely and the bound is decorative.
|
|
95
|
+
--
|
|
96
|
+
-- Clamps rather than raises, unlike acquire. A raise here would kill a lease the agent is
|
|
97
|
+
-- actively holding; shortening it costs one more renew. Same resolution the fleet-kind horizon
|
|
98
|
+
-- clamp above already applies, and the same place an over-long legacy lease reconciles.
|
|
99
|
+
--
|
|
100
|
+
-- The four conditions are acquire's, verbatim, so ONE operator flag governs both paths and a
|
|
101
|
+
-- board that declined our locking (claim_policy='advisory') is not clamped by it either. The
|
|
102
|
+
-- thread comes from the LIVE CLAIM, never from a caller argument -- a caller-supplied thread
|
|
103
|
+
-- would let an agent choose which board's policy is consulted.
|
|
104
|
+
if v_kind in ('file', 'tool', 'custom')
|
|
105
|
+
and not agent_board._is_service_role()
|
|
106
|
+
and agent_board._claimgate_ownerless_guards_enabled()
|
|
107
|
+
and agent_board._claimgate_board_claim_policy(v_thread_id) <> 'advisory'
|
|
108
|
+
and v_effective_expires > now() + agent_board._claimgate_ownerless_ttl_cap() then
|
|
109
|
+
v_effective_expires := now() + agent_board._claimgate_ownerless_ttl_cap();
|
|
110
|
+
end if;
|
|
111
|
+
|
|
112
|
+
-- CLAIMKEY (#133) MUST-FIX 1: re-canonicalize the STORED key as the lease is renewed, but
|
|
113
|
+
-- NEVER propagate a uniqueness collision. A check-then-act guard is NOT race-safe: two aliased
|
|
114
|
+
-- legacy rows renewed concurrently at READ COMMITTED can both observe the canonical key free
|
|
115
|
+
-- and one loses on claims_active_resource_unique (23505). So attempt the canonicalization and,
|
|
116
|
+
-- on unique_violation (deterministic alias OR concurrent race), retry ONCE keeping the CURRENT
|
|
117
|
+
-- key. That makes renew non-raising UNCONDITIONALLY (true normalize-only); the surviving legacy
|
|
118
|
+
-- key is the deferred dual-key alias the ENFORCE phase closes.
|
|
119
|
+
begin
|
|
120
|
+
return query
|
|
121
|
+
update agent_board.claims c
|
|
122
|
+
set lease_expires_at = v_effective_expires,
|
|
123
|
+
resource_key = agent_board._canonical_claim_key(c.resource_kind, c.resource_key),
|
|
124
|
+
updated_at = now()
|
|
125
|
+
where c.lease_token = p_lease_token
|
|
126
|
+
and c.status = 'active'
|
|
127
|
+
and c.released_at is null
|
|
128
|
+
and c.lease_expires_at > now()
|
|
129
|
+
returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
|
|
130
|
+
exception when unique_violation then
|
|
131
|
+
-- the canonical key is already held (by a committed twin or a concurrent renew/acquire):
|
|
132
|
+
-- renew the lease keeping the current key, aliasing deferred to ENFORCE. Only the key change
|
|
133
|
+
-- can raise unique_violation (the lease/timestamp columns are unconstrained), so the retry
|
|
134
|
+
-- cannot itself collide.
|
|
135
|
+
return query
|
|
136
|
+
update agent_board.claims c
|
|
137
|
+
set lease_expires_at = v_effective_expires,
|
|
138
|
+
updated_at = now()
|
|
139
|
+
where c.lease_token = p_lease_token
|
|
140
|
+
and c.status = 'active'
|
|
141
|
+
and c.released_at is null
|
|
142
|
+
and c.lease_expires_at > now()
|
|
143
|
+
returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
|
|
144
|
+
end;
|
|
145
|
+
end;
|
|
146
|
+
$$;
|
|
147
|
+
|
|
148
|
+
-- The comment #373 called out as describing behaviour the code did not have. It now does.
|
|
149
|
+
comment on function agent_board._claimgate_ownerless_ttl_cap() is
|
|
150
|
+
'Max lease horizon for an ownerless (file/tool/custom) claim: 30 days. Enforced at BOTH acquire '
|
|
151
|
+
'(which RAISES 22023 on an over-cap request) and renew (which CLAMPS, because raising would kill '
|
|
152
|
+
'a live lease the agent is relying on). #373 closed the renew half; before that this comment '
|
|
153
|
+
'claimed a horizon renew did not apply and a lease could be renewed past the cap forever. '
|
|
154
|
+
'Governed by _claimgate_ownerless_guards_enabled() on both paths, and skipped for a board whose '
|
|
155
|
+
'claim_policy is advisory.';
|
|
156
|
+
|
|
157
|
+
-- Second stale comment, raised by a review seat: the flags table said this scope_key gates the
|
|
158
|
+
-- ownerless guards "in acquire_claim", which was true when written and is not now. Same rule-5
|
|
159
|
+
-- shape as the helper comment above -- a recorded fact that keeps asserting itself after the code
|
|
160
|
+
-- moved. Corrected here rather than left for the next reader to disbelieve.
|
|
161
|
+
comment on table agent_board.claimgate_enforcement_flags is
|
|
162
|
+
'CLAIMGATE per-scope enforcement flags. Absent row => disabled (SHADOW default: blocks nothing). '
|
|
163
|
+
'scope_key ''ownerless_guards'' gates the ownerless (file/tool/custom) guards on BOTH write '
|
|
164
|
+
'paths: the TTL cap + per-agent quota in acquire_claim, and the TTL clamp in renew_claim (#373 '
|
|
165
|
+
'-- before that the cap was acquire-only and a lease could be renewed past it forever). '
|
|
166
|
+
'Operator-managed (service_role); enabled only after the prod lease-length census.';
|
|
@@ -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;
|