@consiliency/agent-board-schema 1.3.0 → 1.4.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.
Files changed (62) hide show
  1. package/bin/agent-board-schema.js +0 -0
  2. package/fixtures/already-applied.json +104 -0
  3. package/migrations/20260717000100_agent_board_board_role.sql +180 -0
  4. package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
  5. package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
  6. package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
  7. package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
  8. package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
  9. package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
  10. package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
  11. package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
  12. package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
  13. package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
  14. package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
  15. package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
  16. package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
  17. package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
  18. package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
  19. package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
  20. package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
  21. package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
  22. package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
  23. package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
  24. package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
  25. package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
  26. package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
  27. package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
  28. package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
  29. package/package.json +9 -9
  30. package/schema-manifest.json +238 -4
  31. package/src/tooling.test.mjs +42 -1
  32. package/tests/agent_board_agent_lineage.test.sql +73 -0
  33. package/tests/agent_board_agents_status_revoked.test.sql +60 -0
  34. package/tests/agent_board_auth_rpcs.test.sql +17 -16
  35. package/tests/agent_board_board_role.test.sql +129 -0
  36. package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
  37. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
  38. package/tests/agent_board_delegation_enforcement.test.sql +175 -0
  39. package/tests/agent_board_delegation_policies.test.sql +119 -0
  40. package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
  41. package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
  42. package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
  43. package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
  44. package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
  45. package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
  46. package/tests/agent_board_fleet_registry.test.sql +2 -2
  47. package/tests/agent_board_identity_providers.test.sql +104 -0
  48. package/tests/agent_board_inbox_rpcs.test.sql +6 -6
  49. package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
  50. package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
  51. package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
  52. package/tests/agent_board_resigning_refresh.test.sql +73 -0
  53. package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
  54. package/tests/agent_board_schema_version.test.sql +2 -2
  55. package/tests/agent_board_schema_version_rpc.test.sql +1 -1
  56. package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
  57. package/tests/agent_board_send_authz_config.test.sql +84 -0
  58. package/tests/agent_board_send_authz_matrix.test.sql +159 -0
  59. package/tests/agent_board_send_role_gate.test.sql +177 -0
  60. package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
  61. package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
  62. package/tests/agent_board_thread_acl.test.sql +129 -0
