@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,562 @@
|
|
|
1
|
+
-- #388 — the ownerless quota bounds NEW ACQUISITIONS but never CONVERGES an agent already over it.
|
|
2
|
+
--
|
|
3
|
+
-- THE PROPERTY. `acquire_claim` refuses at `count >= _claimgate_ownerless_quota()` (50) concurrent
|
|
4
|
+
-- active ownerless (file/tool/custom) claims. `renew_claim` has never asked the question at all. So
|
|
5
|
+
-- the ceiling binds GROWTH and not PERSISTENCE: an agent already over it renews every one of its
|
|
6
|
+
-- claims indefinitely and stays over, forever. #393 shipped the census that REPORTS those agents
|
|
7
|
+
-- (`v_ownerless_claim_quota`, `ownerless_quota_census()`); nothing CONVERGES them. This does.
|
|
8
|
+
--
|
|
9
|
+
-- ===========================================================================
|
|
10
|
+
-- THE DESIGN: A MONOTONE, NON-EXTENDING CLAMP. "While over quota, a renew may not postpone a
|
|
11
|
+
-- deadline." The lease the agent already holds is untouched to the microsecond; the renew succeeds
|
|
12
|
+
-- and returns the row; the only thing withheld is MORE TIME.
|
|
13
|
+
-- ===========================================================================
|
|
14
|
+
--
|
|
15
|
+
-- WHY NOT A RAISE, and this is the option a reader reaches for first. #387's test file states the
|
|
16
|
+
-- rationale and ORT-3 asserts it by checking a resulting TIMESTAMP rather than an error: acquire
|
|
17
|
+
-- RAISES because nothing is lost by asking again, but a renew that raised would KILL A LEASE THE
|
|
18
|
+
-- AGENT IS ACTIVELY HOLDING, converting a quantity problem into an availability incident. That
|
|
19
|
+
-- reasoning is undisturbed by #388 — being over quota is exactly the state in which the agent has
|
|
20
|
+
-- the most in-flight work to lose — so no existing assertion changes contract here.
|
|
21
|
+
--
|
|
22
|
+
-- WHY NOT THE OBVIOUS CLAMP — a short grace horizon (renew still succeeds, but only to
|
|
23
|
+
-- `now() + 1 hour`, so leases lapse quickly). IT DOES NOT CONVERGE, and the counterexample is
|
|
24
|
+
-- one line: an agent at 60 simply renews all 60 every hour and stays at 60 forever. Convergence
|
|
25
|
+
-- under that design is contingent on the over-quota agent CHOOSING to abandon claims, which is
|
|
26
|
+
-- the one party whose cooperation the guard exists not to require. It also behaves worst for the
|
|
27
|
+
-- careful agent (a 29-day lease is cut to an hour) and not at all for the diligent one. Rejected
|
|
28
|
+
-- on the counterexample, recorded here so the next reader does not re-propose it as "simpler".
|
|
29
|
+
--
|
|
30
|
+
-- WHY THE MONOTONE CLAMP CONVERGES, AND AGAINST WHOM. Every deadline is FIXED at the moment the
|
|
31
|
+
-- agent goes over. No renew can move one later, so each claim lapses at its own already-set expiry,
|
|
32
|
+
-- the count falls monotonically, and the guard releases the instant the count is back at the
|
|
33
|
+
-- ceiling. The agent keeps every second it already had — up to the full 30-day ownerless cap —
|
|
34
|
+
-- which is a longer and fairer grace than any fixed window, and it is per claim rather than uniform.
|
|
35
|
+
--
|
|
36
|
+
-- THAT SENTENCE IS SCOPED, and an earlier version of this header stated it without the scope, which
|
|
37
|
+
-- was WRONG. It holds against an agent whose boards do not opt out. It does NOT hold against an
|
|
38
|
+
-- agent with a cooperating board owner, and #399's panel is right that this is the material
|
|
39
|
+
-- limitation:
|
|
40
|
+
--
|
|
41
|
+
-- THE OWNER-TOGGLE ESCAPE. The advisory carve-out below is resolved at RENEW TIME against the
|
|
42
|
+
-- board's CURRENT policy, and `set_board_claim_policy` is owner-settable. So an owner can flip
|
|
43
|
+
-- the board to 'advisory', let the over-quota agent renew every claim to the full horizon, flip
|
|
44
|
+
-- back to 'inherit', and repeat before the next expiry. Nothing here takes the granted horizon
|
|
45
|
+
-- back — the clamp only refuses to EXTEND, it never shortens — so the agent stays over quota for
|
|
46
|
+
-- as long as the owner keeps flipping. NO ACCOMPLICE IS REQUIRED: `create_board` seeds the
|
|
47
|
+
-- creator's owner membership and that membership is exactly what the policy gate tests, so an
|
|
48
|
+
-- agent that created a board holds the switch that exempts its own claims. ORQ-11 performs the
|
|
49
|
+
-- flip-flop through the real RPCs and pins it.
|
|
50
|
+
--
|
|
51
|
+
-- AND IT IS INVISIBLE TO THE CENSUS, which is the half worth recording. #393 resolves board
|
|
52
|
+
-- policy at QUERY time, so once the board is back to 'inherit' the escaped claim contributes ZERO
|
|
53
|
+
-- to `claims_on_advisory_boards` while the overage it bought is still counted. An operator sees
|
|
54
|
+
-- an agent over quota with no advisory share — the shape that reads as plain squatting — and the
|
|
55
|
+
-- column built to explain the exemption reports none of it. ORQ-12 pins that too.
|
|
56
|
+
--
|
|
57
|
+
-- THE CARVE-OUT IS NOT REMOVED IN RESPONSE, and that is a decision rather than an oversight. A board
|
|
58
|
+
-- coordinating through someone else's mutex asked us not to enforce; refusing to extend a lease that
|
|
59
|
+
-- mutex already granted would break the promise the opt-out makes, which is the whole reason the
|
|
60
|
+
-- opt-out exists. Making the exemption ACQUIRE-time was also rejected: nothing records the policy in
|
|
61
|
+
-- force when a claim was taken — that absence is #393's own documented gap — and inventing a policy
|
|
62
|
+
-- history here would be a second, unreviewed mechanism. What was defective was the CLAIM, not the
|
|
63
|
+
-- code, so the claim is scoped and the limitation is pinned by a test rather than left in prose.
|
|
64
|
+
--
|
|
65
|
+
-- THE REST OF THE CONVERGENCE BOUND, STATED HONESTLY rather than as "unconditional". The horizon is
|
|
66
|
+
-- THE LONGEST LEASE THE AGENT CURRENTLY HOLDS. A 400-day lease grandfathered from before the flag
|
|
67
|
+
-- flip (#369) lapses at ITS OWN expiry and not sooner — a renew-side guard cannot touch a claim
|
|
68
|
+
-- whose holder never renews it, and this one does not pretend to. `service_role` is outside it too,
|
|
69
|
+
-- by the operator carve-out. An agent whose entire overage sits on advisory boards never converges,
|
|
70
|
+
-- which is why the census reports `claims_on_advisory_boards` as its own column: it is precisely
|
|
71
|
+
-- the share this guard cannot reach while the board stays advisory.
|
|
72
|
+
--
|
|
73
|
+
-- `> quota`, NOT `>= quota`, AND THE DIFFERENCE IS A REAL AGENT. `>=` is ACQUIRE's refusal
|
|
74
|
+
-- predicate, and acquire refusing at 50 is what makes 50 the largest population reachable through
|
|
75
|
+
-- it. Reusing `>=` here would freeze an agent sitting legitimately AT the ceiling and walk it down
|
|
76
|
+
-- to 49 — shrinking a legal population rather than converging an illegal one. The predicate used is
|
|
77
|
+
-- the census's own `overage > 0` / `agents_over_quota`, so the fixed point is exactly the ceiling
|
|
78
|
+
-- and the two surfaces answer with one arithmetic.
|
|
79
|
+
--
|
|
80
|
+
-- WHICH ASSERTIONS PIN THAT, corrected by MEASURING rather than by reasoning about the fixtures.
|
|
81
|
+
-- An earlier draft of this header said "ORQ-2 is the assertion that fails on `>=`, and it is the
|
|
82
|
+
-- only one that can: it needs an agent at EXACTLY 50, which no other fixture has." Both halves
|
|
83
|
+
-- were wrong, and the mutation says so:
|
|
84
|
+
--
|
|
85
|
+
-- `>` -> `>=` ran=31, not ok on ORQ-5 AND ORQ-13
|
|
86
|
+
-- remove the least() clamp ran=31, not ok on ORQ-1, ORQ-2, ORQ-3
|
|
87
|
+
--
|
|
88
|
+
-- So ORQ-2 pins the CLAMP, not the boundary, and the boundary is pinned TWICE. The "only one that
|
|
89
|
+
-- can" reasoning was self-refuting: ORQ-13 is the fixed point, and its own text says it "puts the
|
|
90
|
+
-- agent AT the ceiling" — precisely the fixture the header claimed no other case had. Being pinned
|
|
91
|
+
-- by two assertions is stronger than the claim, but a header that names the wrong assertion sends
|
|
92
|
+
-- the next reader to the wrong test when this breaks, so it is corrected rather than left as a
|
|
93
|
+
-- harmless understatement.
|
|
94
|
+
--
|
|
95
|
+
-- NO `pg_advisory_xact_lock`, AND THIS DEVIATES FROM #388 STEP 2 ON PURPOSE. That step prescribes
|
|
96
|
+
-- acquire's agent-scoped lock plus a two-session isolationtester case "so two concurrent renews
|
|
97
|
+
-- cannot both observe room". That requirement belongs to an ADMIT design — one where a renew
|
|
98
|
+
-- CONSUMES a slot and two concurrent renews could consume the same one. This design admits
|
|
99
|
+
-- nothing. The row being renewed is required by the UPDATE's own WHERE (`status = 'active' and
|
|
100
|
+
-- released_at is null and lease_expires_at > now()`) to be one of the counted rows, so the count is
|
|
101
|
+
-- INVARIANT under renew: renew inserts nothing and can only shorten. Two concurrent renews by the
|
|
102
|
+
-- same agent therefore either both freeze or both pass, and either way the population is unchanged.
|
|
103
|
+
-- The only stale read available is "saw 51 while a concurrent release made it 50", whose entire
|
|
104
|
+
-- consequence is one renew that did not extend and which the next renew extends. Acquire keeps its
|
|
105
|
+
-- lock, because acquire COUNTS THEN INSERTS and that genuinely races.
|
|
106
|
+
--
|
|
107
|
+
-- THE CARVE-OUTS ARE ACQUIRE'S FOUR, VERBATIM, so one operator flag governs every ownerless guard
|
|
108
|
+
-- on both write paths and a board coordinating its own locking is not converged by ours:
|
|
109
|
+
-- the kind is ownerless · not service_role · _claimgate_ownerless_guards_enabled()
|
|
110
|
+
-- · the claim's board is not claim_policy = 'advisory'
|
|
111
|
+
-- The agent id and the thread come from the LIVE CLAIM, never from a caller argument — a
|
|
112
|
+
-- caller-supplied thread would let an agent choose which board's policy is consulted.
|
|
113
|
+
--
|
|
114
|
+
-- ONE DEFINITION OF THE COUNT, NOT THREE. `_claimgate_active_ownerless_claims()` below is extracted
|
|
115
|
+
-- from acquire's inline predicate and acquire is re-created to call it, so the two ENFORCEMENT
|
|
116
|
+
-- paths cannot drift. The census view keeps its own copy DELIBERATELY: OQ-6's four near-miss rows
|
|
117
|
+
-- are what make each conjunct of the reader's predicate individually mutation-visible, and routing
|
|
118
|
+
-- the view's headline through the helper would make that mutation invisible. The two are pinned
|
|
119
|
+
-- EQUAL behaviourally instead (ORQ-13), which is the property that actually matters.
|
|
120
|
+
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
-- The count. Acquire's quota predicate, moved here unchanged, and now the single definition both
|
|
123
|
+
-- enforcement paths ask.
|
|
124
|
+
-- ---------------------------------------------------------------------------
|
|
125
|
+
create or replace function agent_board._claimgate_active_ownerless_claims(p_agent_id uuid)
|
|
126
|
+
returns integer
|
|
127
|
+
language sql
|
|
128
|
+
stable
|
|
129
|
+
set search_path = ''
|
|
130
|
+
as $$
|
|
131
|
+
select count(*)::integer
|
|
132
|
+
from agent_board.claims c
|
|
133
|
+
where c.agent_id = p_agent_id
|
|
134
|
+
and c.resource_kind in ('file', 'tool', 'custom')
|
|
135
|
+
and c.status = 'active'
|
|
136
|
+
and c.released_at is null
|
|
137
|
+
and c.lease_expires_at > now();
|
|
138
|
+
$$;
|
|
139
|
+
|
|
140
|
+
-- SERVICE_ROLE ONLY, and NARROWER than the sibling `_claimgate_ownerless_quota()` (which is granted
|
|
141
|
+
-- to `authenticated`) on purpose. That one returns a constant and tells a caller nothing. This one
|
|
142
|
+
-- reads `agent_board.claims`, and it is `security invoker` — so an `authenticated` caller would get
|
|
143
|
+
-- its OWN RLS-filtered count back, wearing the name of the fleet-wide quota count. That is the
|
|
144
|
+
-- census's own #367-shape trap one layer down: a confident, plausible, wrong number. The two
|
|
145
|
+
-- SECURITY DEFINER callers do not need the grant (a definer executes as its owner), so withholding
|
|
146
|
+
-- it costs the enforcement paths nothing.
|
|
147
|
+
revoke all on function agent_board._claimgate_active_ownerless_claims(uuid) from public, anon;
|
|
148
|
+
grant execute on function agent_board._claimgate_active_ownerless_claims(uuid) to service_role;
|
|
149
|
+
|
|
150
|
+
comment on function agent_board._claimgate_active_ownerless_claims(uuid) is
|
|
151
|
+
'#388: THE single definition of an agent''s active ownerless (file/tool/custom) claim count — '
|
|
152
|
+
'acquire_claim''s quota predicate, extracted verbatim (kind in file/tool/custom, status active, '
|
|
153
|
+
'released_at null, lease unexpired). Called by acquire_claim (which refuses at >= '
|
|
154
|
+
'_claimgate_ownerless_quota()) and by renew_claim (which stops EXTENDING at > quota, so an '
|
|
155
|
+
'over-quota agent converges to the ceiling UNLESS its board opts out — a board owner can flip '
|
|
156
|
+
'claim_policy to advisory and back indefinitely, and an agent that created a board is its owner, '
|
|
157
|
+
'so convergence is guaranteed only against an agent whose boards do not opt out, #399). NO '
|
|
158
|
+
'board-policy filter here: the advisory opt-out gates '
|
|
159
|
+
'whether a guard RUNS, not whether the resulting rows COUNT. service_role only, and deliberately '
|
|
160
|
+
'narrower than _claimgate_ownerless_quota(): this reads claims under security invoker, so an '
|
|
161
|
+
'authenticated caller would receive its own RLS-filtered count under a fleet-wide name.';
|
|
162
|
+
|
|
163
|
+
-- ---------------------------------------------------------------------------
|
|
164
|
+
-- acquire_claim, re-created from the 20260922000100 pg_get_functiondef dump with exactly one hunk
|
|
165
|
+
-- changed: the inline count is now the helper call. CREATE OR REPLACE with an unchanged signature,
|
|
166
|
+
-- so the ACL and the COMMENT survive.
|
|
167
|
+
-- ---------------------------------------------------------------------------
|
|
168
|
+
CREATE OR REPLACE FUNCTION agent_board.acquire_claim(p_agent_id uuid, p_resource_kind text, p_resource_key text, p_lease_expires_at timestamp with time zone, p_task_id uuid DEFAULT NULL::uuid, p_thread_id uuid DEFAULT NULL::uuid, p_metadata jsonb DEFAULT '{}'::jsonb)
|
|
169
|
+
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 timestamp with time zone, released_at timestamp with time zone, created_at timestamp with time zone, updated_at timestamp with time zone)
|
|
170
|
+
LANGUAGE plpgsql
|
|
171
|
+
SECURITY DEFINER
|
|
172
|
+
SET search_path TO 'agent_board', 'public'
|
|
173
|
+
AS $function$
|
|
174
|
+
declare
|
|
175
|
+
v_thread_id uuid;
|
|
176
|
+
v_claim_id uuid;
|
|
177
|
+
v_actor_agent_id uuid;
|
|
178
|
+
v_shadow_authorized boolean;
|
|
179
|
+
v_ownerless boolean;
|
|
180
|
+
v_active_ownerless int;
|
|
181
|
+
begin
|
|
182
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
|
|
183
|
+
|
|
184
|
+
if nullif(trim(p_resource_key), '') is null then
|
|
185
|
+
raise exception 'resource key is required' using errcode = '22023';
|
|
186
|
+
end if;
|
|
187
|
+
|
|
188
|
+
-- CLAIMKEY (#133): normalize the resource key to its canonical, fleet-fingerprint-carrying
|
|
189
|
+
-- form BEFORE any lookup/insert, so the row stored (and the uniqueness index contended)
|
|
190
|
+
-- carry canonical identity. NORMALIZE-ONLY — this never rejects (rejection is ENFORCE).
|
|
191
|
+
p_resource_key := agent_board._canonical_claim_key(p_resource_kind, p_resource_key);
|
|
192
|
+
|
|
193
|
+
if p_task_id is not null then
|
|
194
|
+
select tasks.thread_id into v_thread_id from agent_board.tasks where id = p_task_id;
|
|
195
|
+
if v_thread_id is null then
|
|
196
|
+
raise exception 'task not found' using errcode = 'P0002';
|
|
197
|
+
end if;
|
|
198
|
+
if not agent_board._current_agent_can_access_task(p_task_id) then
|
|
199
|
+
raise exception 'task not accessible to current agent' using errcode = '42501';
|
|
200
|
+
end if;
|
|
201
|
+
else
|
|
202
|
+
v_thread_id := p_thread_id;
|
|
203
|
+
end if;
|
|
204
|
+
|
|
205
|
+
if v_thread_id is not null and not agent_board._current_agent_can_access_thread(v_thread_id) then
|
|
206
|
+
raise exception 'thread not accessible to current agent' using errcode = '42501';
|
|
207
|
+
end if;
|
|
208
|
+
|
|
209
|
+
-- message-board#291: the SUBJECT gate. The checks above answer "may this SESSION touch the
|
|
210
|
+
-- task/thread" and are short-circuited true for service_role; this one answers "does the
|
|
211
|
+
-- agent the claim is ABOUT already have access". Without it a service_role caller could name
|
|
212
|
+
-- any agent, and the resulting claims row -- itself an access disjunct, and a PERMANENT one
|
|
213
|
+
-- -- would MINT thread access (and, through the thread disjunct of
|
|
214
|
+
-- _current_agent_can_access_task, task access) rather than record a lock on something the
|
|
215
|
+
-- subject could already see.
|
|
216
|
+
--
|
|
217
|
+
-- UNCONDITIONAL IN *WHO*. DO NOT "optimize" this into `if _is_service_role() and not ...` or
|
|
218
|
+
-- `if v_actor_agent_id is distinct from agent_board.current_agent_id()`. For an authenticated
|
|
219
|
+
-- caller v_actor_agent_id IS current_agent_id() and the session check above already
|
|
220
|
+
-- established this same expression, so the redundancy is PROVABLE rather than suspected; it
|
|
221
|
+
-- is the price of an invariant this function states for itself instead of borrowing from
|
|
222
|
+
-- _resolved_actor_agent_id continuing to raise on a subject mismatch.
|
|
223
|
+
--
|
|
224
|
+
-- CONDITIONAL ON *THREAD PRESENCE*, which is not a loophole. A claim bound to neither a task
|
|
225
|
+
-- nor a thread stores thread_id = null, is a disjunct of nothing and mints no access.
|
|
226
|
+
-- _agent_can_access_thread answers false for a null thread, so gating unconditionally would
|
|
227
|
+
-- refuse every machine / repository / repo_file / ownerless claim in the fleet. This covers
|
|
228
|
+
-- exactly the claims that are keys.
|
|
229
|
+
--
|
|
230
|
+
-- PLACED AFTER THE TASK LOOKUP so a nonexistent task still answers P0002, not 42501 -- that
|
|
231
|
+
-- ordering IS test-pinned. Placed BEFORE the sweep only to match the predicate-before-side-
|
|
232
|
+
-- effects convention CLAIMGATE/ENFORCE states below; it is NOT what makes a denial free of
|
|
233
|
+
-- side effects. `raise exception` unwinds the whole call, so a refused caller changes no state
|
|
234
|
+
-- wherever this sits -- measured, by moving the gate below the sweep and observing the test
|
|
235
|
+
-- file stay entirely green.
|
|
236
|
+
if v_thread_id is not null
|
|
237
|
+
and not agent_board._agent_can_access_thread(v_thread_id, v_actor_agent_id) then
|
|
238
|
+
raise exception 'claim subject has no access to thread' using errcode = '42501';
|
|
239
|
+
end if;
|
|
240
|
+
|
|
241
|
+
-- OBSGATE-4 (#127): a claim on a TASK resource is authorized on the LOCKED task = the task
|
|
242
|
+
-- named by RESOURCE_KEY (resource_kind='task'). The active-resource uniqueness is on
|
|
243
|
+
-- (resource_kind, resource_key), so that is the task a squat contends for — gating resource_key
|
|
244
|
+
-- (NOT p_task_id, which only associates the claim/event) prevents locking task B's key while
|
|
245
|
+
-- authorizing on an unrelated task A passed as p_task_id. A non-uuid 'task' key cannot collide
|
|
246
|
+
-- with a uuid-keyed legitimate claim.
|
|
247
|
+
--
|
|
248
|
+
-- The composition MIRRORS append_task_event's: ACCESS then MUTATE. mutate alone is
|
|
249
|
+
-- insufficient — _current_agent_can_mutate_task admits any non-observer board member via
|
|
250
|
+
-- _current_agent_can_post_to_thread (board send-role), which is NOT thread access; on a
|
|
251
|
+
-- RESTRICTED thread a board member lacking thread access would pass mutate but must still be
|
|
252
|
+
-- denied (they cannot access the task). Requiring access too closes that gap and matches the
|
|
253
|
+
-- authorization a task-BOUND acquire already gets through append_task_event.
|
|
254
|
+
if p_resource_kind = 'task'
|
|
255
|
+
and p_resource_key ~ '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' then
|
|
256
|
+
if not agent_board._current_agent_can_access_task(p_resource_key::uuid) then
|
|
257
|
+
raise exception 'task not accessible to current agent' using errcode = '42501';
|
|
258
|
+
end if;
|
|
259
|
+
if not agent_board._current_agent_can_mutate_task(p_resource_key::uuid) then
|
|
260
|
+
raise exception 'claiming a task resource requires a non-observer board role, or task assignment/creation'
|
|
261
|
+
using errcode = '42501';
|
|
262
|
+
end if;
|
|
263
|
+
end if;
|
|
264
|
+
|
|
265
|
+
-- CLAIMGATE/SHADOW (#133): compute the fleet-graph authority verdict BEFORE the sweep (so
|
|
266
|
+
-- ENFORCE can flip this same point fail-closed with no observable side effect leaking to an
|
|
267
|
+
-- unauthorized caller). The verdict is stamped below regardless of enforcement.
|
|
268
|
+
v_shadow_authorized := agent_board._can_claim_resource(p_resource_kind, p_resource_key);
|
|
269
|
+
|
|
270
|
+
-- CLAIMGATE/ENFORCE (#133): per-kind GATED DENIAL, BEFORE the sweep (predicate-before-side-
|
|
271
|
+
-- effects). When this fleet-backed kind's flag is ON and the actor is UNAUTHORIZED, DENY —
|
|
272
|
+
-- no state has changed yet (sweep/insert are below). When OFF, fall through to the shadow-
|
|
273
|
+
-- stamp only (SHADOW behavior; blocks nothing). Non-fleet kinds never kind-enforce (resolver
|
|
274
|
+
-- returns FALSE). service_role is authorized by the predicate (wrapper short-circuit), so the
|
|
275
|
+
-- operator carve-out holds. Fail-closed-on-unknown is automatic: an unresolvable fleet resource
|
|
276
|
+
-- makes v_shadow_authorized FALSE, so an enforced acquire of it denies.
|
|
277
|
+
if p_resource_kind in ('machine', 'repository', 'repo_file')
|
|
278
|
+
and agent_board._claimgate_kind_enforced(p_resource_kind)
|
|
279
|
+
and not v_shadow_authorized then
|
|
280
|
+
raise exception 'not authorized to claim this % resource (fleet authority required)', p_resource_kind
|
|
281
|
+
using errcode = '42501';
|
|
282
|
+
end if;
|
|
283
|
+
|
|
284
|
+
-- CLAIMGATE/ENFORCE (#133): max-lease horizon. For an ENFORCED fleet-backed kind, CLAMP the
|
|
285
|
+
-- lease to now()+horizon (default 30d, operator-configurable) — a bound on lease term, NOT an
|
|
286
|
+
-- authority block. Clamp (not reject) so an over-long lease reconciles here / at its next renew.
|
|
287
|
+
-- service_role exempt (operator carve-out). Only reached by an AUTHORIZED acquire (unauthorized
|
|
288
|
+
-- was denied above), or when the flag is OFF (then the guard below is skipped entirely).
|
|
289
|
+
if p_resource_kind in ('machine', 'repository', 'repo_file')
|
|
290
|
+
and not agent_board._is_service_role()
|
|
291
|
+
and agent_board._claimgate_kind_enforced(p_resource_kind)
|
|
292
|
+
and p_lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() then
|
|
293
|
+
p_lease_expires_at := now() + agent_board._claimgate_max_lease_horizon();
|
|
294
|
+
end if;
|
|
295
|
+
|
|
296
|
+
-- Ownerless tail (file/tool/custom): the ONE guard SHADOW carries — GATED OFF BY DEFAULT.
|
|
297
|
+
-- _claimgate_ownerless_guards_enabled() is false unless an operator flips it post-census, so in
|
|
298
|
+
-- SHADOW this block is skipped entirely and NOTHING is blocked (only the stamp above lands).
|
|
299
|
+
-- service_role is also exempt (operator carve-out, consistent with the predicate short-circuit).
|
|
300
|
+
v_ownerless := p_resource_kind in ('file', 'tool', 'custom');
|
|
301
|
+
if v_ownerless
|
|
302
|
+
and not agent_board._is_service_role()
|
|
303
|
+
and agent_board._claimgate_ownerless_guards_enabled()
|
|
304
|
+
-- BOARD OPT-OUT: a board whose work is coordinated by SOMEONE ELSE'S locking (a repo lock, a
|
|
305
|
+
-- scheduler lease, an external mutex) can set claim_policy='advisory'. Our claims are then
|
|
306
|
+
-- RECORDED for visibility and never ENFORCED, so we cannot reject an acquire that their own
|
|
307
|
+
-- lock already permitted. Resolved from the claim's thread; a claim with no thread, or a
|
|
308
|
+
-- thread with no board, has no board to ask and falls back to the fleet-wide flags.
|
|
309
|
+
and agent_board._claimgate_board_claim_policy(v_thread_id) <> 'advisory' then
|
|
310
|
+
-- TTL CAP: an ownerless lease may not exceed the max horizon. Rejects an over-long acquire;
|
|
311
|
+
-- this is a lease-length bound, NOT an authority block.
|
|
312
|
+
if p_lease_expires_at > now() + agent_board._claimgate_ownerless_ttl_cap() then
|
|
313
|
+
raise exception 'ownerless claim lease horizon exceeds cap (%)', agent_board._claimgate_ownerless_ttl_cap()
|
|
314
|
+
using errcode = '22023';
|
|
315
|
+
end if;
|
|
316
|
+
|
|
317
|
+
-- QUOTA (transactionally serialized): an actor-scoped advisory XACT lock serializes this
|
|
318
|
+
-- actor's concurrent acquisitions (on DIFFERENT keys, which the row-level uniqueness index
|
|
319
|
+
-- does NOT), so a count-before-insert cannot race two claims past the cap. The lock releases
|
|
320
|
+
-- at commit; it does not block other actors (the key mixes in the actor id).
|
|
321
|
+
perform pg_advisory_xact_lock(hashtext('agent_board.claimgate_ownerless_quota'), hashtext(v_actor_agent_id::text));
|
|
322
|
+
-- #388: THE COUNT IS THE SHARED HELPER NOW, not an inline predicate. renew_claim asks the
|
|
323
|
+
-- same question one line of arithmetic away, and two hand-copies of a predicate across two
|
|
324
|
+
-- enforcement paths is rule 1's own failure shape. The ADVISORY LOCK ABOVE STAYS HERE AND IS
|
|
325
|
+
-- NOT MIRRORED INTO RENEW, deliberately: it exists because acquire COUNTS THEN INSERTS, and a
|
|
326
|
+
-- renew inserts nothing (see this migration's header).
|
|
327
|
+
v_active_ownerless := agent_board._claimgate_active_ownerless_claims(v_actor_agent_id);
|
|
328
|
+
if v_active_ownerless >= agent_board._claimgate_ownerless_quota() then
|
|
329
|
+
raise exception 'ownerless active-claim quota reached (% per agent)', agent_board._claimgate_ownerless_quota()
|
|
330
|
+
using errcode = '54000';
|
|
331
|
+
end if;
|
|
332
|
+
end if;
|
|
333
|
+
|
|
334
|
+
-- CLAIMKEY (#133) MUST-FIX 3: the sweep AND the conflict check are ALIAS-AWARE. p_resource_key
|
|
335
|
+
-- is already canonical (above); comparing against the CANONICAL form of each existing row's key
|
|
336
|
+
-- matches both a canonical row AND an un-healed LEGACY row for the same physical resource. Without
|
|
337
|
+
-- this, acquire('machine','machine:fpDH') would normalize to 'fpDH', see no exact-'fpDH' conflict
|
|
338
|
+
-- against a still-active legacy 'machine:fpDH' row, and INSERT a second live hold — breaking mutual
|
|
339
|
+
-- exclusion during the legacy window. Closing it here in code does NOT depend on the operator-owned
|
|
340
|
+
-- live census.
|
|
341
|
+
update agent_board.claims c
|
|
342
|
+
set status = 'expired',
|
|
343
|
+
updated_at = now()
|
|
344
|
+
where c.resource_kind = p_resource_kind
|
|
345
|
+
and agent_board._canonical_claim_key(c.resource_kind, c.resource_key) = p_resource_key
|
|
346
|
+
and c.status = 'active'
|
|
347
|
+
and c.released_at is null
|
|
348
|
+
and c.lease_expires_at <= now();
|
|
349
|
+
|
|
350
|
+
if exists (
|
|
351
|
+
select 1 from agent_board.claims
|
|
352
|
+
where claims.resource_kind = p_resource_kind
|
|
353
|
+
and agent_board._canonical_claim_key(claims.resource_kind, claims.resource_key) = p_resource_key
|
|
354
|
+
and claims.status = 'active'
|
|
355
|
+
and claims.released_at is null
|
|
356
|
+
and claims.lease_expires_at > now()
|
|
357
|
+
) then
|
|
358
|
+
raise exception 'conflicting active claim' using errcode = '23505';
|
|
359
|
+
end if;
|
|
360
|
+
|
|
361
|
+
insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at, metadata)
|
|
362
|
+
values (
|
|
363
|
+
p_task_id, v_thread_id, v_actor_agent_id, p_resource_kind, p_resource_key, p_lease_expires_at,
|
|
364
|
+
-- SHADOW stamp: record the authority verdict alongside any caller metadata. Merged last so it
|
|
365
|
+
-- is authoritative over a caller-supplied claimgate_shadow key.
|
|
366
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
367
|
+
|| jsonb_build_object(
|
|
368
|
+
'claimgate_shadow',
|
|
369
|
+
jsonb_build_object('kind', p_resource_kind, 'authorized', v_shadow_authorized)
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
returning id into v_claim_id;
|
|
373
|
+
|
|
374
|
+
if p_task_id is not null then
|
|
375
|
+
perform agent_board._append_task_event('unattested:signed-metadata-rpc', p_task_id, v_actor_agent_id, 'claimed', jsonb_build_object('claim_id', v_claim_id, 'resource_kind', p_resource_kind, 'resource_key', p_resource_key));
|
|
376
|
+
end if;
|
|
377
|
+
|
|
378
|
+
return query
|
|
379
|
+
select 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
|
|
380
|
+
from agent_board.claims c
|
|
381
|
+
where c.id = v_claim_id;
|
|
382
|
+
end;
|
|
383
|
+
$function$;
|
|
384
|
+
|
|
385
|
+
-- ---------------------------------------------------------------------------
|
|
386
|
+
-- renew_claim. The #373 body (20260925000100) with the convergence clamp added after the TTL clamp,
|
|
387
|
+
-- and the live claim's agent id and CURRENT expiry read alongside the kind it already reads.
|
|
388
|
+
-- ---------------------------------------------------------------------------
|
|
389
|
+
create or replace function agent_board.renew_claim(p_lease_token uuid, p_lease_expires_at timestamptz)
|
|
390
|
+
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)
|
|
391
|
+
language plpgsql
|
|
392
|
+
security definer
|
|
393
|
+
set search_path = agent_board, public
|
|
394
|
+
as $$
|
|
395
|
+
declare
|
|
396
|
+
v_actor_agent_id uuid;
|
|
397
|
+
v_kind text;
|
|
398
|
+
v_key text;
|
|
399
|
+
v_thread_id uuid;
|
|
400
|
+
v_claim_agent_id uuid;
|
|
401
|
+
v_current_expires timestamptz;
|
|
402
|
+
v_effective_expires timestamptz;
|
|
403
|
+
begin
|
|
404
|
+
if not agent_board._is_service_role() then
|
|
405
|
+
v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
|
|
406
|
+
if not exists (
|
|
407
|
+
select 1
|
|
408
|
+
from agent_board.claims c
|
|
409
|
+
where c.lease_token = p_lease_token
|
|
410
|
+
and c.agent_id = v_actor_agent_id
|
|
411
|
+
) then
|
|
412
|
+
raise exception 'claim not accessible to current agent' using errcode = '42501';
|
|
413
|
+
end if;
|
|
414
|
+
end if;
|
|
415
|
+
|
|
416
|
+
-- CLAIMGATE/ENFORCE (#133): re-authorize + horizon for the predicate-gated kinds. Read the
|
|
417
|
+
-- live lease's resource identity (SECURITY DEFINER, so RLS does not hide it). A dead/expired
|
|
418
|
+
-- lease yields a null kind and the UPDATE below matches nothing (preserves prior behavior).
|
|
419
|
+
--
|
|
420
|
+
-- #388 adds the HOLDER and the CURRENT EXPIRY to this same read. Both come from the live row and
|
|
421
|
+
-- never from a caller argument: a caller-supplied agent id is an assertion about who holds the
|
|
422
|
+
-- claim, and a caller-supplied expiry would let the convergence clamp be handed its own baseline.
|
|
423
|
+
select c.resource_kind, c.resource_key, c.thread_id, c.agent_id, c.lease_expires_at
|
|
424
|
+
into v_kind, v_key, v_thread_id, v_claim_agent_id, v_current_expires
|
|
425
|
+
from agent_board.claims c
|
|
426
|
+
where c.lease_token = p_lease_token
|
|
427
|
+
and c.status = 'active'
|
|
428
|
+
and c.released_at is null
|
|
429
|
+
and c.lease_expires_at > now();
|
|
430
|
+
|
|
431
|
+
-- RE-AUTH: bound revocation latency to one lease term. Uses the SAME session wrapper acquire
|
|
432
|
+
-- consults (_can_claim_resource -> _can_claim_resource_for(current_agent_id())), so service_role
|
|
433
|
+
-- keeps its carve-out and acquire+renew share ONE per-kind flag (a mutation splitting the flags
|
|
434
|
+
-- fails a test). Task-renew is EXCLUDED (ownership-only, ratified supersession).
|
|
435
|
+
if v_kind in ('machine', 'repository', 'repo_file')
|
|
436
|
+
and agent_board._claimgate_kind_enforced(v_kind) then
|
|
437
|
+
if not agent_board._can_claim_resource(v_kind, agent_board._canonical_claim_key(v_kind, v_key)) then
|
|
438
|
+
raise exception 'not authorized to renew this % claim (fleet authority revoked)', v_kind
|
|
439
|
+
using errcode = '42501';
|
|
440
|
+
end if;
|
|
441
|
+
end if;
|
|
442
|
+
|
|
443
|
+
-- HORIZON: clamp the renewed lease to now()+horizon for an ENFORCED fleet-gated kind — this is
|
|
444
|
+
-- where an existing overlong lease reconciles (no bulk-clamp migration; the horizon clamps it
|
|
445
|
+
-- at its next renew). service_role exempt. Same per-kind flag as acquire.
|
|
446
|
+
v_effective_expires := p_lease_expires_at;
|
|
447
|
+
if v_kind in ('machine', 'repository', 'repo_file')
|
|
448
|
+
and not agent_board._is_service_role()
|
|
449
|
+
and agent_board._claimgate_kind_enforced(v_kind)
|
|
450
|
+
and p_lease_expires_at > now() + agent_board._claimgate_max_lease_horizon() then
|
|
451
|
+
v_effective_expires := now() + agent_board._claimgate_max_lease_horizon();
|
|
452
|
+
end if;
|
|
453
|
+
|
|
454
|
+
-- #373 OWNERLESS TTL CAP AT RENEW. The half that was missing: without this an agent renews a
|
|
455
|
+
-- file/tool/custom lease past the 30-day cap indefinitely and the bound is decorative.
|
|
456
|
+
--
|
|
457
|
+
-- Clamps rather than raises, unlike acquire. A raise here would kill a lease the agent is
|
|
458
|
+
-- actively holding; shortening it costs one more renew. Same resolution the fleet-kind horizon
|
|
459
|
+
-- clamp above already applies, and the same place an over-long legacy lease reconciles.
|
|
460
|
+
--
|
|
461
|
+
-- The four conditions are acquire's, verbatim, so ONE operator flag governs both paths and a
|
|
462
|
+
-- board that declined our locking (claim_policy='advisory') is not clamped by it either. The
|
|
463
|
+
-- thread comes from the LIVE CLAIM, never from a caller argument -- a caller-supplied thread
|
|
464
|
+
-- would let an agent choose which board's policy is consulted.
|
|
465
|
+
if v_kind in ('file', 'tool', 'custom')
|
|
466
|
+
and not agent_board._is_service_role()
|
|
467
|
+
and agent_board._claimgate_ownerless_guards_enabled()
|
|
468
|
+
and agent_board._claimgate_board_claim_policy(v_thread_id) <> 'advisory'
|
|
469
|
+
and v_effective_expires > now() + agent_board._claimgate_ownerless_ttl_cap() then
|
|
470
|
+
v_effective_expires := now() + agent_board._claimgate_ownerless_ttl_cap();
|
|
471
|
+
end if;
|
|
472
|
+
|
|
473
|
+
-- #388 OWNERLESS QUOTA CONVERGENCE AT RENEW. The cap above bounds how LONG a lease may be; this
|
|
474
|
+
-- bounds how long an agent may stay OVER THE CEILING, which nothing did before. While the holder
|
|
475
|
+
-- is over quota a renew may not POSTPONE a deadline: it still succeeds, the lease it already
|
|
476
|
+
-- holds is preserved to the microsecond, and the only thing withheld is more time. Each claim
|
|
477
|
+
-- then lapses at its own already-set expiry, the count falls monotonically, and this block stops
|
|
478
|
+
-- firing the instant the count is back at the ceiling.
|
|
479
|
+
--
|
|
480
|
+
-- least(), not an assignment. An over-quota agent asking for a SHORTER lease must still get the
|
|
481
|
+
-- shorter one — that is the agent converging voluntarily, and refusing it would be perverse.
|
|
482
|
+
--
|
|
483
|
+
-- `>` and not `>=`. `>=` is ACQUIRE's refusal predicate; reusing it here would freeze an agent
|
|
484
|
+
-- sitting legitimately AT the ceiling and walk it down to 49. The predicate is the census's own
|
|
485
|
+
-- `overage > 0`, so the fixed point is exactly the ceiling.
|
|
486
|
+
--
|
|
487
|
+
-- Same four carve-outs as the cap above, evaluated against the LIVE claim's holder and thread.
|
|
488
|
+
-- No advisory lock: the row being renewed is required by the UPDATE's own WHERE to be one of the
|
|
489
|
+
-- counted rows, so the count is invariant under renew and there is no slot to race for. See the
|
|
490
|
+
-- migration header for why that departs from #388 step 2.
|
|
491
|
+
if v_kind in ('file', 'tool', 'custom')
|
|
492
|
+
and not agent_board._is_service_role()
|
|
493
|
+
and agent_board._claimgate_ownerless_guards_enabled()
|
|
494
|
+
and agent_board._claimgate_board_claim_policy(v_thread_id) <> 'advisory'
|
|
495
|
+
and agent_board._claimgate_active_ownerless_claims(v_claim_agent_id)
|
|
496
|
+
> agent_board._claimgate_ownerless_quota() then
|
|
497
|
+
v_effective_expires := least(v_effective_expires, v_current_expires);
|
|
498
|
+
end if;
|
|
499
|
+
|
|
500
|
+
-- CLAIMKEY (#133) MUST-FIX 1: re-canonicalize the STORED key as the lease is renewed, but
|
|
501
|
+
-- NEVER propagate a uniqueness collision. A check-then-act guard is NOT race-safe: two aliased
|
|
502
|
+
-- legacy rows renewed concurrently at READ COMMITTED can both observe the canonical key free
|
|
503
|
+
-- and one loses on claims_active_resource_unique (23505). So attempt the canonicalization and,
|
|
504
|
+
-- on unique_violation (deterministic alias OR concurrent race), retry ONCE keeping the CURRENT
|
|
505
|
+
-- key. That makes renew non-raising UNCONDITIONALLY (true normalize-only); the surviving legacy
|
|
506
|
+
-- key is the deferred dual-key alias the ENFORCE phase closes.
|
|
507
|
+
begin
|
|
508
|
+
return query
|
|
509
|
+
update agent_board.claims c
|
|
510
|
+
set lease_expires_at = v_effective_expires,
|
|
511
|
+
resource_key = agent_board._canonical_claim_key(c.resource_kind, c.resource_key),
|
|
512
|
+
updated_at = now()
|
|
513
|
+
where c.lease_token = p_lease_token
|
|
514
|
+
and c.status = 'active'
|
|
515
|
+
and c.released_at is null
|
|
516
|
+
and c.lease_expires_at > now()
|
|
517
|
+
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;
|
|
518
|
+
exception when unique_violation then
|
|
519
|
+
-- the canonical key is already held (by a committed twin or a concurrent renew/acquire):
|
|
520
|
+
-- renew the lease keeping the current key, aliasing deferred to ENFORCE. Only the key change
|
|
521
|
+
-- can raise unique_violation (the lease/timestamp columns are unconstrained), so the retry
|
|
522
|
+
-- cannot itself collide.
|
|
523
|
+
return query
|
|
524
|
+
update agent_board.claims c
|
|
525
|
+
set lease_expires_at = v_effective_expires,
|
|
526
|
+
updated_at = now()
|
|
527
|
+
where c.lease_token = p_lease_token
|
|
528
|
+
and c.status = 'active'
|
|
529
|
+
and c.released_at is null
|
|
530
|
+
and c.lease_expires_at > now()
|
|
531
|
+
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;
|
|
532
|
+
end;
|
|
533
|
+
end;
|
|
534
|
+
$$;
|
|
535
|
+
|
|
536
|
+
-- ---------------------------------------------------------------------------
|
|
537
|
+
-- Rule 5: two comments that were true when written and are not now. Corrected in the same
|
|
538
|
+
-- migration that falsifies them rather than left for the next reader to disbelieve.
|
|
539
|
+
-- ---------------------------------------------------------------------------
|
|
540
|
+
comment on function agent_board._claimgate_ownerless_quota() is
|
|
541
|
+
'CLAIMGATE/SHADOW (#133): max concurrent ACTIVE ownerless (file/tool/custom) claims per agent. '
|
|
542
|
+
'Single source for the migration logic and its tests. GENEROUS placeholder (50) pending prod '
|
|
543
|
+
'census. Enforced on BOTH write paths: acquire_claim REFUSES at count >= quota (54000), and '
|
|
544
|
+
'renew_claim stops EXTENDING at count > quota (#388) so an agent already over the ceiling '
|
|
545
|
+
'converges back to it instead of renewing its overage forever — GUARANTEED ONLY against an agent '
|
|
546
|
+
'whose boards do not opt out: a board owner may flip claim_policy to advisory, let the agent '
|
|
547
|
+
'renew to the full horizon, and flip back, indefinitely (#399; pinned by ORQ-11/12). The two '
|
|
548
|
+
'predicates differ '
|
|
549
|
+
'deliberately — >= is what makes the quota the largest reachable population, > is what makes the '
|
|
550
|
+
'ceiling the convergence fixed point — and both count through '
|
|
551
|
+
'_claimgate_active_ownerless_claims(). Governed by _claimgate_ownerless_guards_enabled() on both '
|
|
552
|
+
'paths, service_role exempt, and skipped for a board whose claim_policy is advisory.';
|
|
553
|
+
|
|
554
|
+
comment on table agent_board.claimgate_enforcement_flags is
|
|
555
|
+
'CLAIMGATE per-scope enforcement flags. Absent row => disabled (SHADOW default: blocks nothing). '
|
|
556
|
+
'scope_key ''ownerless_guards'' gates EVERY ownerless (file/tool/custom) guard on BOTH write '
|
|
557
|
+
'paths: the TTL cap + per-agent quota in acquire_claim, and in renew_claim both the TTL clamp '
|
|
558
|
+
'(#373) and the quota-convergence clamp (#388, which stops an over-quota holder from extending '
|
|
559
|
+
'any lease until it is back at the ceiling — on a board that has not opted out; the advisory '
|
|
560
|
+
'opt-out is owner-toggleable and re-toggleable, so it is also an escape from convergence, #399). '
|
|
561
|
+
'Operator-managed (service_role); enabled only after '
|
|
562
|
+
'the prod lease-length census.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|
package/schema-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "agent_board_schema_manifest.v1",
|
|
3
3
|
"interface": "IF-0-MBPLANE-BUNDLE-1",
|
|
4
4
|
"package": "@consiliency/agent-board-schema",
|
|
5
|
-
"packageVersion": "1.
|
|
5
|
+
"packageVersion": "1.48.0",
|
|
6
6
|
"schemaVersion": 8,
|
|
7
7
|
"minClientVersion": "1.1.0",
|
|
8
8
|
"source": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"migrationCount":
|
|
50
|
-
"aggregateSha256": "
|
|
49
|
+
"migrationCount": 146,
|
|
50
|
+
"aggregateSha256": "cbb24bed446f3334f4c0593716b51ee00e5b6faf57bfd7b48a8767f019551551",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1326,6 +1326,42 @@
|
|
|
1326
1326
|
"sourceRef": "message-board",
|
|
1327
1327
|
"bytes": 10530,
|
|
1328
1328
|
"sha256": "deb58ead6c4d7bbda686b85191d5eb2f2771ae2a405933ffbfbaabd7ad0117d9"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
"order": 142,
|
|
1332
|
+
"id": "20260925000100_agent_board_ownerless_renew_ttl_cap",
|
|
1333
|
+
"filename": "20260925000100_agent_board_ownerless_renew_ttl_cap.sql",
|
|
1334
|
+
"origin": "message-board-forward",
|
|
1335
|
+
"sourceRef": "message-board",
|
|
1336
|
+
"bytes": 9981,
|
|
1337
|
+
"sha256": "da312f43cabf07c6dfd10dbe2a119fb46f49f24155e21adce4f38e291a38103c"
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
"order": 143,
|
|
1341
|
+
"id": "20260926000100_agent_board_ownerless_quota_census",
|
|
1342
|
+
"filename": "20260926000100_agent_board_ownerless_quota_census.sql",
|
|
1343
|
+
"origin": "message-board-forward",
|
|
1344
|
+
"sourceRef": "message-board",
|
|
1345
|
+
"bytes": 17894,
|
|
1346
|
+
"sha256": "bbfabcb2d45cbcebd1e6e121032a1045e8406ea8763a784762a92d210e1e1043"
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
"order": 144,
|
|
1350
|
+
"id": "20260927000100_agent_board_fleet_domain_no_downgrade",
|
|
1351
|
+
"filename": "20260927000100_agent_board_fleet_domain_no_downgrade.sql",
|
|
1352
|
+
"origin": "message-board-forward",
|
|
1353
|
+
"sourceRef": "message-board",
|
|
1354
|
+
"bytes": 25689,
|
|
1355
|
+
"sha256": "ec5c4d89aff2d6d77d44cb3d00c1096b39010c6064c23d72eb3c5c354f8a5e74"
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
"order": 145,
|
|
1359
|
+
"id": "20260928000100_agent_board_ownerless_quota_renew_convergence",
|
|
1360
|
+
"filename": "20260928000100_agent_board_ownerless_quota_renew_convergence.sql",
|
|
1361
|
+
"origin": "message-board-forward",
|
|
1362
|
+
"sourceRef": "message-board",
|
|
1363
|
+
"bytes": 36921,
|
|
1364
|
+
"sha256": "9982efff25378703a744187b15081b5cd53ab38fa6ac7d2950092a471780169e"
|
|
1329
1365
|
}
|
|
1330
1366
|
],
|
|
1331
1367
|
"excludedBridge": {
|