@consiliency/agent-board-schema 1.43.0 → 1.45.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 +8 -0
- package/migrations/20260924000100_agent_board_agent_liveness_denominator.sql +169 -0
- package/migrations/20260925000100_agent_board_ownerless_renew_ttl_cap.sql +166 -0
- package/package.json +1 -1
- package/schema-manifest.json +21 -3
- package/src/tooling.test.mjs +17 -2
- package/tests/agent_board_agent_liveness.test.sql +271 -0
- package/tests/agent_board_ownerless_renew_ttl.test.sql +226 -0
- package/tests/catalog/p_metadata_parameters.json +4 -0
|
@@ -564,6 +564,14 @@
|
|
|
564
564
|
{
|
|
565
565
|
"id": "20260923000100_agent_board_heartbeat_preserve",
|
|
566
566
|
"sha256": "22713b970564bc59433026f70934758982b4cf7832be9c06197faa2d4ecc0040"
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
"id": "20260924000100_agent_board_agent_liveness_denominator",
|
|
570
|
+
"sha256": "deb58ead6c4d7bbda686b85191d5eb2f2771ae2a405933ffbfbaabd7ad0117d9"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
"id": "20260925000100_agent_board_ownerless_renew_ttl_cap",
|
|
574
|
+
"sha256": "da312f43cabf07c6dfd10dbe2a119fb46f49f24155e21adce4f38e291a38103c"
|
|
567
575
|
}
|
|
568
576
|
]
|
|
569
577
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
-- #367 — a liveness reader that cannot report "all clear" about a fleet it is not watching.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT THIS CLOSES. `v_ops_presence_heartbeats` selects FROM `agent_board.presence`:
|
|
4
|
+
--
|
|
5
|
+
-- from agent_board.presence p
|
|
6
|
+
-- join agent_board.agents a on a.id = p.agent_id
|
|
7
|
+
-- where p.expires_at < now();
|
|
8
|
+
--
|
|
9
|
+
-- `presence` is an upsert keyed by `agent_id`, written only by `heartbeat`. An agent that has
|
|
10
|
+
-- NEVER heartbeated has no row, and a row that does not exist cannot be expired. So that view
|
|
11
|
+
-- detects exactly one thing: "was reporting, then stopped". It is structurally blind to
|
|
12
|
+
-- "never reported."
|
|
13
|
+
--
|
|
14
|
+
-- MEASURED, not theorised. On the canonical project on 2026-08-29, immediately after #366 shipped
|
|
15
|
+
-- the writer half: `presence` held 0 rows while 8 agents were `status = 'active'`. Every one of
|
|
16
|
+
-- them was unmonitored, and `select * from v_ops_presence_heartbeats` returned zero rows — output
|
|
17
|
+
-- byte-identical to a fleet in perfect health. That is an absence of signal carrying an alarm's
|
|
18
|
+
-- authority, which is worse than no reader at all, because a reader is something people trust.
|
|
19
|
+
--
|
|
20
|
+
-- THE FIX IS THE JOIN DIRECTION. This view selects FROM `agents` and LEFT JOINs `presence`, so a
|
|
21
|
+
-- never-reporting agent is NAMED rather than missing. That is the whole idea; everything else here
|
|
22
|
+
-- follows from it.
|
|
23
|
+
--
|
|
24
|
+
-- WHAT THIS DELIBERATELY DOES NOT CLAIM. #367 offers two shapes: (1) scope the claim to what the
|
|
25
|
+
-- data supports, and (2) establish an expectation so that never-reporting is itself an anomaly.
|
|
26
|
+
-- This is (1). `never_reported` is a REPORTED STATE, not a judgement — the view says "this agent
|
|
27
|
+
-- has no presence row", never "this agent is stalled". Shape (2) would require deciding what
|
|
28
|
+
-- "expected to be alive" means for an agent that is idle by design, and #366 is deliberate that an
|
|
29
|
+
-- agent waiting on a human is indistinguishable from a stalled one from outside the process.
|
|
30
|
+
-- Naming the state without asserting it is abnormal is how this stays honest while still being
|
|
31
|
+
-- strictly more informative than the view it complements.
|
|
32
|
+
--
|
|
33
|
+
-- WHY THE SUMMARY FUNCTION EXISTS AT ALL. A caller that reads only rows still cannot tell
|
|
34
|
+
-- "0 stale agents" from "nobody reports". The summary always emits `active_agents` alongside
|
|
35
|
+
-- `reporting_agents`, so an empty stale set is reported as `0 stale of 3 reporting, of 8 active`
|
|
36
|
+
-- and can never be rendered as "all clear". #367's first acceptance criterion is that the
|
|
37
|
+
-- denominator appears in the output; putting it in a function rather than in a comment is what
|
|
38
|
+
-- makes that mechanical.
|
|
39
|
+
|
|
40
|
+
-- ---------------------------------------------------------------------------
|
|
41
|
+
-- The view. `security_invoker` so RLS on `agents`/`presence` applies to the READER, matching
|
|
42
|
+
-- every other view in agent_board — a definer view here would hand every caller a fleet-wide
|
|
43
|
+
-- roster regardless of what they may see.
|
|
44
|
+
-- ---------------------------------------------------------------------------
|
|
45
|
+
create or replace view agent_board.v_agent_liveness
|
|
46
|
+
with (security_invoker = true)
|
|
47
|
+
as
|
|
48
|
+
select
|
|
49
|
+
a.id as agent_id,
|
|
50
|
+
a.handle as agent_handle,
|
|
51
|
+
-- The vocabulary is closed and total: every active agent lands in exactly one of these.
|
|
52
|
+
-- `never_reported` is FIRST because it is the case the view exists to make visible.
|
|
53
|
+
case
|
|
54
|
+
when p.agent_id is null then 'never_reported'
|
|
55
|
+
when p.expires_at < now() then 'stale'
|
|
56
|
+
else 'fresh'
|
|
57
|
+
end as liveness_state,
|
|
58
|
+
p.availability,
|
|
59
|
+
p.thread_id,
|
|
60
|
+
p.activity_summary,
|
|
61
|
+
p.last_heartbeat_at,
|
|
62
|
+
p.expires_at,
|
|
63
|
+
-- NULL rather than a number for a never-reporting agent. A zero here would read as
|
|
64
|
+
-- "reported just now", which is the opposite of the truth.
|
|
65
|
+
case
|
|
66
|
+
when p.last_heartbeat_at is null then null
|
|
67
|
+
else round(extract(epoch from (now() - p.last_heartbeat_at)) / 60.0, 2)
|
|
68
|
+
end as heartbeat_lag_minutes
|
|
69
|
+
from agent_board.agents a
|
|
70
|
+
left join agent_board.presence p on p.agent_id = a.id
|
|
71
|
+
where a.status = 'active';
|
|
72
|
+
|
|
73
|
+
comment on view agent_board.v_agent_liveness is
|
|
74
|
+
'#367 liveness reader. Selects FROM agents and LEFT JOINs presence, so an agent that has NEVER '
|
|
75
|
+
'heartbeated is NAMED with liveness_state = never_reported rather than being absent — the blind '
|
|
76
|
+
'spot in v_ops_presence_heartbeats, which selects from presence and therefore cannot see an '
|
|
77
|
+
'agent with no row. liveness_state is one of never_reported / stale / fresh and is a REPORTED '
|
|
78
|
+
'STATE, not a judgement: never_reported means "no presence row", NOT "stalled". Deciding that '
|
|
79
|
+
'idleness is abnormal is #367 shape (2) and is deliberately not done here. security_invoker, so '
|
|
80
|
+
'RLS applies to the reader.';
|
|
81
|
+
|
|
82
|
+
-- ---------------------------------------------------------------------------
|
|
83
|
+
-- The summary. Its ONLY job is to make an empty result impossible to misread.
|
|
84
|
+
-- ---------------------------------------------------------------------------
|
|
85
|
+
create or replace function agent_board.agent_liveness_summary()
|
|
86
|
+
returns table(
|
|
87
|
+
active_agents integer,
|
|
88
|
+
reporting_agents integer,
|
|
89
|
+
fresh integer,
|
|
90
|
+
stale integer,
|
|
91
|
+
never_reported integer
|
|
92
|
+
)
|
|
93
|
+
language sql
|
|
94
|
+
stable
|
|
95
|
+
security invoker
|
|
96
|
+
set search_path = agent_board, public
|
|
97
|
+
as $$
|
|
98
|
+
select
|
|
99
|
+
count(*)::integer as active_agents,
|
|
100
|
+
count(*) filter (where liveness_state <> 'never_reported')::integer as reporting_agents,
|
|
101
|
+
count(*) filter (where liveness_state = 'fresh')::integer as fresh,
|
|
102
|
+
count(*) filter (where liveness_state = 'stale')::integer as stale,
|
|
103
|
+
count(*) filter (where liveness_state = 'never_reported')::integer as never_reported
|
|
104
|
+
from agent_board.v_agent_liveness;
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
comment on function agent_board.agent_liveness_summary() is
|
|
108
|
+
'#367 denominator. Returns active_agents and reporting_agents ALONGSIDE the stale count, so a '
|
|
109
|
+
'caller can never render "0 stale" as "all clear" — 0 stale of 0 reporting, of 8 active, is an '
|
|
110
|
+
'unmonitored fleet, and it reads as one. The four state counts sum to active_agents by '
|
|
111
|
+
'construction because v_agent_liveness assigns every active agent exactly one state. '
|
|
112
|
+
'security invoker + stable: it is a projection of the view and inherits the caller''s RLS.';
|
|
113
|
+
|
|
114
|
+
-- ---------------------------------------------------------------------------
|
|
115
|
+
-- The OLD view is left in service deliberately — it answers a narrower question correctly and
|
|
116
|
+
-- operators' saved queries reference it — but it now carries its own blind spot in the catalog.
|
|
117
|
+
-- Two review seats independently made the same point: shipping a fix while the DOCUMENTED alarm
|
|
118
|
+
-- path still has the defect closes an issue without closing the exposure. `specs/operations.md`
|
|
119
|
+
-- was repointed in the same change; this comment is for whoever reads the catalog instead.
|
|
120
|
+
-- ---------------------------------------------------------------------------
|
|
121
|
+
comment on view agent_board.v_ops_presence_heartbeats is
|
|
122
|
+
'Expired presence leases. BLIND SPOT, by construction: it selects FROM presence, and presence '
|
|
123
|
+
'has no row for an agent that has NEVER heartbeated — so this detects only "was reporting, then '
|
|
124
|
+
'stopped" and an empty result is byte-identical to a healthy fleet. Measured 2026-08-29 on the '
|
|
125
|
+
'canonical project: 0 presence rows, 8 active agents, this view returned nothing. For "who in '
|
|
126
|
+
'the fleet is alive" use agent_board.v_agent_liveness and agent_board.agent_liveness_summary() '
|
|
127
|
+
'(#367), which select FROM agents and state their denominator. This view remains correct for '
|
|
128
|
+
'the narrower question it actually answers.';
|
|
129
|
+
|
|
130
|
+
-- ---------------------------------------------------------------------------
|
|
131
|
+
-- SERVICE_ROLE ONLY, and this is the most important line in the file.
|
|
132
|
+
--
|
|
133
|
+
-- The first version of this migration granted both to `authenticated` as well, reasoning that
|
|
134
|
+
-- `security_invoker` made that safe because RLS would apply to the reader. That reasoning was
|
|
135
|
+
-- exactly backwards, and a red-team review seat found it. RLS does not merely LIMIT what an
|
|
136
|
+
-- authenticated caller sees here — it CHANGES WHAT THE ANSWER MEANS:
|
|
137
|
+
--
|
|
138
|
+
-- * `agents` is `using (current_agent_id() is not null)` → an agent sees EVERY agent row.
|
|
139
|
+
-- * `presence` is `using (agent_id = current_agent_id())` → an agent sees ONLY ITS OWN row.
|
|
140
|
+
--
|
|
141
|
+
-- So for an authenticated caller the LEFT JOIN converts "this row is invisible to you" into
|
|
142
|
+
-- `p.agent_id is null`, which this view then labels `never_reported`. Measured on a local cluster
|
|
143
|
+
-- with TWO genuinely healthy agents, both with fresh presence rows:
|
|
144
|
+
--
|
|
145
|
+
-- as service_role → fresh=2 never_reported=0 (true)
|
|
146
|
+
-- as authenticated agent one → fresh=1 never_reported=1 (FABRICATED)
|
|
147
|
+
--
|
|
148
|
+
-- The reader would invent the exact alarm it exists to report, and it would do so confidently and
|
|
149
|
+
-- per-caller, so two readers would disagree with no way to tell which was right. That is strictly
|
|
150
|
+
-- worse than the blind spot this migration closes: `v_ops_presence_heartbeats` UNDER-reports
|
|
151
|
+
-- (silence), and this would OVER-report (a false positive with an agent's name on it).
|
|
152
|
+
--
|
|
153
|
+
-- An authenticated agent cannot answer "who in the fleet is alive" at all, because it cannot see
|
|
154
|
+
-- anyone else's presence. That is a property of the RLS model, not a gap in this view, and the
|
|
155
|
+
-- honest response is to withhold the surface rather than to serve a per-caller answer that reads
|
|
156
|
+
-- like a fleet-wide one. Liveness is an operations question and this is an operations surface.
|
|
157
|
+
--
|
|
158
|
+
-- `security_invoker` is KEPT even though only service_role is granted: it is defence in depth, so
|
|
159
|
+
-- that if a future migration widens the grant the rows are still RLS-filtered rather than leaked
|
|
160
|
+
-- wholesale. AL-9 pins the grant so widening it fails a test that says why.
|
|
161
|
+
-- ---------------------------------------------------------------------------
|
|
162
|
+
grant select on agent_board.v_agent_liveness to service_role;
|
|
163
|
+
|
|
164
|
+
-- The REVOKE is not redundant, and AL-9b caught it missing. A newly created function is granted
|
|
165
|
+
-- EXECUTE to PUBLIC by default, so `authenticated` inherits it through PUBLIC and granting only
|
|
166
|
+
-- service_role above changes nothing on its own. This is the schema-mechanics trap AGENTS.md
|
|
167
|
+
-- names, arriving on a plain `create or replace function` rather than on a DROP.
|
|
168
|
+
revoke all on function agent_board.agent_liveness_summary() from public;
|
|
169
|
+
grant execute on function agent_board.agent_liveness_summary() to service_role;
|
|
@@ -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.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.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.45.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": 143,
|
|
50
|
+
"aggregateSha256": "e5f248a69031759fbd220b5504268c4ca3159fc7cf641404badf2f2ae855e4ae",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1317,6 +1317,24 @@
|
|
|
1317
1317
|
"sourceRef": "message-board",
|
|
1318
1318
|
"bytes": 7731,
|
|
1319
1319
|
"sha256": "22713b970564bc59433026f70934758982b4cf7832be9c06197faa2d4ecc0040"
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
"order": 141,
|
|
1323
|
+
"id": "20260924000100_agent_board_agent_liveness_denominator",
|
|
1324
|
+
"filename": "20260924000100_agent_board_agent_liveness_denominator.sql",
|
|
1325
|
+
"origin": "message-board-forward",
|
|
1326
|
+
"sourceRef": "message-board",
|
|
1327
|
+
"bytes": 10530,
|
|
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"
|
|
1320
1338
|
}
|
|
1321
1339
|
],
|
|
1322
1340
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 143)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(143);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -262,6 +262,21 @@ describe('manifest integrity', () => {
|
|
|
262
262
|
// existing row; a first heartbeat still lands 'ready'/'{}'. Signature unchanged (parameter
|
|
263
263
|
// defaults are not part of function identity), so the ACL and COMMENT survive.
|
|
264
264
|
'20260923000100_agent_board_heartbeat_preserve.sql',
|
|
265
|
+
// #367 LIVENESSDENOM. v_ops_presence_heartbeats selects FROM presence, and presence has no
|
|
266
|
+
// row for an agent that never heartbeated -- so it detects only "was reporting, then
|
|
267
|
+
// stopped" and is structurally blind to "never reported". Measured on the canonical project
|
|
268
|
+
// the day #366 shipped the writer half: 0 presence rows, 8 active agents, and the view
|
|
269
|
+
// returned zero rows, which is byte-identical to a healthy fleet. v_agent_liveness selects
|
|
270
|
+
// FROM agents and LEFT JOINs presence so a never-reporting agent is NAMED, and
|
|
271
|
+
// agent_liveness_summary() always emits reporting_agents beside the stale count so a caller
|
|
272
|
+
// cannot render "0 stale" as "all clear".
|
|
273
|
+
'20260924000100_agent_board_agent_liveness_denominator.sql',
|
|
274
|
+
// #373 OWNERLESSRENEWTTL. _claimgate_ownerless_ttl_cap bounded a file/tool/custom lease at
|
|
275
|
+
// ACQUIRE and nowhere else, so an agent could renew past the 30-day cap indefinitely and the
|
|
276
|
+
// advertised bound did not exist. renew_claim now clamps (rather than raises -- a raise would
|
|
277
|
+
// kill a live lease) under acquire's own four conditions, so one operator flag governs both
|
|
278
|
+
// paths and an advisory board is exempt on both.
|
|
279
|
+
'20260925000100_agent_board_ownerless_renew_ttl_cap.sql',
|
|
265
280
|
]) {
|
|
266
281
|
expect(names.has(f)).toBe(true);
|
|
267
282
|
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
-- #367 — the reader must not be able to say "all clear" about a fleet it is not watching.
|
|
2
|
+
--
|
|
3
|
+
-- AL-3 IS THE ASSERTION THIS FILE EXISTS FOR, and it is written as a DIFFERENTIAL against the
|
|
4
|
+
-- view it complements. Asserting only "v_agent_liveness names the never-reporting agent" would
|
|
5
|
+
-- pass against a view that names everything and prove nothing about the defect; asserting only
|
|
6
|
+
-- "v_ops_presence_heartbeats misses it" would be a test of code this migration does not change.
|
|
7
|
+
-- Scoring BOTH surfaces on the SAME agent in the SAME transaction is what makes the assertion
|
|
8
|
+
-- about the difference rather than about either one.
|
|
9
|
+
--
|
|
10
|
+
-- AL-6 IS THE OTHER HALF, and it is the one that would have caught the real incident. It builds
|
|
11
|
+
-- the exact production shape — active agents, ZERO presence rows — and asserts the summary reports
|
|
12
|
+
-- `0 stale of 0 reporting, of N active`. A reader that returned "0 stale" alone is byte-identical
|
|
13
|
+
-- to a healthy fleet, which is what made the blind spot dangerous rather than merely incomplete.
|
|
14
|
+
--
|
|
15
|
+
-- SCOPING. Every assertion is scoped to THIS test's agents by handle prefix. An unscoped count
|
|
16
|
+
-- over agent_board.agents is polluted by every fixture any other test left behind and fails
|
|
17
|
+
-- looking like a real defect.
|
|
18
|
+
create extension if not exists pgtap with schema extensions;
|
|
19
|
+
set search_path = extensions, public, agent_board;
|
|
20
|
+
|
|
21
|
+
begin;
|
|
22
|
+
|
|
23
|
+
select plan(17);
|
|
24
|
+
|
|
25
|
+
-- ---------------------------------------------------------------------------
|
|
26
|
+
-- Fixture: three ACTIVE agents in three distinct liveness states, plus one INACTIVE agent that
|
|
27
|
+
-- must not appear at all.
|
|
28
|
+
-- fresh — heartbeated, lease in the future
|
|
29
|
+
-- stale — heartbeated, lease in the past
|
|
30
|
+
-- never — no presence row, which is the case the view exists to surface
|
|
31
|
+
-- retired — status <> 'active'; a liveness reader must not name a decommissioned agent
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
create temp table _al (sfx text, fresh uuid, stale uuid, never uuid, retired uuid) on commit drop;
|
|
34
|
+
|
|
35
|
+
do $$
|
|
36
|
+
declare
|
|
37
|
+
v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
38
|
+
v_fresh uuid := gen_random_uuid();
|
|
39
|
+
v_stale uuid := gen_random_uuid();
|
|
40
|
+
v_never uuid := gen_random_uuid();
|
|
41
|
+
v_retired uuid := gen_random_uuid();
|
|
42
|
+
v_u uuid;
|
|
43
|
+
begin
|
|
44
|
+
foreach v_u in array array[v_fresh, v_stale, v_never, v_retired] loop
|
|
45
|
+
null;
|
|
46
|
+
end loop;
|
|
47
|
+
|
|
48
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
49
|
+
values (v_fresh, 'agent', 'al-fresh-' || v_sfx, 'AL Fresh', 'active'),
|
|
50
|
+
(v_stale, 'agent', 'al-stale-' || v_sfx, 'AL Stale', 'active'),
|
|
51
|
+
(v_never, 'agent', 'al-never-' || v_sfx, 'AL Never', 'active'),
|
|
52
|
+
(v_retired, 'agent', 'al-retired-' || v_sfx, 'AL Retired', 'inactive');
|
|
53
|
+
|
|
54
|
+
-- Presence rows written DIRECTLY rather than through heartbeat(): this file is about the
|
|
55
|
+
-- reader, and going through the RPC would couple it to the actor-resolution path #366 owns.
|
|
56
|
+
insert into agent_board.presence(agent_id, availability, last_heartbeat_at, expires_at, metadata, updated_at)
|
|
57
|
+
values (v_fresh, 'busy', now() - interval '1 minute', now() + interval '20 minutes', '{}'::jsonb, now()),
|
|
58
|
+
(v_stale, 'away', now() - interval '2 hours', now() - interval '90 minutes', '{}'::jsonb, now());
|
|
59
|
+
|
|
60
|
+
insert into _al values (v_sfx, v_fresh, v_stale, v_never, v_retired);
|
|
61
|
+
end $$;
|
|
62
|
+
|
|
63
|
+
-- ---------------------------------------------------------------------------
|
|
64
|
+
-- AL-1..3: the three states, and the differential that names the defect.
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
select is(
|
|
67
|
+
(select liveness_state from agent_board.v_agent_liveness where agent_id = (select fresh from _al)),
|
|
68
|
+
'fresh',
|
|
69
|
+
'AL-1: an agent with an unexpired lease reads fresh');
|
|
70
|
+
|
|
71
|
+
select is(
|
|
72
|
+
(select liveness_state from agent_board.v_agent_liveness where agent_id = (select stale from _al)),
|
|
73
|
+
'stale',
|
|
74
|
+
'AL-2: an agent whose lease has passed reads stale');
|
|
75
|
+
|
|
76
|
+
select is(
|
|
77
|
+
(select liveness_state from agent_board.v_agent_liveness where agent_id = (select never from _al)),
|
|
78
|
+
'never_reported',
|
|
79
|
+
'AL-3 THE POINT: an agent with NO presence row is NAMED never_reported rather than being absent');
|
|
80
|
+
|
|
81
|
+
-- The differential. Same agent, same transaction, the pre-existing surface — which selects FROM
|
|
82
|
+
-- presence and therefore cannot see it. If this ever returns a row, the blind spot closed
|
|
83
|
+
-- somewhere else and AL-3 has stopped being about anything.
|
|
84
|
+
select is(
|
|
85
|
+
(select count(*)::int from agent_board.v_ops_presence_heartbeats where agent_id = (select never from _al)),
|
|
86
|
+
0,
|
|
87
|
+
'AL-3b DIFFERENTIAL: v_ops_presence_heartbeats CANNOT see that same agent — which is why AL-3 is a fix and not a restatement');
|
|
88
|
+
|
|
89
|
+
-- ---------------------------------------------------------------------------
|
|
90
|
+
-- AL-4: a decommissioned agent is not a liveness subject. Without the status filter every agent
|
|
91
|
+
-- ever created accumulates in the never_reported bucket and the signal drowns.
|
|
92
|
+
-- ---------------------------------------------------------------------------
|
|
93
|
+
select is(
|
|
94
|
+
(select count(*)::int from agent_board.v_agent_liveness where agent_id = (select retired from _al)),
|
|
95
|
+
0,
|
|
96
|
+
'AL-4: an inactive agent does not appear at all — a liveness reader must not name a decommissioned agent');
|
|
97
|
+
|
|
98
|
+
-- ---------------------------------------------------------------------------
|
|
99
|
+
-- AL-5: the lag column. NULL for a never-reporting agent, because 0 would read as
|
|
100
|
+
-- "reported just now" — the exact inversion of the truth.
|
|
101
|
+
-- ---------------------------------------------------------------------------
|
|
102
|
+
select is(
|
|
103
|
+
(select heartbeat_lag_minutes from agent_board.v_agent_liveness where agent_id = (select never from _al)),
|
|
104
|
+
null,
|
|
105
|
+
'AL-5: heartbeat_lag_minutes is NULL for a never-reporting agent, never 0 — a zero would read as "reported just now"');
|
|
106
|
+
|
|
107
|
+
select ok(
|
|
108
|
+
(select heartbeat_lag_minutes > 100 from agent_board.v_agent_liveness where agent_id = (select stale from _al)),
|
|
109
|
+
'AL-5b: and it is a real elapsed figure for one that did report (2h ago reads as >100 minutes)');
|
|
110
|
+
|
|
111
|
+
-- ---------------------------------------------------------------------------
|
|
112
|
+
-- AL-6: THE DENOMINATOR, built as the production shape that motivated the issue.
|
|
113
|
+
--
|
|
114
|
+
-- Scored on a scoped recomputation rather than on the global summary, because the summary counts
|
|
115
|
+
-- every active agent in the database and other fixtures pollute it. The PROPERTY under test is
|
|
116
|
+
-- "the reader states reporting alongside stale", and it holds per-subset exactly as it holds
|
|
117
|
+
-- globally.
|
|
118
|
+
-- ---------------------------------------------------------------------------
|
|
119
|
+
select is(
|
|
120
|
+
(select
|
|
121
|
+
count(*) filter (where liveness_state <> 'never_reported')::text || ' reporting of ' ||
|
|
122
|
+
count(*)::text || ' active, ' ||
|
|
123
|
+
count(*) filter (where liveness_state = 'stale')::text || ' stale'
|
|
124
|
+
from agent_board.v_agent_liveness
|
|
125
|
+
where agent_handle like 'al-%' || (select sfx from _al)),
|
|
126
|
+
'2 reporting of 3 active, 1 stale',
|
|
127
|
+
'AL-6: the reader states REPORTING alongside STALE, so a count can never be read without its denominator');
|
|
128
|
+
|
|
129
|
+
-- The real incident shape: active agents, nothing reporting. Deleting this fixture's presence
|
|
130
|
+
-- rows reproduces `presence_rows = 0, active_agents = 8` from the canonical project.
|
|
131
|
+
delete from agent_board.presence
|
|
132
|
+
where agent_id in (select fresh from _al union all select stale from _al);
|
|
133
|
+
|
|
134
|
+
select is(
|
|
135
|
+
(select
|
|
136
|
+
count(*) filter (where liveness_state = 'stale')::text || ' stale of ' ||
|
|
137
|
+
count(*) filter (where liveness_state <> 'never_reported')::text || ' reporting, of ' ||
|
|
138
|
+
count(*)::text || ' active'
|
|
139
|
+
from agent_board.v_agent_liveness
|
|
140
|
+
where agent_handle like 'al-%' || (select sfx from _al)),
|
|
141
|
+
'0 stale of 0 reporting, of 3 active',
|
|
142
|
+
'AL-6b THE INCIDENT SHAPE: with nothing reporting, the output says so — "0 stale" alone would be byte-identical to a healthy fleet');
|
|
143
|
+
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
-- THE ASYMMETRY BELOW IS LOAD-BEARING FOR AL-7b AND AL-10, and neither had it at first.
|
|
146
|
+
-- AL-6b has just deleted this fixture's presence rows, and the persistent seeds carry active
|
|
147
|
+
-- agents with NO presence rows, so at this point the whole database is fresh=0 and stale=0. Every
|
|
148
|
+
-- comparison downstream then scores 0-against-0 and reads as agreement: AL-7b reduced to
|
|
149
|
+
-- `reporting_agents = 0` (so a `reporting_agents` hardcoded to 0 passed it), and AL-10 compared
|
|
150
|
+
-- '0/0' to '0/0' (so swapping the fresh/stale predicates passed it). Two review seats found the
|
|
151
|
+
-- first; the mutation battery found the second. Restoring exactly ONE fresh row is the minimum
|
|
152
|
+
-- state in which either mutation is observable at all.
|
|
153
|
+
insert into agent_board.presence(agent_id, availability, last_heartbeat_at, expires_at, metadata, updated_at)
|
|
154
|
+
values ((select never from _al), 'ready', now(), now() + interval '20 minutes', '{}'::jsonb, now());
|
|
155
|
+
|
|
156
|
+
-- AL-7: the summary function is TOTAL — the four state counts sum to active_agents. If a fourth
|
|
157
|
+
-- state is ever added to the view's CASE without being added to the function, this fails.
|
|
158
|
+
-- ---------------------------------------------------------------------------
|
|
159
|
+
select is(
|
|
160
|
+
(select fresh + stale + never_reported from agent_board.agent_liveness_summary()),
|
|
161
|
+
(select active_agents from agent_board.agent_liveness_summary()),
|
|
162
|
+
'AL-7: the state counts sum to active_agents — a new state that the summary does not count fails here');
|
|
163
|
+
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
-- AL-7b: THE SUMMARY'S OWN reporting_agents, and this assertion exists because the mutation
|
|
166
|
+
-- battery found it missing. Corrupting `reporting_agents` to `count(*)` — so an entirely
|
|
167
|
+
-- unmonitored fleet reports full participation — produced NO failure anywhere in this file.
|
|
168
|
+
-- AL-6/AL-6b recompute the denominator FROM THE VIEW and never call the summary at all, and AL-7's
|
|
169
|
+
-- sum is unaffected by that field. So the one column that makes the denominator mechanical was
|
|
170
|
+
-- unasserted, while the migration's comment claimed it was the point of the function.
|
|
171
|
+
--
|
|
172
|
+
-- Scored as a RELATIONSHIP rather than a literal, because the summary is global and every other
|
|
173
|
+
-- test's fixtures are in it: `reporting_agents` must equal `fresh + stale`. That holds under any
|
|
174
|
+
-- amount of pollution, and it is discriminating here precisely because this file's fixture
|
|
175
|
+
-- guarantees at least one never-reporting agent exists — so active_agents is strictly greater,
|
|
176
|
+
-- and a reporting_agents that counted everyone fails.
|
|
177
|
+
-- ---------------------------------------------------------------------------
|
|
178
|
+
select is(
|
|
179
|
+
(select reporting_agents from agent_board.agent_liveness_summary()),
|
|
180
|
+
(select fresh + stale from agent_board.agent_liveness_summary()),
|
|
181
|
+
'AL-7b: the summary''s OWN reporting_agents equals fresh + stale — a denominator that counted never-reporting agents as reporting would say an unmonitored fleet was fully participating');
|
|
182
|
+
|
|
183
|
+
-- ---------------------------------------------------------------------------
|
|
184
|
+
-- AL-8: security_invoker, asserted from the catalog. A definer view here would hand every caller
|
|
185
|
+
-- a fleet-wide roster regardless of what RLS says they may see.
|
|
186
|
+
-- ---------------------------------------------------------------------------
|
|
187
|
+
select ok(
|
|
188
|
+
(select 'security_invoker=true' = any(c.reloptions)
|
|
189
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
190
|
+
where n.nspname = 'agent_board' and c.relname = 'v_agent_liveness'),
|
|
191
|
+
'AL-8: the view is security_invoker — RLS applies to the reader, not to the view owner');
|
|
192
|
+
|
|
193
|
+
select ok(
|
|
194
|
+
(select not p.prosecdef
|
|
195
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
196
|
+
where n.nspname = 'agent_board' and p.proname = 'agent_liveness_summary'),
|
|
197
|
+
'AL-8b: the SUMMARY is not SECURITY DEFINER either — AL-8 inspects only the view, so a definer function over an invoker view would silently restore the fleet-wide read this design refuses');
|
|
198
|
+
|
|
199
|
+
-- ---------------------------------------------------------------------------
|
|
200
|
+
-- AL-9: SERVICE_ROLE ONLY. The blocking finding of this PR's review.
|
|
201
|
+
--
|
|
202
|
+
-- The first version granted both surfaces to `authenticated` too, on the reasoning that
|
|
203
|
+
-- security_invoker made it safe. RLS does not just limit what an authenticated caller sees here —
|
|
204
|
+
-- it changes what the answer MEANS. `agents` is readable by any agent; `presence` is own-row only.
|
|
205
|
+
-- So the LEFT JOIN turns "invisible to you" into `never_reported`, and the view FABRICATES the
|
|
206
|
+
-- alarm it exists to report. Measured with two genuinely healthy agents: service_role saw
|
|
207
|
+
-- fresh=2/never=0, an authenticated caller saw fresh=1/never=1.
|
|
208
|
+
--
|
|
209
|
+
-- Asserted via has_*_privilege rather than by reading pg_class.relacl, because that resolves
|
|
210
|
+
-- inherited and PUBLIC-default grants which a direct ACL read cannot see.
|
|
211
|
+
-- ---------------------------------------------------------------------------
|
|
212
|
+
select is(
|
|
213
|
+
(select coalesce(string_agg(r, ', ' order by r), '')
|
|
214
|
+
from unnest(array['anon','authenticated','service_role']) r
|
|
215
|
+
where has_table_privilege(r, 'agent_board.v_agent_liveness', 'select')),
|
|
216
|
+
'service_role',
|
|
217
|
+
'AL-9 SECURITY: only service_role may read the view — granted to authenticated it would report never_reported for every agent whose presence row RLS hides, fabricating alarms about healthy agents');
|
|
218
|
+
|
|
219
|
+
select is(
|
|
220
|
+
(select coalesce(string_agg(r, ', ' order by r), '')
|
|
221
|
+
from unnest(array['anon','authenticated','service_role']) r
|
|
222
|
+
where has_function_privilege(r, 'agent_board.agent_liveness_summary()', 'execute')),
|
|
223
|
+
'service_role',
|
|
224
|
+
'AL-9b: and the summary is service_role only for the same reason — its denominator is per-caller under RLS, so two readers would disagree with no way to say which was right');
|
|
225
|
+
|
|
226
|
+
-- ---------------------------------------------------------------------------
|
|
227
|
+
-- AL-10: the summary's fresh/stale are the VIEW's fresh/stale. AL-7/7b assert only SUMS, and a
|
|
228
|
+
-- review seat pointed out that swapping the two predicates inside agent_liveness_summary() leaves
|
|
229
|
+
-- every one of them green: both identities still hold and nothing checked the values themselves.
|
|
230
|
+
-- Scored as an identity against the view so it is immune to other fixtures' pollution.
|
|
231
|
+
--
|
|
232
|
+
-- THE ASYMMETRY BELOW IS LOAD-BEARING, and the first version of this assertion did not have it.
|
|
233
|
+
-- AL-6b has just deleted this fixture's presence rows, so at this point fresh = 0 and stale = 0 --
|
|
234
|
+
-- and '0/0' equals '0/0' whichever way round the two predicates are. The assertion scored a
|
|
235
|
+
-- VACUOUS MATCH and read as agreement; the swap mutation ran clean against it. Restoring exactly
|
|
236
|
+
-- ONE fresh row makes the two counts differ, which is the minimum condition under which a swap is
|
|
237
|
+
-- observable at all. Measured, not reasoned: the mutation fails this line now and did not before.
|
|
238
|
+
-- ---------------------------------------------------------------------------
|
|
239
|
+
select is(
|
|
240
|
+
(select fresh || '/' || stale from agent_board.agent_liveness_summary()),
|
|
241
|
+
(select
|
|
242
|
+
(count(*) filter (where liveness_state = 'fresh'))::text || '/' ||
|
|
243
|
+
(count(*) filter (where liveness_state = 'stale'))::text
|
|
244
|
+
from agent_board.v_agent_liveness),
|
|
245
|
+
'AL-10: the summary''s fresh and stale ARE the view''s fresh and stale — swapping the two predicates inside the function keeps every sum identity true and is invisible without this');
|
|
246
|
+
|
|
247
|
+
-- ---------------------------------------------------------------------------
|
|
248
|
+
-- AL-11: THE CLASS, from the catalog rather than from a list written here (rule 1).
|
|
249
|
+
--
|
|
250
|
+
-- The claim this migration makes is "v_ops_presence_heartbeats is the surface with the blind
|
|
251
|
+
-- spot". That was checked by enumerating pg_views rather than by reading the diff, and the answer
|
|
252
|
+
-- was yes — but a claim checked once is a claim about the moment it was checked. This asserts the
|
|
253
|
+
-- membership so a NEW view over presence fails here and NAMES ITSELF, forcing whoever adds it to
|
|
254
|
+
-- state its join direction.
|
|
255
|
+
--
|
|
256
|
+
-- Each member's claim is asserted POSITIVELY in the message rather than by absence from a list:
|
|
257
|
+
-- v_agent_dashboard FROM agents LEFT JOIN presence — correct direction, no blind spot
|
|
258
|
+
-- v_agent_liveness FROM agents LEFT JOIN presence — this migration
|
|
259
|
+
-- v_ops_presence_heartbeats FROM presence JOIN agents — the blind spot, kept deliberately
|
|
260
|
+
-- v_ops_table_growth names 'presence' as a row-count subject, not a liveness reader
|
|
261
|
+
-- ---------------------------------------------------------------------------
|
|
262
|
+
select is(
|
|
263
|
+
(select string_agg(c.relname, ', ' order by c.relname)
|
|
264
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
265
|
+
where n.nspname = 'agent_board' and c.relkind = 'v'
|
|
266
|
+
and pg_get_viewdef(c.oid) ~ 'presence'),
|
|
267
|
+
'v_agent_dashboard, v_agent_liveness, v_ops_presence_heartbeats, v_ops_table_growth',
|
|
268
|
+
'AL-11 CLASS: exactly these four agent_board views read presence — a new one fails here and names itself, so its join direction gets stated instead of assumed');
|
|
269
|
+
|
|
270
|
+
select * from finish();
|
|
271
|
+
rollback;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
-- #373 — the ownerless TTL cap must bind at RENEW, not only at acquire.
|
|
2
|
+
--
|
|
3
|
+
-- ORT-3 IS THE ASSERTION THIS FILE EXISTS FOR, and it is written as the ATTACK: acquire a lease
|
|
4
|
+
-- inside the cap, then renew it far past the cap. Before this migration that succeeded and could
|
|
5
|
+
-- be repeated forever, so the 30-day bound the cap advertised did not exist. The existing suite
|
|
6
|
+
-- could not have caught its removal because it exercises acquire only — which is exactly how the
|
|
7
|
+
-- gap survived.
|
|
8
|
+
--
|
|
9
|
+
-- ORT-4 IS THE TWIN, and without it ORT-3 is satisfiable by a function that simply refuses to
|
|
10
|
+
-- renew anything. Clamping must still RENEW: a within-cap request has to pass through untouched.
|
|
11
|
+
-- A guard that works by breaking the feature is the failure mode this repo has hit before.
|
|
12
|
+
--
|
|
13
|
+
-- WHY CLAMP AND NOT RAISE, asserted rather than assumed (ORT-3 checks the resulting timestamp, not
|
|
14
|
+
-- an error): acquire RAISES on an over-cap request because nothing is lost by asking again, but a
|
|
15
|
+
-- renew that raised would kill a lease the agent is actively holding. Renew already resolves the
|
|
16
|
+
-- fleet-kind horizon the same way. If someone later "hardens" this into a raise, ORT-3 fails.
|
|
17
|
+
--
|
|
18
|
+
-- SCOPING. The guard flag is fleet-wide state shared with every other test, so this file SETS it
|
|
19
|
+
-- explicitly rather than inheriting whatever ran before — an inherited `false` would make every
|
|
20
|
+
-- assertion here pass vacuously against a completely unguarded function.
|
|
21
|
+
create extension if not exists pgtap with schema extensions;
|
|
22
|
+
set search_path = extensions, public, agent_board;
|
|
23
|
+
|
|
24
|
+
begin;
|
|
25
|
+
|
|
26
|
+
select plan(11);
|
|
27
|
+
|
|
28
|
+
-- ---------------------------------------------------------------------------
|
|
29
|
+
-- Fixture: one agent, one board-linked thread, and the guard flag ON.
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
create temp table _ort (agent uuid, auth_user uuid, thread uuid, board uuid) on commit drop;
|
|
32
|
+
|
|
33
|
+
do $$
|
|
34
|
+
declare
|
|
35
|
+
v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
36
|
+
v_agent uuid := gen_random_uuid();
|
|
37
|
+
v_user uuid;
|
|
38
|
+
v_thread uuid := gen_random_uuid();
|
|
39
|
+
v_board uuid;
|
|
40
|
+
begin
|
|
41
|
+
v_user := public.ensure_github_user('ort-' || v_sfx, 'ort-' || v_sfx || '@example.test', 'ort-' || v_sfx, 'ORT Agent');
|
|
42
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
43
|
+
values (v_agent, 'agent', 'ort-' || v_sfx, 'ORT Agent', 'active', v_user, v_user);
|
|
44
|
+
|
|
45
|
+
perform set_config('request.jwt.claim.role', 'authenticated', true);
|
|
46
|
+
perform set_config('request.jwt.claim.sub', v_user::text, true);
|
|
47
|
+
select board_id into v_board
|
|
48
|
+
from agent_board.create_board(v_agent, 'ort-' || v_sfx, 'ORT Board', 'ownerless renew', '{}'::jsonb, 'private');
|
|
49
|
+
|
|
50
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
|
|
51
|
+
values (v_thread, 'coordination', v_agent, 'ORT thread', v_board);
|
|
52
|
+
|
|
53
|
+
insert into _ort values (v_agent, v_user, v_thread, v_board);
|
|
54
|
+
end $$;
|
|
55
|
+
|
|
56
|
+
-- The guard must be ON or every assertion below passes against an unguarded function.
|
|
57
|
+
insert into agent_board.claimgate_enforcement_flags(scope_key, enabled)
|
|
58
|
+
values ('ownerless_guards', true)
|
|
59
|
+
on conflict (scope_key) do update set enabled = true;
|
|
60
|
+
|
|
61
|
+
-- ---------------------------------------------------------------------------
|
|
62
|
+
-- ORT-1/2: the acquire half still behaves — the baseline this migration must not disturb.
|
|
63
|
+
-- ---------------------------------------------------------------------------
|
|
64
|
+
select throws_ok(
|
|
65
|
+
format($$ select * from agent_board.acquire_claim(%L, 'file', 'ort-overcap-acquire', now() + interval '400 days', null, %L) $$,
|
|
66
|
+
(select agent from _ort)::text, (select thread from _ort)::text),
|
|
67
|
+
'22023',
|
|
68
|
+
null,
|
|
69
|
+
'ORT-1: acquire still REJECTS an over-cap ownerless lease (22023) — the half that already worked');
|
|
70
|
+
|
|
71
|
+
select lives_ok(
|
|
72
|
+
format($$ select * from agent_board.acquire_claim(%L, 'file', 'ort-within-cap', now() + interval '1 day', null, %L) $$,
|
|
73
|
+
(select agent from _ort)::text, (select thread from _ort)::text),
|
|
74
|
+
'ORT-2: a within-cap acquire still succeeds');
|
|
75
|
+
|
|
76
|
+
-- ---------------------------------------------------------------------------
|
|
77
|
+
-- ORT-3: THE ATTACK. Renew that same lease far past the cap. Before this migration the renewed
|
|
78
|
+
-- lease landed at +400 days; now it is clamped to the 30-day horizon.
|
|
79
|
+
-- ---------------------------------------------------------------------------
|
|
80
|
+
create temp table _tok (lease_token uuid) on commit drop;
|
|
81
|
+
insert into _tok
|
|
82
|
+
select lease_token from agent_board.claims
|
|
83
|
+
where agent_id = (select agent from _ort) and resource_key = 'ort-within-cap' and status = 'active';
|
|
84
|
+
|
|
85
|
+
do $$
|
|
86
|
+
begin
|
|
87
|
+
perform agent_board.renew_claim((select lease_token from _tok), now() + interval '400 days');
|
|
88
|
+
end $$;
|
|
89
|
+
|
|
90
|
+
select ok(
|
|
91
|
+
(select lease_expires_at <= now() + agent_board._claimgate_ownerless_ttl_cap() + interval '1 minute'
|
|
92
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
93
|
+
'ORT-3 THE POINT: a renew asking for +400 days is CLAMPED to the 30-day cap — before this it landed at 400 days and could be repeated forever');
|
|
94
|
+
|
|
95
|
+
select ok(
|
|
96
|
+
(select lease_expires_at > now() + interval '29 days'
|
|
97
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
98
|
+
'ORT-3b: and it is clamped TO the cap, not to something arbitrarily short');
|
|
99
|
+
|
|
100
|
+
-- ---------------------------------------------------------------------------
|
|
101
|
+
-- ORT-4: THE TWIN. Clamping must not become refusing — a within-cap renew passes through exactly.
|
|
102
|
+
-- ---------------------------------------------------------------------------
|
|
103
|
+
do $$
|
|
104
|
+
begin
|
|
105
|
+
perform agent_board.renew_claim((select lease_token from _tok), now() + interval '2 days');
|
|
106
|
+
end $$;
|
|
107
|
+
|
|
108
|
+
select ok(
|
|
109
|
+
(select lease_expires_at between now() + interval '47 hours' and now() + interval '49 hours'
|
|
110
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
111
|
+
'ORT-4: a WITHIN-cap renew is untouched (+2 days lands at +2 days) — a guard that refused every renew would pass ORT-3 and be useless');
|
|
112
|
+
|
|
113
|
+
-- ---------------------------------------------------------------------------
|
|
114
|
+
-- ORT-5: the operator carve-out. service_role is exempt on renew exactly as on acquire, so an
|
|
115
|
+
-- operator can still extend a lease deliberately.
|
|
116
|
+
-- ---------------------------------------------------------------------------
|
|
117
|
+
do $$
|
|
118
|
+
begin
|
|
119
|
+
perform set_config('request.jwt.claims', '', true);
|
|
120
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
121
|
+
perform set_config('request.jwt.claim.role', '', true);
|
|
122
|
+
perform agent_board.renew_claim((select lease_token from _tok), now() + interval '400 days');
|
|
123
|
+
end $$;
|
|
124
|
+
|
|
125
|
+
select ok(
|
|
126
|
+
(select lease_expires_at > now() + interval '300 days'
|
|
127
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
128
|
+
'ORT-5: service_role is EXEMPT at renew, same carve-out as acquire — an operator extension is not clamped');
|
|
129
|
+
|
|
130
|
+
-- ---------------------------------------------------------------------------
|
|
131
|
+
-- ORT-6: the flag governs BOTH paths. With ownerless_guards off, renew stops clamping — so the
|
|
132
|
+
-- operator switch means the same thing on each path rather than half-latching.
|
|
133
|
+
-- ---------------------------------------------------------------------------
|
|
134
|
+
do $$
|
|
135
|
+
begin
|
|
136
|
+
update agent_board.claimgate_enforcement_flags set enabled = false where scope_key = 'ownerless_guards';
|
|
137
|
+
perform set_config('request.jwt.claim.role', 'authenticated', true);
|
|
138
|
+
perform set_config('request.jwt.claim.sub', (select auth_user from _ort)::text, true);
|
|
139
|
+
perform agent_board.renew_claim((select lease_token from _tok), now() + interval '400 days');
|
|
140
|
+
end $$;
|
|
141
|
+
|
|
142
|
+
select ok(
|
|
143
|
+
(select lease_expires_at > now() + interval '300 days'
|
|
144
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
145
|
+
'ORT-6: with ownerless_guards OFF the renew is NOT clamped — one flag governs acquire and renew alike');
|
|
146
|
+
|
|
147
|
+
-- ---------------------------------------------------------------------------
|
|
148
|
+
-- ORT-7: the board opt-out reaches renew too. A board coordinating its own locking
|
|
149
|
+
-- (claim_policy='advisory') is not clamped on either path.
|
|
150
|
+
-- ---------------------------------------------------------------------------
|
|
151
|
+
do $$
|
|
152
|
+
begin
|
|
153
|
+
update agent_board.claimgate_enforcement_flags set enabled = true where scope_key = 'ownerless_guards';
|
|
154
|
+
perform agent_board.set_board_claim_policy((select board from _ort), 'advisory');
|
|
155
|
+
perform agent_board.renew_claim((select lease_token from _tok), now() + interval '400 days');
|
|
156
|
+
end $$;
|
|
157
|
+
|
|
158
|
+
select ok(
|
|
159
|
+
(select lease_expires_at > now() + interval '300 days'
|
|
160
|
+
from agent_board.claims where lease_token = (select lease_token from _tok)),
|
|
161
|
+
'ORT-7: a board with claim_policy=advisory is not clamped at renew either — the opt-out is not acquire-only, which would have been the same bug one layer down');
|
|
162
|
+
|
|
163
|
+
-- ---------------------------------------------------------------------------
|
|
164
|
+
-- ORT-8: CLASS. Catalog-derived, so the acquire/renew asymmetry that caused #373 fails a test if
|
|
165
|
+
-- it ever returns. Scored on the pair rather than on renew alone: asserting only "renew has the
|
|
166
|
+
-- cap" would pass if acquire silently LOST it.
|
|
167
|
+
-- ---------------------------------------------------------------------------
|
|
168
|
+
select is(
|
|
169
|
+
(select string_agg(p.proname || '=' || (agent_board_test.stripped_body(p.oid, true) ~ '_claimgate_ownerless_ttl_cap')::text, ', ' order by p.proname)
|
|
170
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
171
|
+
where n.nspname = 'agent_board' and p.proname in ('acquire_claim', 'renew_claim')),
|
|
172
|
+
'acquire_claim=true, renew_claim=true',
|
|
173
|
+
'ORT-8 CLASS: BOTH acquire and renew reference the ownerless TTL cap — #373 was exactly this pair reading true/false');
|
|
174
|
+
|
|
175
|
+
-- ---------------------------------------------------------------------------
|
|
176
|
+
-- ORT-9: THE OTHER TWO KINDS. Every assertion above drives `file`, so dropping 'tool' or 'custom'
|
|
177
|
+
-- from renew's kind list passed the entire battery — named independently by two review seats as
|
|
178
|
+
-- the seventh mutation, and they were right. ORT-8's catalog scan cannot see it either: the helper
|
|
179
|
+
-- reference stays present while two thirds of the class stop being covered.
|
|
180
|
+
--
|
|
181
|
+
-- Both kinds are exercised BEHAVIOURALLY rather than by widening the scan, because the defect is
|
|
182
|
+
-- "this kind is not clamped", which only a clamp observation can catch.
|
|
183
|
+
-- ---------------------------------------------------------------------------
|
|
184
|
+
do $$
|
|
185
|
+
declare k text; tok uuid;
|
|
186
|
+
begin
|
|
187
|
+
perform set_config('request.jwt.claim.role', 'authenticated', true);
|
|
188
|
+
perform set_config('request.jwt.claim.sub', (select auth_user from _ort)::text, true);
|
|
189
|
+
perform agent_board.set_board_claim_policy((select board from _ort), 'inherit');
|
|
190
|
+
foreach k in array array['tool', 'custom'] loop
|
|
191
|
+
select lease_token into tok
|
|
192
|
+
from agent_board.acquire_claim((select agent from _ort), k, 'ort-' || k, now() + interval '1 day', null, (select thread from _ort));
|
|
193
|
+
perform agent_board.renew_claim(tok, now() + interval '400 days');
|
|
194
|
+
end loop;
|
|
195
|
+
end $$;
|
|
196
|
+
|
|
197
|
+
select is(
|
|
198
|
+
(select string_agg(resource_kind || '=' ||
|
|
199
|
+
(lease_expires_at <= now() + agent_board._claimgate_ownerless_ttl_cap() + interval '1 minute')::text, ', ' order by resource_kind)
|
|
200
|
+
from agent_board.claims
|
|
201
|
+
where agent_id = (select agent from _ort) and resource_kind in ('tool', 'custom') and status = 'active'),
|
|
202
|
+
'custom=true, tool=true',
|
|
203
|
+
'ORT-9: tool and custom are clamped at renew too — every other assertion drives `file`, so dropping either from the kind list passed the whole battery');
|
|
204
|
+
|
|
205
|
+
-- ---------------------------------------------------------------------------
|
|
206
|
+
-- ORT-10: A CLAIM WITH NO THREAD IS STILL CLAMPED. The renew-side twin of BCP-5: the board opt-out
|
|
207
|
+
-- is resolved from the claim's thread, and a claim with no thread must fall back to `inherit`
|
|
208
|
+
-- rather than escaping the guard by omission. Named by a third seat as its own seventh mutation —
|
|
209
|
+
-- adding `and v_thread_id is not null` to the renew guard would let an agent acquire a within-cap
|
|
210
|
+
-- threadless lease and then renew it to +400 days.
|
|
211
|
+
-- ---------------------------------------------------------------------------
|
|
212
|
+
do $$
|
|
213
|
+
declare tok uuid;
|
|
214
|
+
begin
|
|
215
|
+
select lease_token into tok
|
|
216
|
+
from agent_board.acquire_claim((select agent from _ort), 'file', 'ort-nothread', now() + interval '1 day', null, null);
|
|
217
|
+
perform agent_board.renew_claim(tok, now() + interval '400 days');
|
|
218
|
+
end $$;
|
|
219
|
+
|
|
220
|
+
select ok(
|
|
221
|
+
(select lease_expires_at <= now() + agent_board._claimgate_ownerless_ttl_cap() + interval '1 minute'
|
|
222
|
+
from agent_board.claims where agent_id = (select agent from _ort) and resource_key = 'ort-nothread' and status = 'active'),
|
|
223
|
+
'ORT-10: a claim with NO thread is still clamped at renew — the board opt-out cannot be obtained by omitting the thread');
|
|
224
|
+
|
|
225
|
+
select * from finish();
|
|
226
|
+
rollback;
|
|
@@ -321,6 +321,10 @@
|
|
|
321
321
|
"signature": "add_artifact(p_created_by_agent_id uuid, p_uri text, p_artifact_type text, p_storage_kind text, p_thread_id uuid, p_task_id uuid, p_message_id uuid, p_task_event_id uuid, p_metadata jsonb)",
|
|
322
322
|
"declares_p_metadata": true
|
|
323
323
|
},
|
|
324
|
+
{
|
|
325
|
+
"signature": "agent_liveness_summary()",
|
|
326
|
+
"declares_p_metadata": false
|
|
327
|
+
},
|
|
324
328
|
{
|
|
325
329
|
"signature": "append_task_event(p_task_id uuid, p_actor_agent_id uuid, p_event_type text, p_payload jsonb, p_status text, p_assigned_agent_id uuid, p_completed_at timestamp with time zone)",
|
|
326
330
|
"declares_p_metadata": false
|