File without changes
@@ -180,6 +180,110 @@
180
180
  {
181
181
  "id": "20260714000200_agent_board_workload_providers_rls",
182
182
  "sha256": "37a6f94d47b0948b1d5f384e52101eb48d9a15ba96a351669c87b907cc8a03e1"
183
+ },
184
+ {
185
+ "id": "20260717000100_agent_board_board_role",
186
+ "sha256": "969980a7b52c6fee9329d2367006e5ce6d36b6471b53fee2247e493ac8ae512f"
187
+ },
188
+ {
189
+ "id": "20260717000200_agent_board_thread_acl",
190
+ "sha256": "afb323d6f9189f1f37a7fee3ffceaaa17474059d46a1fdd84adb73c28bfb27ba"
191
+ },
192
+ {
193
+ "id": "20260717000300_agent_board_agent_lineage",
194
+ "sha256": "48aff20015f74e5f201a2985f71b33cd3e718aeb9dfb25fbd5720cb824fe85b3"
195
+ },
196
+ {
197
+ "id": "20260717000400_agent_board_delegation_policies",
198
+ "sha256": "ef7827af400e8a6206c17c8cc35623c280a92d25f4b678314847ff308d9d9dc7"
199
+ },
200
+ {
201
+ "id": "20260718000100_agent_board_send_authz_config",
202
+ "sha256": "d56909d4fbb7054e74818903d3eea84451d561c60908026dbbdb1956f500565d"
203
+ },
204
+ {
205
+ "id": "20260718000200_agent_board_send_role_gate",
206
+ "sha256": "805afdef2c11e52966bec1cd47a55dfd5e687af9181316d9ef71975b516de288"
207
+ },
208
+ {
209
+ "id": "20260718000300_agent_board_fleet_policy_default_deny",
210
+ "sha256": "47ad42dd50fb31ede0cb6044a22cce04a31168af2dead73ed8e01dabf84fddea"
211
+ },
212
+ {
213
+ "id": "20260718000400_agent_board_grant_ceiling_wiring",
214
+ "sha256": "0461f6d403fb1dd07e9423db898cd41a15a3ecfbf75402cd0085791d57a646b7"
215
+ },
216
+ {
217
+ "id": "20260718000500_agent_board_fleet_agent_session_class",
218
+ "sha256": "028f931aaba673fba29dd92ef3c17efcc08cb50b0dab01789986652a54fb003a"
219
+ },
220
+ {
221
+ "id": "20260718000600_agent_board_mint_fleet_agent_session",
222
+ "sha256": "25972553fd1ec4f350e751495d79695a8b0ccc66c0b8b272fdf517115ba37307"
223
+ },
224
+ {
225
+ "id": "20260718000700_agent_board_nonfleet_session_reaper",
226
+ "sha256": "9b6ff596ac386c49530c9405535b5255dcb09c271288957711a0d553446ef161"
227
+ },
228
+ {
229
+ "id": "20260718000800_agent_board_regrant_scoped_send",
230
+ "sha256": "1332cf2ee23d875cf0b3d646890a0fffe0efc6d2312da4e315d5dc0abadaf74f"
231
+ },
232
+ {
233
+ "id": "20260718000900_agent_board_agents_status_revoked",
234
+ "sha256": "f0e9b95c286bb4b6873cdc882e30262788636886360c0100b745569d6553a51b"
235
+ },
236
+ {
237
+ "id": "20260718001000_agent_board_delegation_enforcement",
238
+ "sha256": "7e93bd1ff924b06922bb9312684f57ce81c3d3e921dbf52357ae5059a01dbc8b"
239
+ },
240
+ {
241
+ "id": "20260718001100_agent_board_revoke_agent_subtree",
242
+ "sha256": "bcf14370a3da288ebe43e337d050f5fb6fb736fad69c9f6b7a0b6e0992371b7e"
243
+ },
244
+ {
245
+ "id": "20260718001200_agent_board_resigning_refresh",
246
+ "sha256": "47248250ab9bc11abdea8533d62611efba9812b913dc2dc5405612eb2fbafd12"
247
+ },
248
+ {
249
+ "id": "20260718001300_agent_board_identity_providers",
250
+ "sha256": "ab8169709faf7f74b27a593a7c980022ee073f0371b39c0dda18f2202c0f4eae"
251
+ },
252
+ {
253
+ "id": "20260718001400_agent_board_enrollment_federation_audit",
254
+ "sha256": "c42e5122625639155d9ff1d2511ebdec3ac379ca0660e67c159e37efb8ea24f5"
255
+ },
256
+ {
257
+ "id": "20260718001500_agent_board_enroll_federated_identity",
258
+ "sha256": "497d1f657d78c56744017409f5bd2ca361cd691ee1d0bab5b9a55ab3dd4b8ae7"
259
+ },
260
+ {
261
+ "id": "20260719000100_agent_board_signing_challenge_pending_cap",
262
+ "sha256": "1e48b413d31f11b9c120480c8453c8502fdde1c3a7997dcd9e22e957a2459b0b"
263
+ },
264
+ {
265
+ "id": "20260719000200_agent_board_signing_challenge_reaper",
266
+ "sha256": "488bbf16bf526b7df7c9b3fa5b068964703aa04ba657d6e6ceaff856a9523634"
267
+ },
268
+ {
269
+ "id": "20260719000300_agent_board_enroll_rate_cap",
270
+ "sha256": "402c6d8bc5d4662b10d6fe0228a2bdd30026be96a5b5a237e26e1d44a639ae1d"
271
+ },
272
+ {
273
+ "id": "20260719001000_agent_board_identity_providers_portal_contract",
274
+ "sha256": "e0406afebf9b36f3793e04ca757a252f038b4100697598e0a2e76050193674a0"
275
+ },
276
+ {
277
+ "id": "20260719001100_agent_board_enroll_federated_identity_pop",
278
+ "sha256": "3d4517243a7d228fb04ea41e9dc7236c870cb5830fb2f815bfa12426eee47bd7"
279
+ },
280
+ {
281
+ "id": "20260719001200_agent_board_enroll_federated_identity_raw_fingerprint",
282
+ "sha256": "70943047b522ec23ee3d451f704c961d130a053bc210dad8759cbc9a22444454"
283
+ },
284
+ {
285
+ "id": "20260719001300_agent_board_enroll_rate_events_reaper_rls",
286
+ "sha256": "b2f58eadc13cb227991c4d978c5460878dc3cbada43c58285b55154b89a28459"
183
287
  }
