@consiliency/agent-board-schema 1.40.0 → 1.42.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.
@@ -0,0 +1,228 @@
1
+ -- Board-settable claim policy: a board may decline OUR lock, never grant itself ANOTHER OWNER'S
2
+ -- resource.
3
+ --
4
+ -- THE ASK. A board's work may be coordinated by somebody else's locking — a repository lock, a
5
+ -- scheduler lease, an external mutex. If their lock already granted the work, our TTL cap or quota
6
+ -- rejecting the acquire is us breaking a system that was working. So the enforcement is opt-out
7
+ -- and the board owner holds the switch.
8
+ --
9
+ -- BCP-6 IS THE ASSERTION THIS FILE EXISTS FOR. Everything else here checks that the opt-out
10
+ -- works; BCP-6 checks that it does not work on the things it must never reach. `advisory` disables
11
+ -- the OWNERLESS guards (file/tool/custom lease cap + per-agent quota) and must leave the
12
+ -- fleet-authority gate on machine/repository/repo_file untouched — that gate answers "may this
13
+ -- agent claim this resource at all", which is authorization, not a locking convenience.
14
+ --
15
+ -- Structurally that holds because the opt-out sits inside the `v_ownerless` branch and the fleet
16
+ -- kinds are gated far above it. BCP-6 asserts it anyway: "it is in the right branch" is exactly
17
+ -- the kind of reasoning that stops being true when someone moves a line, and this file would
18
+ -- otherwise be a set of tests that only ever prove the permissive direction.
19
+ --
20
+ -- BCP-5 IS THE SECOND ONE THAT MATTERS. A claim reaches its board through its thread. So the
21
+ -- obvious hole is a claim that simply omits the thread — if that inherited "no board, therefore no
22
+ -- enforcement", any agent could opt itself out by passing null. It resolves to 'inherit' instead,
23
+ -- and BCP-5 pins that the fleet-wide flags still govern.
24
+ create extension if not exists pgtap with schema extensions;
25
+ set search_path = extensions, public, agent_board;
26
+
27
+ begin;
28
+
29
+ select plan(13);
30
+
31
+ -- ---------------------------------------------------------------------------
32
+ -- Fixture: one board with an owner and a non-owner member, a board-linked thread, and a
33
+ -- thread with NO board (for BCP-5).
34
+ -- ---------------------------------------------------------------------------
35
+ create temp table _bcp (
36
+ board uuid, owner_agent uuid, owner_user uuid,
37
+ member_agent uuid, member_user uuid,
38
+ thread uuid, free_thread uuid
39
+ ) on commit drop;
40
+
41
+ do $$
42
+ declare
43
+ v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
44
+ v_owner uuid := gen_random_uuid();
45
+ v_member uuid := gen_random_uuid();
46
+ v_owner_user uuid; v_member_user uuid;
47
+ v_board uuid; v_thread uuid := gen_random_uuid(); v_free uuid := gen_random_uuid();
48
+ begin
49
+ v_owner_user := public.ensure_github_user('bcp-own-' || v_sfx, 'bcp-own-' || v_sfx || '@example.test', 'bcp-own-' || v_sfx, 'BCP Owner');
50
+ v_member_user := public.ensure_github_user('bcp-mem-' || v_sfx, 'bcp-mem-' || v_sfx || '@example.test', 'bcp-mem-' || v_sfx, 'BCP Member');
51
+
52
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
53
+ values (v_owner, 'agent', 'bcp-own-' || v_sfx, 'BCP Owner', 'active', v_owner_user, v_owner_user),
54
+ (v_member, 'agent', 'bcp-mem-' || v_sfx, 'BCP Member', 'active', v_member_user, v_member_user);
55
+
56
+ -- create_board seeds the creator's owner membership (ROOMOWN).
57
+ perform set_config('request.jwt.claim.role', 'authenticated', true);
58
+ perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
59
+ select board_id into v_board
60
+ from agent_board.create_board(v_owner, 'bcp-' || v_sfx, 'BCP Board', 'claim policy', '{}'::jsonb, 'private');
61
+
62
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
63
+ values (v_board, v_member, 'joined', v_owner, 'member');
64
+
65
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
66
+ values (v_thread, 'coordination', v_owner, 'BCP board thread', v_board),
67
+ (v_free, 'coordination', v_owner, 'BCP boardless thread', null);
68
+
69
+ insert into _bcp values (v_board, v_owner, v_owner_user, v_member, v_member_user, v_thread, v_free);
70
+ end $$;
71
+
72
+ -- The fleet-wide guards must be ON for any of the opt-out assertions to mean anything: with them
73
+ -- off, everything succeeds and every permissive assertion below would pass vacuously.
74
+ insert into agent_board.claimgate_enforcement_flags(scope_key, enabled)
75
+ values ('ownerless_guards', true)
76
+ on conflict (scope_key) do update set enabled = true;
77
+
78
+ -- ---------------------------------------------------------------------------
79
+ -- BCP-1..2: the column.
80
+ -- ---------------------------------------------------------------------------
81
+ select is(
82
+ (select claim_policy from agent_board.boards where id = (select board from _bcp)),
83
+ 'inherit',
84
+ 'BCP-1: a newly created board defaults to inherit, so this migration changes no existing behaviour');
85
+
86
+ -- Scored by extracting the constraint's OWN literal list, not by checking that two known words
87
+ -- appear in it. The weaker spelling passes against a vocabulary that also admits a third value --
88
+ -- a review seat pointed out the first version of this assertion did exactly that.
89
+ select is(
90
+ (select string_agg(m[1], ',' order by m[1])
91
+ from pg_constraint c,
92
+ lateral regexp_matches(pg_get_constraintdef(c.oid), '''([a-z-]+)''', 'g') m
93
+ where c.conrelid = 'agent_board.boards'::regclass
94
+ and c.conname = 'boards_claim_policy_check'),
95
+ 'advisory,inherit',
96
+ 'BCP-2: the vocabulary is EXACTLY inherit and advisory -- a third admitted value fails here');
97
+
98
+ -- ---------------------------------------------------------------------------
99
+ -- BCP-3..4: the opt-out, in both directions. Both use an over-cap lease, so the ONLY difference
100
+ -- between them is the board policy.
101
+ -- ---------------------------------------------------------------------------
102
+ select lives_ok(
103
+ $$ select agent_board.set_board_claim_policy((select board from _bcp), 'advisory') $$,
104
+ 'BCP-3a: a board owner may set advisory (lives_ok, so a raise fails HERE rather than aborting)');
105
+
106
+ select lives_ok(
107
+ $$ select * from agent_board.acquire_claim(
108
+ (select owner_agent from _bcp), 'file', 'bcp-over-cap-advisory',
109
+ now() + interval '400 days', null, (select thread from _bcp)) $$,
110
+ 'BCP-3b: with advisory, an OVER-CAP ownerless lease is allowed — their lock governs, not ours');
111
+
112
+ do $$
113
+ begin
114
+ perform agent_board.set_board_claim_policy((select board from _bcp), 'inherit');
115
+ end $$;
116
+
117
+ select throws_ok(
118
+ $$ select * from agent_board.acquire_claim(
119
+ (select owner_agent from _bcp), 'file', 'bcp-over-cap-inherit',
120
+ now() + interval '400 days', null, (select thread from _bcp)) $$,
121
+ '22023',
122
+ null,
123
+ 'BCP-4: with inherit, the SAME over-cap lease is rejected — so BCP-3b is the policy working, not the guard being absent');
124
+
125
+ -- ---------------------------------------------------------------------------
126
+ -- BCP-5: no board means no opt-out. A claim that omits the thread must not inherit permissiveness.
127
+ -- ---------------------------------------------------------------------------
128
+ select throws_ok(
129
+ $$ select * from agent_board.acquire_claim(
130
+ (select owner_agent from _bcp), 'file', 'bcp-no-thread',
131
+ now() + interval '400 days', null, null) $$,
132
+ '22023',
133
+ null,
134
+ 'BCP-5: a claim with NO thread cannot opt itself out by omission — it resolves to inherit and the fleet flags govern');
135
+
136
+ select is(
137
+ agent_board._claimgate_board_claim_policy(null),
138
+ 'inherit',
139
+ 'BCP-5b: the resolver itself answers inherit for a null thread rather than NULL, so the comparison in acquire_claim can never be NULL-swallowed');
140
+
141
+ select is(
142
+ agent_board._claimgate_board_claim_policy((select free_thread from _bcp)),
143
+ 'inherit',
144
+ 'BCP-5c: and inherit for a real thread that is not board-linked');
145
+
146
+ -- ---------------------------------------------------------------------------
147
+ -- BCP-6: THE SECURITY BOUNDARY. advisory must NOT reach the fleet-authority gate.
148
+ --
149
+ -- Scored from the catalog rather than by attempting a machine claim, because a behavioural probe
150
+ -- here would need a full fleet-registration fixture and would prove only that ONE machine claim
151
+ -- was refused. This asks the structural question instead: does the board policy appear anywhere
152
+ -- the fleet kinds are decided? It must appear ONLY inside the ownerless branch.
153
+ -- ---------------------------------------------------------------------------
154
+ select is(
155
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
156
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
157
+ where n.nspname = 'agent_board'
158
+ and p.proname in ('_can_claim_resource', '_claimgate_kind_enforced', '_claimgate_promotion_blockers')
159
+ and agent_board_test.stripped_body(p.oid, true) ~ '_claimgate_board_claim_policy'),
160
+ '',
161
+ 'BCP-6 SECURITY: the board policy is reachable from NO fleet-authority function — a board may decline our lock, never grant itself another owner''s resource');
162
+
163
+ -- ---------------------------------------------------------------------------
164
+ -- BCP-7..8: who may set it. Mirrors set_board_role_gate's gate exactly.
165
+ -- ---------------------------------------------------------------------------
166
+ select throws_ok(
167
+ format($$ select set_config('request.jwt.claim.sub', %L, true);
168
+ select agent_board.set_board_claim_policy(%L, 'advisory') $$,
169
+ (select member_user from _bcp)::text, (select board from _bcp)::text),
170
+ '42501',
171
+ null,
172
+ 'BCP-7: a non-owner MEMBER may not set the claim policy');
173
+
174
+ select throws_ok(
175
+ format($$ select set_config('request.jwt.claims', '', true);
176
+ select set_config('request.jwt.claim.sub', '', true);
177
+ select set_config('request.jwt.claim.role', '', true);
178
+ select agent_board.set_board_claim_policy(%L, 'advisory') $$, (select board from _bcp)::text),
179
+ '42501',
180
+ null,
181
+ 'BCP-8: an unresolved principal (service_role) is not an owner and is refused — same carve-out as set_board_role_gate');
182
+
183
+ -- ---------------------------------------------------------------------------
184
+ -- BCP-9: an unknown value is refused by the RPC with the vocabulary, not by the column CHECK with
185
+ -- a constraint name. A caller asking for 'off' should be told what the options are.
186
+ -- ---------------------------------------------------------------------------
187
+ select throws_ok(
188
+ format($$ select set_config('request.jwt.claim.role', 'authenticated', true);
189
+ select set_config('request.jwt.claim.sub', %L, true);
190
+ select agent_board.set_board_claim_policy(%L, 'off') $$,
191
+ (select owner_user from _bcp)::text, (select board from _bcp)::text),
192
+ '22023',
193
+ null,
194
+ 'BCP-9: an unknown policy is refused as a bad argument, naming the vocabulary — not as a raw CHECK violation');
195
+
196
+ -- ---------------------------------------------------------------------------
197
+ -- BCP-10: A FLEET-REVOKED OWNER IS STILL AN OWNER BY MEMBERSHIP. This function shipped its first
198
+ -- version WITHOUT this check while its own commit message claimed to mirror set_board_role_gate
199
+ -- "exactly" -- a review seat found it by reading both functions side by side instead of taking
200
+ -- the claim. Fleet revocation deliberately leaves agents.status = 'active' (the ledger row is the
201
+ -- only authority signal), so the membership test in BCP-7/8 passes for a revoked owner and cannot
202
+ -- stand alone.
203
+ --
204
+ -- Disabling a board's claim enforcement is an authority write in the same class as flipping the
205
+ -- role gate, so it owes the same post-UPDATE revocation assertion.
206
+ -- ---------------------------------------------------------------------------
207
+ do $$
208
+ begin
209
+ -- revoke as an operator (service_role: no resolved actor)
210
+ perform set_config('request.jwt.claims', '', true);
211
+ perform set_config('request.jwt.claim.sub', '', true);
212
+ perform set_config('request.jwt.claim.role', '', true);
213
+ perform agent_board.revoke_fleet_subject(
214
+ 'agent', (select owner_agent from _bcp), (select owner_agent from _bcp),
215
+ 'BCP-10 revocation', '{}'::jsonb);
216
+ end $$;
217
+
218
+ select throws_ok(
219
+ format($$ select set_config('request.jwt.claim.role', 'authenticated', true);
220
+ select set_config('request.jwt.claim.sub', %L, true);
221
+ select agent_board.set_board_claim_policy(%L, 'advisory') $$,
222
+ (select owner_user from _bcp)::text, (select board from _bcp)::text),
223
+ '42501',
224
+ null,
225
+ 'BCP-10: a FLEET-REVOKED owner may not disable claim enforcement, even though the board still lists them as owner and their agent row is still active');
226
+
227
+ select * from finish();
228
+ rollback;
@@ -539,12 +539,19 @@ select is(
539
539
  or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.board_memberships\M[^;]*\mset\M[^;]*\mrole\s*='
540
540
  or agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+agent_board\.board_authority_ceilings\M'
541
541
  or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mrole_gate_enforced\s*='
542
+ -- BOARDCLAIMPOLICY: boards.claim_policy is the same SHAPE of authority as
543
+ -- role_gate_enforced above -- a board-level switch deciding what a later gate does
544
+ -- (there the creator-identity exemption, here whether our ownerless claim guards apply
545
+ -- at all). Counting it as an authority write is what makes the pairing assertion below
546
+ -- hold, and is why set_board_claim_policy carries the ledger check.
547
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mclaim_policy\s*='
542
548
  or agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_invite_tokens\M')),
543
549
  '_fedsession_autojoin_default_board,_redeem_unbounded_invite_for_agent,'
544
550
  || '_upsert_board_membership_state,bootstrap_board_owner_memberships,create_board,'
545
- || 'create_invite_link,grant_board_role,set_board_authority_ceiling,set_board_role_gate',
551
+ || 'create_invite_link,grant_board_role,set_board_authority_ceiling,set_board_claim_policy,'
552
+ || 'set_board_role_gate',
546
553
  'the functions that WRITE a board-authority relation — in ANY shape, including a mintable '
547
- 'invite preset_role — are exactly these nine');
554
+ 'invite preset_role — are exactly these ten');
548
555
 
