@consiliency/agent-board-schema 1.44.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.
@@ -568,6 +568,10 @@
568
568
  {
569
569
  "id": "20260924000100_agent_board_agent_liveness_denominator",
570
570
  "sha256": "deb58ead6c4d7bbda686b85191d5eb2f2771ae2a405933ffbfbaabd7ad0117d9"
571
+ },
572
+ {
573
+ "id": "20260925000100_agent_board_ownerless_renew_ttl_cap",
574
+ "sha256": "da312f43cabf07c6dfd10dbe2a119fb46f49f24155e21adce4f38e291a38103c"
571
575
  }
572
576
  ]
573
577
  }
@@ -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.44.0",
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",
@@ -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.44.0",
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": 142,
50
- "aggregateSha256": "e608fdc22d910cb43f3d31cf32cb5a3316da2d46404150547c9b2b54a74f585a",
49
+ "migrationCount": 143,
50
+ "aggregateSha256": "e5f248a69031759fbd220b5504268c4ca3159fc7cf641404badf2f2ae855e4ae",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1326,6 +1326,15 @@
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"
1329
1338
  }
1330
1339
  ],
1331
1340
  "excludedBridge": {
@@ -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 + one LIVENESSDENOM 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 = 142)', () => {
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(142);
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,
@@ -271,6 +271,12 @@ describe('manifest integrity', () => {
271
271
  // agent_liveness_summary() always emits reporting_agents beside the stale count so a caller
272
272
  // cannot render "0 stale" as "all clear".
273
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',
274
280
  ]) {
275
281
  expect(names.has(f)).toBe(true);
276
282
  }
@@ -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;