184
288
  ]
185
289
  }
@@ -0,0 +1,180 @@
1
+ -- MBAUTHZ SL-1 (Lane A): board authority role + per-board role-gate flag.
2
+ --
3
+ -- Additive, no send-path behavior change. Adds:
4
+ -- * agent_board.board_memberships.role (backfilled: owner for the board
5
+ -- creator's membership, member otherwise; new rows default member),
6
+ -- * agent_board.boards.role_gate_enforced (per-board flag, default false),
7
+ -- * a role gate inside invite_board_member / resolve_board_request that only
8
+ -- applies when the board's role_gate_enforced flag is true. When false
9
+ -- (default) the existing any-active-member behavior is preserved verbatim,
10
+ -- and a null actor (service_role / current_agent_id() = null) still bypasses.
11
+
12
+ -- 1. board_memberships.role -------------------------------------------------
13
+ alter table agent_board.board_memberships
14
+ add column if not exists role text not null default 'member'
15
+ check (role in ('owner', 'admin', 'member', 'observer'));
16
+
17
+ comment on column agent_board.board_memberships.role is
18
+ 'Board authority role (partial order owner>admin>member>observer). Backfilled owner for the board creator, member otherwise; new rows default member.';
19
+
20
+ -- One-time, idempotent backfill: the creator's membership becomes owner.
21
+ update agent_board.board_memberships bm
22
+ set role = 'owner'
23
+ from agent_board.boards b
24
+ where bm.board_id = b.id
25
+ and bm.agent_id = b.created_by_agent_id
26
+ and bm.role <> 'owner';
27
+
28
+ -- 2. boards.role_gate_enforced (board-native flag; deliberately NOT the
29
+ -- portal-owned public.app_settings) ------------------------------------
30
+ alter table agent_board.boards
31
+ add column if not exists role_gate_enforced boolean not null default false;
32
+
33
+ comment on column agent_board.boards.role_gate_enforced is
34
+ 'When true, invite_board_member/resolve_board_request require the actor to hold board role owner or admin. Default false preserves the any-active-member behavior.';
35
+
36
+ -- 3. Role-gated invite_board_member -----------------------------------------
37
+ create or replace function agent_board.invite_board_member(
38
+ p_board_id uuid,
39
+ p_agent_id uuid,
40
+ p_created_by_agent_id uuid,
41
+ p_metadata jsonb default '{}'::jsonb
42
+ )
43
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamptz, updated_at timestamptz)
44
+ language plpgsql
45
+ security definer
46
+ set search_path = agent_board, public
47
+ as $$
48
+ declare
49
+ v_board agent_board.boards%rowtype;
50
+ v_membership agent_board.board_memberships%rowtype;
51
+ v_actor uuid := agent_board.current_agent_id();
52
+ begin
53
+ select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
54
+ if v_board.id is null then
55
+ raise exception 'board not found' using errcode = 'P0002';
56
+ end if;
57
+ if v_actor is not null and v_actor <> v_board.created_by_agent_id then
58
+ if v_board.role_gate_enforced then
59
+ if not exists (
60
+ select 1
61
+ from agent_board.board_memberships bm
62
+ where bm.board_id = p_board_id
63
+ and bm.agent_id = v_actor
64
+ and bm.membership_state in ('joined', 'subscribed')
65
+ and bm.role in ('owner', 'admin')
66
+ ) then
67
+ raise exception 'board invite requires owner or admin role' using errcode = '42501';
68
+ end if;
69
+ else
70
+ if not exists (
71
+ select 1
72
+ from agent_board.board_memberships bm
73
+ where bm.board_id = p_board_id
74
+ and bm.agent_id = v_actor
75
+ and bm.membership_state in ('joined', 'subscribed')
76
+ ) then
77
+ raise exception 'board invite requires active membership' using errcode = '42501';
78
+ end if;
79
+ end if;
80
+ end if;
81
+ if not exists (select 1 from agent_board.agents where id = p_agent_id) then
82
+ raise exception 'agent not found' using errcode = 'P0002';
83
+ end if;
84
+ if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
85
+ raise exception 'creator agent not found' using errcode = 'P0002';
86
+ end if;
87
+
88
+ v_membership := agent_board._upsert_board_membership_state(
89
+ p_board_id,
90
+ p_agent_id,
91
+ 'invited',
92
+ p_created_by_agent_id,
93
+ p_metadata
94
+ );
95
+
96
+ return query
97
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
98
+ end;
99
+ $$;
100
+
101
+ -- 4. Role-gated resolve_board_request ---------------------------------------
102
+ create or replace function agent_board.resolve_board_request(
103
+ p_board_id uuid,
104
+ p_agent_id uuid,
105
+ p_membership_state text,
106
+ p_created_by_agent_id uuid,
107
+ p_metadata jsonb default '{}'::jsonb
108
+ )
109
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
110
+ language plpgsql
111
+ security definer
112
+ set search_path = agent_board, public
113
+ as $$
114
+ declare
115
+ v_board agent_board.boards%rowtype;
116
+ v_existing_state text;
117
+ v_membership agent_board.board_memberships%rowtype;
118
+ v_actor uuid := agent_board.current_agent_id();
119
+ begin
120
+ if p_membership_state not in ('joined', 'subscribed', 'removed') then
121
+ raise exception 'resolve_board_request only accepts joined, subscribed, or removed' using errcode = '22023';
122
+ end if;
123
+ select * into v_board from agent_board.boards where id = p_board_id and status = 'active';
124
+ if v_board.id is null then
125
+ raise exception 'board not found' using errcode = 'P0002';
126
+ end if;
127
+ if v_actor is not null and v_actor <> v_board.created_by_agent_id then
128
+ if v_board.role_gate_enforced then
129
+ if not exists (
130
+ select 1
131
+ from agent_board.board_memberships bm
132
+ where bm.board_id = p_board_id
133
+ and bm.agent_id = v_actor
134
+ and bm.membership_state in ('joined', 'subscribed')
135
+ and bm.role in ('owner', 'admin')
136
+ ) then
137
+ raise exception 'board request resolution requires owner or admin role' using errcode = '42501';
138
+ end if;
139
+ else
140
+ if not exists (
141
+ select 1
142
+ from agent_board.board_memberships bm
143
+ where bm.board_id = p_board_id
144
+ and bm.agent_id = v_actor
145
+ and bm.membership_state in ('joined', 'subscribed')
146
+ ) then
147
+ raise exception 'board request resolution requires active membership' using errcode = '42501';
148
+ end if;
149
+ end if;
150
+ end if;
151
+
152
+ select bm.membership_state
153
+ into v_existing_state
154
+ from agent_board.board_memberships bm
155
+ where bm.board_id = p_board_id
156
+ and bm.agent_id = p_agent_id
157
+ order by bm.created_at desc
158
+ limit 1;
159
+
160
+ if v_existing_state not in ('requested', 'invited') then
161
+ raise exception 'pending board membership not found' using errcode = 'P0002';
162
+ end if;
163
+
164
+ v_membership := agent_board._upsert_board_membership_state(
165
+ p_board_id,
166
+ p_agent_id,
167
+ p_membership_state,
168
+ p_created_by_agent_id,
169
+ p_metadata
170
+ );
171
+
172
+ return query
173
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
174
+ end;
175
+ $$;
176
+
177
+ -- Re-assert grants (create or replace preserves them, but be explicit so a
178
+ -- fresh replay is order-independent).
179
+ grant execute on function agent_board.invite_board_member(uuid, uuid, uuid, jsonb) to authenticated, service_role;
180
+ grant execute on function agent_board.resolve_board_request(uuid, uuid, text, uuid, jsonb) to authenticated, service_role;
@@ -0,0 +1,149 @@
1
+ -- MBAUTHZ SL-2 (Lane B): per-thread ACL.
2
+ --
3
+ -- Additive, no send-path behavior change. Adds:
4
+ -- * agent_board.threads.access_mode ('inherit' default | 'restricted'),
5
+ -- * agent_board.thread_memberships (+ RLS),
6
+ -- * a redefinition of _current_agent_can_access_thread so that
7
+ -- access_mode='inherit' reproduces the current OR verbatim (unchanged
8
+ -- behavior for every existing thread) while access_mode='restricted'
9
+ -- consults thread_memberships (unexpired, unrevoked). The _is_service_role()
10
+ -- short-circuit is retained, so live service_role traffic is unaffected.
11
+
12
+ -- 1. threads.access_mode ----------------------------------------------------
13
+ alter table agent_board.threads
14
+ add column if not exists access_mode text not null default 'inherit'
15
+ check (access_mode in ('inherit', 'restricted'));
16
+
17
+ comment on column agent_board.threads.access_mode is
18
+ 'inherit (default): thread access is the historical creator/author/task/receipt/claim/inbox/board OR. restricted: access requires an active agent_board.thread_memberships row.';
19
+
20
+ -- 2. thread_memberships -----------------------------------------------------
21
+ create table if not exists agent_board.thread_memberships (
22
+ id uuid primary key default gen_random_uuid(),
23
+ thread_id uuid not null references agent_board.threads(id) on delete cascade,
24
+ agent_id uuid not null references agent_board.agents(id) on delete cascade,
25
+ role text not null default 'member' check (role in ('owner', 'admin', 'member')),
26
+ granted_by_agent_id uuid references agent_board.agents(id) on delete set null,
27
+ expires_at timestamptz,
28
+ revoked_at timestamptz,
29
+ created_at timestamptz not null default now()
30
+ );
31
+
32
+ comment on table agent_board.thread_memberships is
33
+ 'Per-thread access-control list consulted when threads.access_mode = restricted. A membership is active when revoked_at is null and (expires_at is null or expires_at > now()).';
34
+
35
+ -- One active membership per (thread, agent).
36
+ create unique index if not exists idx_agent_board_thread_memberships_active_unique
37
+ on agent_board.thread_memberships(thread_id, agent_id)
38
+ where revoked_at is null;
39
+
40
+ create index if not exists idx_agent_board_thread_memberships_agent
41
+ on agent_board.thread_memberships(agent_id, thread_id);
42
+
43
+ alter table agent_board.thread_memberships enable row level security;
44
+
45
+ drop policy if exists agent_board_authenticated_select_thread_memberships on agent_board.thread_memberships;
46
+ create policy agent_board_authenticated_select_thread_memberships
47
+ on agent_board.thread_memberships
48
+ as permissive
49
+ for select
50
+ to authenticated
51
+ using (
52
+ agent_id = agent_board.current_agent_id()
53
+ or granted_by_agent_id = agent_board.current_agent_id()
54
+ or agent_board._current_agent_can_access_thread(thread_id)
55
+ );
56
+
57
+ grant select on agent_board.thread_memberships to authenticated;
58
+ grant select, insert, update on agent_board.thread_memberships to service_role;
59
+
60
+ -- 3. Redefine _current_agent_can_access_thread ------------------------------
61
+ -- inherit mode reproduces the historical OR verbatim; restricted mode consults
62
+ -- the ACL. _is_service_role() short-circuit preserved.
63
+ create or replace function agent_board._current_agent_can_access_thread(
64
+ p_thread_id uuid
65
+ )
66
+ returns boolean
67
+ language sql
68
+ stable
69
+ security definer
70
+ set search_path = ''
71
+ as $$
72
+ select
73
+ agent_board._is_service_role()
74
+ or exists (
75
+ select 1
76
+ from agent_board.threads t
77
+ where t.id = p_thread_id
78
+ and (
79
+ (
80
+ t.access_mode = 'inherit'
81
+ and (
82
+ t.created_by_agent_id = agent_board.current_agent_id()
83
+ or exists (
84
+ select 1
85
+ from agent_board.messages m
86
+ where m.thread_id = t.id
87
+ and m.author_agent_id = agent_board.current_agent_id()
88
+ )
89
+ or exists (
90
+ select 1
91
+ from agent_board.tasks task
92
+ where task.thread_id = t.id
93
+ and (
94
+ task.created_by_agent_id = agent_board.current_agent_id()
95
+ or task.assigned_agent_id = agent_board.current_agent_id()
96
+ )
97
+ )
98
+ or exists (
99
+ select 1
100
+ from agent_board.read_receipts rr
101
+ where rr.thread_id = t.id
102
+ and rr.agent_id = agent_board.current_agent_id()
103
+ )
104
+ or exists (
105
+ select 1
106
+ from agent_board.claims c
107
+ where c.thread_id = t.id
108
+ and c.agent_id = agent_board.current_agent_id()
109
+ )
110
+ or exists (
111
+ select 1
112
+ from agent_board.inbox_items ii
113
+ left join agent_board.messages im on im.id = ii.source_message_id
114
+ left join agent_board.tasks it on it.id = ii.source_task_id
115
+ left join agent_board.delegations ide on ide.id = ii.source_delegation_id
116
+ left join agent_board.tasks idt on idt.id = ide.child_task_id
117
+ where ii.recipient_agent_id = agent_board.current_agent_id()
118
+ and (
119
+ im.thread_id = t.id
120
+ or it.thread_id = t.id
121
+ or idt.thread_id = t.id
122
+ )
123
+ )
124
+ or exists (
125
+ select 1
126
+ from agent_board.delivery_recipients dr
127
+ join agent_board.messages bm on bm.id = dr.source_message_id
128
+ where dr.board_id is not null
129
+ and bm.thread_id = t.id
130
+ and agent_board._current_agent_has_board_access(dr.board_id)
131
+ )
132
+ )
133
+ )
134
+ or (
135
+ t.access_mode = 'restricted'
136
+ and exists (
137
+ select 1
138
+ from agent_board.thread_memberships tm
139
+ where tm.thread_id = t.id
140
+ and tm.agent_id = agent_board.current_agent_id()
141
+ and tm.revoked_at is null
142
+ and (tm.expires_at is null or tm.expires_at > now())
143
+ )
144
+ )
145
+ )
146
+ )
147
+ $$;
148
+
149
+ revoke all on function agent_board._current_agent_can_access_thread(uuid) from public, anon, authenticated;
@@ -0,0 +1,54 @@
1
+ -- MBAUTHZ SL-3 (Lane C): agent lineage.
2
+ --
3
+ -- Additive, no send-path behavior change. Adds self-referential
4
+ -- agents.parent_agent_id / agents.root_agent_id (nullable) plus an
5
+ -- owner-consistency guard: a parented agent (root_agent_id set) must share the
6
+ -- owner_user_id of its lineage root. Unparented rows (NULL lineage) are
7
+ -- unaffected.
8
+
9
+ alter table agent_board.agents
10
+ add column if not exists parent_agent_id uuid references agent_board.agents(id) on delete set null,
11
+ add column if not exists root_agent_id uuid references agent_board.agents(id) on delete set null;
12
+
13
+ comment on column agent_board.agents.parent_agent_id is
14
+ 'Direct delegating parent agent (self-referential, nullable). NULL for top-level agents.';
15
+ comment on column agent_board.agents.root_agent_id is
16
+ 'Lineage root agent (self-referential, nullable). When set, this agent must share the root agent''s owner_user_id.';
17
+
18
+ create index if not exists idx_agent_board_agents_parent on agent_board.agents(parent_agent_id) where parent_agent_id is not null;
19
+ create index if not exists idx_agent_board_agents_root on agent_board.agents(root_agent_id) where root_agent_id is not null;
20
+
21
+ -- Owner-consistency guard.
22
+ create or replace function agent_board._enforce_agent_lineage_owner()
23
+ returns trigger
24
+ language plpgsql
25
+ security definer
26
+ set search_path = ''
27
+ as $$
28
+ declare
29
+ v_root_owner uuid;
30
+ begin
31
+ if new.root_agent_id is null then
32
+ return new;
33
+ end if;
34
+
35
+ select a.owner_user_id
36
+ into v_root_owner
37
+ from agent_board.agents a
38
+ where a.id = new.root_agent_id;
39
+
40
+ if new.owner_user_id is distinct from v_root_owner then
41
+ raise exception 'agent lineage owner mismatch: agent owner_user_id must equal its root agent owner_user_id'
42
+ using errcode = '23514';
43
+ end if;
44
+
45
+ return new;
46
+ end;
47
+ $$;
48
+
49
+ drop trigger if exists trg_agent_board_agents_lineage_owner on agent_board.agents;
50
+ create trigger trg_agent_board_agents_lineage_owner
51
+ before insert or update of parent_agent_id, root_agent_id, owner_user_id
52
+ on agent_board.agents
53
+ for each row
54
+ execute function agent_board._enforce_agent_lineage_owner();
@@ -0,0 +1,175 @@
1
+ -- MBAUTHZ SL-4 (Lane D): delegation policies, board authority ceilings, and the
2
+ -- _grant_within_ceiling helper.
3
+ --
4
+ -- Additive, no send-path behavior change. MBAUTHZ OWNS this schema + helper;
5
+ -- MBSENDAUTHZ (later phase) only consumes it. The board ceiling is a SEPARATE
6
+ -- table keyed on board_id (maintainer decision), NOT a boards column; a board
7
+ -- with no ceiling row defaults to 'owner' (unchanged behavior).
8
+
9
+ -- 1. delegation_policies -----------------------------------------------------
10
+ create table if not exists agent_board.delegation_policies (
11
+ id uuid primary key default gen_random_uuid(),
12
+ root_agent_id uuid not null references agent_board.agents(id) on delete cascade,
13
+ mode text not null default 'independent_auth' check (mode in ('independent_auth', 'root_minted')),
14
+ max_depth int not null default 1,
15
+ max_active_children int,
16
+ allowed_scopes jsonb,
17
+ -- The grant/working clock (hours-scale), NOT the <=15-min session-token clock.
18
+ child_ttl_ceiling interval not null default interval '8 hours',
19
+ require_child_signing_key boolean not null default true,
20
+ revoked_at timestamptz,
21
+ created_at timestamptz not null default now()
22
+ );
23
+
24
+ comment on table agent_board.delegation_policies is
25
+ 'Per-root-agent delegation policy. child_ttl_ceiling is the hours-scale grant/working clock, not the session-token clock.';
26
+
27
+ create index if not exists idx_agent_board_delegation_policies_root
28
+ on agent_board.delegation_policies(root_agent_id)
29
+ where revoked_at is null;
30
+
31
+ alter table agent_board.delegation_policies enable row level security;
32
+
33
+ drop policy if exists agent_board_authenticated_select_delegation_policies on agent_board.delegation_policies;
34
+ create policy agent_board_authenticated_select_delegation_policies
35
+ on agent_board.delegation_policies
36
+ as permissive
37
+ for select
38
+ to authenticated
39
+ using (root_agent_id = agent_board.current_agent_id());
40
+
41
+ grant select on agent_board.delegation_policies to authenticated;
42
+ grant select, insert, update on agent_board.delegation_policies to service_role;
43
+
44
+ -- 2. board_authority_ceilings (separate table; not a boards column) ---------
45
+ create table if not exists agent_board.board_authority_ceilings (
46
+ board_id uuid primary key references agent_board.boards(id) on delete cascade,
47
+ max_grantable_role text not null default 'owner' check (max_grantable_role in ('owner', 'admin', 'member', 'observer')),
48
+ created_at timestamptz not null default now(),
49
+ updated_at timestamptz not null default now()
50
+ );
51
+
52
+ comment on table agent_board.board_authority_ceilings is
53
+ 'Optional per-board cap on the highest role a grantor may grant. No row means no explicit ceiling, resolved as owner (unchanged behavior).';
54
+
55
+ alter table agent_board.board_authority_ceilings enable row level security;
56
+
57
+ drop policy if exists agent_board_authenticated_select_board_authority_ceilings on agent_board.board_authority_ceilings;
58
+ create policy agent_board_authenticated_select_board_authority_ceilings
59
+ on agent_board.board_authority_ceilings
60
+ as permissive
61
+ for select
62
+ to authenticated
63
+ using (agent_board._current_agent_has_board_access(board_id));
64
+
65
+ grant select on agent_board.board_authority_ceilings to authenticated;
66
+ grant select, insert, update on agent_board.board_authority_ceilings to service_role;
67
+
68
+ -- 3. _grant_within_ceiling ---------------------------------------------------
69
+ -- Role partial order owner>admin>member>observer. A grant is permitted iff the
70
+ -- grantee role does not outrank the grantor's own board role AND does not
71
+ -- outrank the board's ceiling (default owner when no ceiling row exists).
72
+ create or replace function agent_board._grant_within_ceiling(
73
+ p_grantor_agent_id uuid,
74
+ p_board_id uuid,
75
+ p_grantee_role text
76
+ )
77
+ returns boolean
78
+ language sql
79
+ stable
80
+ security definer
81
+ set search_path = ''
82
+ as $$
83
+ with ranks(role, rank) as (
84
+ values ('owner', 4), ('admin', 3), ('member', 2), ('observer', 1)
85
+ ),
86
+ grantor as (
87
+ select r.rank
88
+ from agent_board.board_memberships bm
89
+ join ranks r on r.role = bm.role
90
+ where bm.board_id = p_board_id
91
+ and bm.agent_id = p_grantor_agent_id
92
+ and bm.membership_state in ('joined', 'subscribed')
93
+ order by r.rank desc
94
+ limit 1
95
+ ),
96
+ ceiling as (
97
+ select coalesce(
98
+ (select r.rank
99
+ from agent_board.board_authority_ceilings c
100
+ join ranks r on r.role = c.max_grantable_role
101
+ where c.board_id = p_board_id),
102
+ 4 -- no row -> owner
103
+ ) as rank
104
+ ),
105
+ grantee as (
106
+ select rank from ranks where role = p_grantee_role
107
+ )
108
+ select
109
+ (select rank from grantee) is not null
110
+ and exists (select 1 from grantor)
111
+ and (select rank from grantee) <= (select rank from grantor)
112
+ and (select rank from grantee) <= (select rank from ceiling)
113
+ $$;
114
+
115
+ comment on function agent_board._grant_within_ceiling(uuid, uuid, text) is
116
+ 'True iff p_grantee_role does not outrank the grantor''s active board role nor the board ceiling (default owner). MBAUTHZ owns this; MBSENDAUTHZ consumes it.';
117
+
118
+ revoke all on function agent_board._grant_within_ceiling(uuid, uuid, text) from public, anon;
119
+ grant execute on function agent_board._grant_within_ceiling(uuid, uuid, text) to authenticated, service_role;
120
+
121
+ -- 4. set_board_authority_ceiling (owner-settable cap) ------------------------
122
+ -- #13: the board owner (the human root's delegated authority) can set the per-board
123
+ -- ceiling. Permitted ONLY when the caller (current_agent_id()) holds an owner
124
+ -- board_memberships row for that board; every other caller raises 42501. The
125
+ -- grantee-bounded _grant_within_ceiling semantics above are unchanged — this only
126
+ -- lets the owner set the stored ceiling row that _grant_within_ceiling reads.
127
+ -- service_role (current_agent_id() = null) is intentionally NOT an owner here; it
128
+ -- retains its direct table-write grant for operator provisioning.
129
+ create or replace function agent_board.set_board_authority_ceiling(
130
+ p_board_id uuid,
131
+ p_max_grantable_role text
132
+ )
133
+ returns table(
134
+ ceiling_board_id uuid,
135
+ ceiling_max_grantable_role text
136
+ )
137
+ language plpgsql
138
+ security definer
139
+ set search_path = agent_board, public
140
+ as $$
141
+ declare
142
+ v_actor uuid := agent_board.current_agent_id();
143
+ begin
144
+ if p_max_grantable_role not in ('owner', 'admin', 'member', 'observer') then
145
+ raise exception 'max_grantable_role must be one of owner/admin/member/observer' using errcode = '22023';
146
+ end if;
147
+
148
+ if v_actor is null
149
+ or not exists (
150
+ select 1
151
+ from agent_board.board_memberships bm
152
+ where bm.board_id = p_board_id
153
+ and bm.agent_id = v_actor
154
+ and bm.role = 'owner'
155
+ and bm.membership_state in ('joined', 'subscribed')
156
+ ) then
157
+ raise exception 'only a board owner may set the board authority ceiling' using errcode = '42501';
158
+ end if;
159
+
160
+ insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
161
+ values (p_board_id, p_max_grantable_role)
162
+ on conflict (board_id) do update
163
+ set max_grantable_role = excluded.max_grantable_role,
164
+ updated_at = now();
165
+
166
+ return query
167
+ select p_board_id, p_max_grantable_role;
168
+ end;
169
+ $$;
170
+
171
+ comment on function agent_board.set_board_authority_ceiling(uuid, text) is
172
+ 'Owner-gated setter for a board''s authority ceiling: upserts board_authority_ceilings only when current_agent_id() is an owner of the board (else raises 42501). Leaves _grant_within_ceiling semantics unchanged.';
173
+
174
+ revoke all on function agent_board.set_board_authority_ceiling(uuid, text) from public, anon;
175
+ grant execute on function agent_board.set_board_authority_ceiling(uuid, text) to authenticated, service_role;