549
556
  -- EITHER helper counts as consulting the ledger: the invite redeem path calls the UNLOCKED
550
557
  -- predicate (see the migration header), so a side that matched only the share-locking assert
@@ -558,8 +565,8 @@ select is(
558
565
  and p.proname not in ('_assert_actor_not_fleet_revoked', '_actor_fleet_revoked')
559
566
  and agent_board_test.stripped_body(p.oid) ~ '\m(_assert_actor_not_fleet_revoked|_actor_fleet_revoked)\s*\('),
560
567
  '_redeem_unbounded_invite_for_agent,create_board,create_invite_link,grant_board_role,'
561
- || 'set_board_authority_ceiling,set_board_role_gate',
562
- 'the functions that consult the fleet revocation ledger are exactly these six (#280 added '
568
+ || 'set_board_authority_ceiling,set_board_claim_policy,set_board_role_gate',
569
+ 'the functions that consult the fleet revocation ledger are exactly these seven (#280 added '
563
570
  'create_board)');
564
571
 
565
572
  -- The gate proper: the two sets must differ by EXACTLY the three asserted exemptions, and any
@@ -588,6 +595,7 @@ select is(
588
595
  or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.board_memberships\M[^;]*\mset\M[^;]*\mrole\s*='
589
596
  or agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+agent_board\.board_authority_ceilings\M'
590
597
  or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mrole_gate_enforced\s*='
598
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mclaim_policy\s*='
591
599
  or agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_invite_tokens\M')
592
600
  <> (agent_board_test.stripped_body(p.oid) ~ '\m(_assert_actor_not_fleet_revoked|_actor_fleet_revoked)\s*\('))
593
601
  ) x),
@@ -511,7 +511,7 @@ select is(
511
511
  where n.nspname = 'agent_board'
512
512
  and p.proname like 'signed\_%'
513
513
  and agent_board_test.stripped_body(p.oid)
514
- ~ '\m(send_direct_message|send_multicast_message|post_board_message|append_task_event|create_tasking_delivery)\s*\('),
514
+ ~ '\m(send_direct_message|send_multicast_message|post_board_message|_?append_task_event|create_tasking_delivery)\s*\('),
515
515
  'signed_append_task_event,signed_create_tasking_delivery,signed_post_board_message,'
516
516
  || 'signed_send_direct_message,signed_send_multicast_message',
517
517
  'the five exempt wrappers each perform a message/task write, which is why they are exempt');