@consiliency/agent-board-schema 1.4.0 → 1.5.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/20260723000100_agent_board_roomown_owner_seed.sql +167 -0
- package/migrations/20260723000200_agent_board_roomown_grant_role_live.sql +72 -0
- package/package.json +1 -1
- package/schema-manifest.json +21 -3
- package/src/tooling.test.mjs +6 -2
- package/tests/agent_board_board_role.test.sql +3 -3
- package/tests/agent_board_delegation_policies.test.sql +2 -1
- package/tests/agent_board_enrollment_audit_content.test.sql +5 -0
- package/tests/agent_board_inbox_rpcs.test.sql +4 -4
- package/tests/agent_board_portal_offline_invariant.test.sql +7 -3
- package/tests/agent_board_roomown.test.sql +233 -0
- package/tests/agent_board_scoped_send_regrant.test.sql +7 -3
- package/tests/agent_board_send_authz_matrix.test.sql +11 -8
- package/tests/agent_board_send_role_gate.test.sql +8 -4
- package/tests/agent_board_v2_invariants.test.sql +5 -5
|
@@ -284,6 +284,14 @@
|
|
|
284
284
|
{
|
|
285
285
|
"id": "20260719001300_agent_board_enroll_rate_events_reaper_rls",
|
|
286
286
|
"sha256": "b2f58eadc13cb227991c4d978c5460878dc3cbada43c58285b55154b89a28459"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"id": "20260723000100_agent_board_roomown_owner_seed",
|
|
290
|
+
"sha256": "ecfaf262832b3d499102ec3a2ee1d48e043b33282287d0e987555e868c07b804"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"id": "20260723000200_agent_board_roomown_grant_role_live",
|
|
294
|
+
"sha256": "40c9f585af16211fe9ab1d35efca6787e49b31afc7c85f208da22b9566a65575"
|
|
287
295
|
}
|
|
288
296
|
]
|
|
289
297
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
-- ROOMOWN Lane A: room creators actually OWN the room.
|
|
2
|
+
--
|
|
3
|
+
-- Before this migration create_board wrote ONLY the boards row and never seeded a
|
|
4
|
+
-- board_memberships owner row, so the creator held no membership and could not act
|
|
5
|
+
-- as owner (grant_board_role / set_board_authority_ceiling both resolve the grantor
|
|
6
|
+
-- rank from a joined/subscribed membership). This migration:
|
|
7
|
+
--
|
|
8
|
+
-- 1. create or replace create_board so that, on a GENUINE INSERT, it also seeds a
|
|
9
|
+
-- joined `owner` board_memberships row bound to the board's ACTUAL
|
|
10
|
+
-- created_by_agent_id. The pre-existing slug-collision UPDATE path is now
|
|
11
|
+
-- OWNER-ONLY: a non-owner colliding on an existing slug raises 42501 and mutates
|
|
12
|
+
-- nothing (closing a confidentiality bypass where any authenticated agent could
|
|
13
|
+
-- overwrite another board's name/visibility/metadata). The effective actor is
|
|
14
|
+
-- resolved through _resolved_actor_agent_id, so an authenticated caller can only
|
|
15
|
+
-- create/own as itself while service_role retains create-on-behalf-of.
|
|
16
|
+
--
|
|
17
|
+
-- 2. bootstrap_board_owner_memberships(): a service_role-only repair sweep that
|
|
18
|
+
-- seeds the owner membership for pre-existing owner-less boards' created_by only.
|
|
19
|
+
--
|
|
20
|
+
-- Additive; no schema_version bump (net-new owner-seed + repair, no previously-live
|
|
21
|
+
-- authenticated contract re-activated here).
|
|
22
|
+
|
|
23
|
+
-- 1. create_board: seed owner on insert; owner-only slug-collision update ----------
|
|
24
|
+
create or replace function agent_board.create_board(
|
|
25
|
+
p_created_by_agent_id uuid,
|
|
26
|
+
p_slug text,
|
|
27
|
+
p_name text,
|
|
28
|
+
p_description text default null,
|
|
29
|
+
p_metadata jsonb default '{}'::jsonb,
|
|
30
|
+
p_visibility text default 'private'
|
|
31
|
+
)
|
|
32
|
+
returns table(board_id uuid, slug text, name text, status text, visibility text, created_at timestamptz, updated_at timestamptz)
|
|
33
|
+
language plpgsql
|
|
34
|
+
security definer
|
|
35
|
+
set search_path = agent_board, public
|
|
36
|
+
as $$
|
|
37
|
+
#variable_conflict use_column
|
|
38
|
+
declare
|
|
39
|
+
v_actor uuid;
|
|
40
|
+
v_existing agent_board.boards%rowtype;
|
|
41
|
+
v_board agent_board.boards%rowtype;
|
|
42
|
+
begin
|
|
43
|
+
if nullif(trim(p_slug), '') is null or nullif(trim(p_name), '') is null then
|
|
44
|
+
raise exception 'board slug and name are required' using errcode = '22023';
|
|
45
|
+
end if;
|
|
46
|
+
if coalesce(p_visibility, 'private') not in ('private', 'invite_only', 'open', 'directory_hidden') then
|
|
47
|
+
raise exception 'invalid board visibility' using errcode = '22023';
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
-- Effective actor: service_role -> supplied arg (create-on-behalf-of);
|
|
51
|
+
-- authenticated -> current_agent_id() and rejects arg <> current (42501).
|
|
52
|
+
v_actor := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
|
|
53
|
+
|
|
54
|
+
if not exists (select 1 from agent_board.agents where id = v_actor) then
|
|
55
|
+
raise exception 'agent not found' using errcode = 'P0002';
|
|
56
|
+
end if;
|
|
57
|
+
|
|
58
|
+
select * into v_existing
|
|
59
|
+
from agent_board.boards b
|
|
60
|
+
where b.slug = trim(p_slug);
|
|
61
|
+
|
|
62
|
+
if found then
|
|
63
|
+
-- Slug-collision path is OWNER-ONLY, keyed on CURRENT authority: the actor must
|
|
64
|
+
-- hold an ACTIVE owner-role membership on the existing board — NOT merely be its
|
|
65
|
+
-- original `created_by_agent_id`. A demoted or departed original creator no longer
|
|
66
|
+
-- owns the board and must NOT be able to re-open/rename/flip-visibility it.
|
|
67
|
+
if not exists (
|
|
68
|
+
select 1 from agent_board.board_memberships m
|
|
69
|
+
where m.board_id = v_existing.id
|
|
70
|
+
and m.agent_id = v_actor
|
|
71
|
+
and m.role = 'owner'
|
|
72
|
+
and m.membership_state in ('joined', 'subscribed')
|
|
73
|
+
) then
|
|
74
|
+
raise exception 'board slug already in use' using errcode = '42501';
|
|
75
|
+
end if;
|
|
76
|
+
|
|
77
|
+
update agent_board.boards as b
|
|
78
|
+
set name = trim(p_name),
|
|
79
|
+
description = nullif(trim(p_description), ''),
|
|
80
|
+
status = 'active',
|
|
81
|
+
visibility = coalesce(p_visibility, 'private'),
|
|
82
|
+
metadata = coalesce(b.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
|
|
83
|
+
updated_at = now()
|
|
84
|
+
where b.id = v_existing.id
|
|
85
|
+
returning * into v_board;
|
|
86
|
+
-- No owner re-seed: the owner was seeded at this board's original creation, and
|
|
87
|
+
-- re-promoting on every create_board would resurrect a deliberately demoted owner.
|
|
88
|
+
else
|
|
89
|
+
insert into agent_board.boards as b(slug, name, description, created_by_agent_id, visibility, metadata)
|
|
90
|
+
values (
|
|
91
|
+
trim(p_slug),
|
|
92
|
+
trim(p_name),
|
|
93
|
+
nullif(trim(p_description), ''),
|
|
94
|
+
v_actor,
|
|
95
|
+
coalesce(p_visibility, 'private'),
|
|
96
|
+
coalesce(p_metadata, '{}'::jsonb)
|
|
97
|
+
)
|
|
98
|
+
returning * into v_board;
|
|
99
|
+
|
|
100
|
+
-- Seed the creator's owner membership, bound to the board's ACTUAL
|
|
101
|
+
-- created_by_agent_id (never the raw param). Idempotent against the active
|
|
102
|
+
-- partial-unique index so at most one active owner row exists.
|
|
103
|
+
insert into agent_board.board_memberships as bm(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
104
|
+
values (v_board.id, v_board.created_by_agent_id, 'joined', 'owner', v_board.created_by_agent_id)
|
|
105
|
+
on conflict (board_id, agent_id) where (membership_state in ('joined', 'subscribed'))
|
|
106
|
+
do update set role = 'owner', updated_at = now();
|
|
107
|
+
end if;
|
|
108
|
+
|
|
109
|
+
return query
|
|
110
|
+
select v_board.id, v_board.slug, v_board.name, v_board.status, v_board.visibility, v_board.created_at, v_board.updated_at;
|
|
111
|
+
end;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
comment on function agent_board.create_board(uuid, text, text, text, jsonb, text) is
|
|
115
|
+
'Creates a board and, on genuine insert, seeds the creator''s joined owner membership (bound to the board''s created_by_agent_id). The slug-collision update path is owner-only (non-owner raises 42501, mutates nothing). Effective actor resolved via _resolved_actor_agent_id.';
|
|
116
|
+
|
|
117
|
+
revoke all on function agent_board.create_board(uuid, text, text, text, jsonb, text) from public, anon;
|
|
118
|
+
grant execute on function agent_board.create_board(uuid, text, text, text, jsonb, text) to authenticated, service_role;
|
|
119
|
+
|
|
120
|
+
-- 2. bootstrap_board_owner_memberships(): service_role-only owner-seed repair -------
|
|
121
|
+
create or replace function agent_board.bootstrap_board_owner_memberships()
|
|
122
|
+
returns table(board_id uuid, owner_agent_id uuid, membership_id uuid)
|
|
123
|
+
language plpgsql
|
|
124
|
+
security definer
|
|
125
|
+
set search_path = agent_board, public
|
|
126
|
+
as $$
|
|
127
|
+
#variable_conflict use_column
|
|
128
|
+
declare
|
|
129
|
+
v_row record;
|
|
130
|
+
v_membership_id uuid;
|
|
131
|
+
begin
|
|
132
|
+
-- service_role-only: current_agent_id() is null for service_role, non-null for any
|
|
133
|
+
-- authenticated principal (guarded here in addition to the EXECUTE revoke below).
|
|
134
|
+
if not agent_board._is_service_role() then
|
|
135
|
+
raise exception 'bootstrap_board_owner_memberships is service_role-only' using errcode = '42501';
|
|
136
|
+
end if;
|
|
137
|
+
|
|
138
|
+
for v_row in
|
|
139
|
+
select b.id as board_id, b.created_by_agent_id as owner_agent_id
|
|
140
|
+
from agent_board.boards b
|
|
141
|
+
where not exists (
|
|
142
|
+
select 1
|
|
143
|
+
from agent_board.board_memberships bm
|
|
144
|
+
where bm.board_id = b.id
|
|
145
|
+
and bm.agent_id = b.created_by_agent_id
|
|
146
|
+
and bm.membership_state in ('joined', 'subscribed')
|
|
147
|
+
)
|
|
148
|
+
loop
|
|
149
|
+
insert into agent_board.board_memberships as bm(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
150
|
+
values (v_row.board_id, v_row.owner_agent_id, 'joined', 'owner', v_row.owner_agent_id)
|
|
151
|
+
on conflict (board_id, agent_id) where (membership_state in ('joined', 'subscribed'))
|
|
152
|
+
do update set role = 'owner', updated_at = now()
|
|
153
|
+
returning bm.id into v_membership_id;
|
|
154
|
+
|
|
155
|
+
board_id := v_row.board_id;
|
|
156
|
+
owner_agent_id := v_row.owner_agent_id;
|
|
157
|
+
membership_id := v_membership_id;
|
|
158
|
+
return next;
|
|
159
|
+
end loop;
|
|
160
|
+
end;
|
|
161
|
+
$$;
|
|
162
|
+
|
|
163
|
+
comment on function agent_board.bootstrap_board_owner_memberships() is
|
|
164
|
+
'service_role-only repair sweep: seeds a joined owner membership for pre-existing owner-less boards, bound to each board''s created_by_agent_id. Takes no caller/role param.';
|
|
165
|
+
|
|
166
|
+
revoke all on function agent_board.bootstrap_board_owner_memberships() from public, anon, authenticated;
|
|
167
|
+
grant execute on function agent_board.bootstrap_board_owner_memberships() to service_role;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- ROOMOWN Lane B: activate the dormant grant_board_role chokepoint for authenticated
|
|
2
|
+
-- board owners/admins, with a privilege-escalation-safe grantor binding.
|
|
3
|
+
--
|
|
4
|
+
-- 20260718000400 shipped grant_board_role dormant (revoked from authenticated,
|
|
5
|
+
-- granted service_role) and TRUSTED p_grantor_agent_id. Now that create_board seeds
|
|
6
|
+
-- the creator's owner membership (ROOMOWN Lane A), the owner can drive grants from
|
|
7
|
+
-- their own authenticated session. Activating it safely requires binding the grantor
|
|
8
|
+
-- to the RESOLVED actor, NOT coalesce(current_agent_id(), p_grantor_agent_id):
|
|
9
|
+
-- current_agent_id() returns null for ANY authenticated caller whose agent is
|
|
10
|
+
-- suspended/credential-rotating/unmapped (20260710000000:15 filters status='active'),
|
|
11
|
+
-- and such a caller still carries a valid `authenticated` token. A coalesce would then
|
|
12
|
+
-- honor an attacker-supplied p_grantor_agent_id (owner IDs are non-secret) and let them
|
|
13
|
+
-- self-escalate. _resolved_actor_agent_id fails closed: service_role -> supplied arg;
|
|
14
|
+
-- authenticated -> current_agent_id() and raises 42501 if null or if arg <> current.
|
|
15
|
+
--
|
|
16
|
+
-- Additive; no schema_version bump. grant_board_role was never authenticated, so no
|
|
17
|
+
-- old client keys on it — this is a net-new capability, not a re-activation.
|
|
18
|
+
|
|
19
|
+
create or replace function agent_board.grant_board_role(
|
|
20
|
+
p_board_id uuid,
|
|
21
|
+
p_agent_id uuid,
|
|
22
|
+
p_grantee_role text,
|
|
23
|
+
p_grantor_agent_id uuid default null
|
|
24
|
+
)
|
|
25
|
+
returns table(board_id uuid, agent_id uuid, role text, updated_at timestamptz)
|
|
26
|
+
language plpgsql
|
|
27
|
+
security definer
|
|
28
|
+
set search_path = agent_board, public
|
|
29
|
+
as $$
|
|
30
|
+
declare
|
|
31
|
+
v_grantor uuid;
|
|
32
|
+
v_updated agent_board.board_memberships%rowtype;
|
|
33
|
+
begin
|
|
34
|
+
if p_grantee_role not in ('owner', 'admin', 'member', 'observer') then
|
|
35
|
+
raise exception 'invalid grantee role' using errcode = '22023';
|
|
36
|
+
end if;
|
|
37
|
+
|
|
38
|
+
-- Grantor is the RESOLVED actor. service_role -> supplied arg; authenticated ->
|
|
39
|
+
-- current_agent_id() and raises 42501 if null (suspended/unmapped) or if the
|
|
40
|
+
-- supplied arg <> current_agent_id(). This closes the single-principal escalation.
|
|
41
|
+
v_grantor := agent_board._resolved_actor_agent_id(p_grantor_agent_id, 'p_grantor_agent_id');
|
|
42
|
+
|
|
43
|
+
if not exists (
|
|
44
|
+
select 1 from agent_board.board_memberships bm
|
|
45
|
+
where bm.board_id = p_board_id and bm.agent_id = p_agent_id
|
|
46
|
+
) then
|
|
47
|
+
raise exception 'grantee has no membership on this board' using errcode = 'P0002';
|
|
48
|
+
end if;
|
|
49
|
+
|
|
50
|
+
-- Ceiling consumption: grantee-bounded against min(grantor role, board ceiling).
|
|
51
|
+
if not agent_board._grant_within_ceiling(v_grantor, p_board_id, p_grantee_role) then
|
|
52
|
+
raise exception 'grant exceeds grantor role or board authority ceiling' using errcode = '42501';
|
|
53
|
+
end if;
|
|
54
|
+
|
|
55
|
+
update agent_board.board_memberships bm
|
|
56
|
+
set role = p_grantee_role,
|
|
57
|
+
updated_at = now()
|
|
58
|
+
where bm.board_id = p_board_id
|
|
59
|
+
and bm.agent_id = p_agent_id
|
|
60
|
+
returning * into v_updated;
|
|
61
|
+
|
|
62
|
+
return query
|
|
63
|
+
select v_updated.board_id, v_updated.agent_id, v_updated.role, v_updated.updated_at;
|
|
64
|
+
end;
|
|
65
|
+
$$;
|
|
66
|
+
|
|
67
|
+
comment on function agent_board.grant_board_role(uuid, uuid, text, uuid) is
|
|
68
|
+
'Live board-role grant chokepoint. Grantor is the resolved actor (_resolved_actor_agent_id: service_role uses the supplied arg, authenticated uses current_agent_id() and fails closed on null/mismatch). Requires grantee membership (P0002), then enforces _grant_within_ceiling(grantor, board, grantee_role) before setting board_memberships.role.';
|
|
69
|
+
|
|
70
|
+
-- Live: authenticated owners/admins may grant (within ceiling); service_role retained.
|
|
71
|
+
revoke all on function agent_board.grant_board_role(uuid, uuid, text, uuid) from public, anon;
|
|
72
|
+
grant execute on function agent_board.grant_board_role(uuid, uuid, text, uuid) to authenticated, service_role;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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.5.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": 73,
|
|
50
|
+
"aggregateSha256": "a43737edf382d8aee59069d6a9aec9e39fce17b31f092a3d7fcd96303b6f70e9",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -687,6 +687,24 @@
|
|
|
687
687
|
"sourceRef": "message-board",
|
|
688
688
|
"bytes": 3112,
|
|
689
689
|
"sha256": "b2f58eadc13cb227991c4d978c5460878dc3cbada43c58285b55154b89a28459"
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
"order": 71,
|
|
693
|
+
"id": "20260723000100_agent_board_roomown_owner_seed",
|
|
694
|
+
"filename": "20260723000100_agent_board_roomown_owner_seed.sql",
|
|
695
|
+
"origin": "message-board-forward",
|
|
696
|
+
"sourceRef": "message-board",
|
|
697
|
+
"bytes": 7871,
|
|
698
|
+
"sha256": "ecfaf262832b3d499102ec3a2ee1d48e043b33282287d0e987555e868c07b804"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
"order": 72,
|
|
702
|
+
"id": "20260723000200_agent_board_roomown_grant_role_live",
|
|
703
|
+
"filename": "20260723000200_agent_board_roomown_grant_role_live.sql",
|
|
704
|
+
"origin": "message-board-forward",
|
|
705
|
+
"sourceRef": "message-board",
|
|
706
|
+
"bytes": 3716,
|
|
707
|
+
"sha256": "40c9f585af16211fe9ab1d35efca6787e49b31afc7c85f208da22b9566a65575"
|
|
690
708
|
}
|
|
691
709
|
],
|
|
692
710
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -37,9 +37,9 @@ describe('manifest integrity', () => {
|
|
|
37
37
|
expect(manifest.schemaVersion).toBe(8);
|
|
38
38
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
39
39
|
});
|
|
40
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 forward migrations (migrationCount = prior
|
|
40
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN forward migrations (migrationCount = prior 71 + 2 = 73)', () => {
|
|
41
41
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
42
|
-
expect(manifest.migrationCount).toBe(
|
|
42
|
+
expect(manifest.migrationCount).toBe(73);
|
|
43
43
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
44
44
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
45
45
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -74,6 +74,10 @@ describe('manifest integrity', () => {
|
|
|
74
74
|
'20260719000300_agent_board_enroll_rate_cap.sql',
|
|
75
75
|
'20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql',
|
|
76
76
|
'20260719001300_agent_board_enroll_rate_events_reaper_rls.sql',
|
|
77
|
+
// ROOMOWN: room ownership + self-service authority (create_board owner-seed +
|
|
78
|
+
// bootstrap; grant_board_role activated for authenticated). schema stays 8.
|
|
79
|
+
'20260723000100_agent_board_roomown_owner_seed.sql',
|
|
80
|
+
'20260723000200_agent_board_roomown_grant_role_live.sql',
|
|
77
81
|
]) {
|
|
78
82
|
expect(names.has(f)).toBe(true);
|
|
79
83
|
}
|
|
@@ -47,11 +47,11 @@ begin
|
|
|
47
47
|
select board_id into v_board
|
|
48
48
|
from agent_board.create_board(v_creator, 'br-board-' || v_suffix, 'BR Board', 'board role test', '{}'::jsonb, 'private');
|
|
49
49
|
|
|
50
|
-
--
|
|
51
|
-
--
|
|
50
|
+
-- The creator's owner membership is now seeded by create_board (ROOMOWN). The
|
|
51
|
+
-- `other` membership is inserted with the column DEFAULT (member) to exercise the
|
|
52
|
+
-- one-time backfill; member/admin get explicit roles for gate tests.
|
|
52
53
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id)
|
|
53
54
|
values
|
|
54
|
-
(v_board, v_creator, 'joined', v_creator),
|
|
55
55
|
(v_board, v_other, 'joined', v_creator);
|
|
56
56
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
57
57
|
values
|
|
@@ -37,9 +37,10 @@ begin
|
|
|
37
37
|
select board_id into v_board
|
|
38
38
|
from agent_board.create_board(v_owner, 'dg-board-' || v_suffix, 'DG Board', 'delegation test', '{}'::jsonb, 'private');
|
|
39
39
|
|
|
40
|
+
-- The owner's owner membership is now seeded by create_board (ROOMOWN); only the
|
|
41
|
+
-- member membership needs an explicit insert.
|
|
40
42
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
41
43
|
values
|
|
42
|
-
(v_board, v_owner, 'joined', v_owner, 'owner'),
|
|
43
44
|
(v_board, v_member, 'joined', v_owner, 'member');
|
|
44
45
|
|
|
45
46
|
-- Policy with only root_agent_id set -> defaults apply (child_ttl_ceiling 8h).
|
|
@@ -72,6 +72,10 @@ select is(
|
|
|
72
72
|
select pg_get_functiondef(p.oid) as def, p.proname
|
|
73
73
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
74
74
|
where n.nspname = 'agent_board'
|
|
75
|
+
-- pg_get_functiondef() RAISES on aggregate ('a') / window ('w') procs
|
|
76
|
+
-- ("X is an aggregate function"); restrict to callable functions +
|
|
77
|
+
-- procedures (the RPC surface this invariant is about).
|
|
78
|
+
and p.prokind in ('f', 'p')
|
|
75
79
|
and p.proname <> 'enroll_federated_identity'
|
|
76
80
|
) d
|
|
77
81
|
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
@@ -81,6 +85,7 @@ select is(
|
|
|
81
85
|
select pg_get_functiondef(p.oid) as def
|
|
82
86
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
83
87
|
where n.nspname = 'agent_board'
|
|
88
|
+
and p.prokind in ('f', 'p') -- pg_get_functiondef raises on aggregate/window procs
|
|
84
89
|
and p.proname in (
|
|
85
90
|
'mint_fleet_agent_session', 'post_board_message', 'send_direct_message',
|
|
86
91
|
'send_multicast_message', 'create_tasking_delivery', 'revoke_agent')
|
|
@@ -371,7 +371,7 @@ begin
|
|
|
371
371
|
'{}'::jsonb
|
|
372
372
|
);
|
|
373
373
|
v_board_message := v_result.message_id;
|
|
374
|
-
perform is(v_result.delivered_recipient_count,
|
|
374
|
+
perform is(v_result.delivered_recipient_count, 3, 'board delivery reaches joined and subscribed members only (incl. the seeded owner-creator)');
|
|
375
375
|
perform is(
|
|
376
376
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_board_message and recipient_agent_id = v_left_member),
|
|
377
377
|
0,
|
|
@@ -402,15 +402,15 @@ begin
|
|
|
402
402
|
'{}'::jsonb
|
|
403
403
|
);
|
|
404
404
|
v_task := v_result.task_id;
|
|
405
|
-
perform is(v_result.delivered_recipient_count,
|
|
405
|
+
perform is(v_result.delivered_recipient_count, 4, 'tasking counts explicit and board fan-out attempts (incl. the seeded owner-creator)');
|
|
406
406
|
perform is(
|
|
407
407
|
(select count(*)::integer from agent_board.delivery_recipients where source_task_id = v_task),
|
|
408
|
-
|
|
408
|
+
3,
|
|
409
409
|
'tasking suppresses duplicate recipient rows across explicit and board fan-out'
|
|
410
410
|
);
|
|
411
411
|
perform is(
|
|
412
412
|
(select count(*)::integer from agent_board.inbox_items where source_task_id = v_task),
|
|
413
|
-
|
|
413
|
+
3,
|
|
414
414
|
'tasking writes one inbox row per unique recipient'
|
|
415
415
|
);
|
|
416
416
|
|
|
@@ -28,6 +28,7 @@ do $$
|
|
|
28
28
|
declare
|
|
29
29
|
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
30
30
|
v_agent uuid; v_auth uuid;
|
|
31
|
+
v_creator uuid := gen_random_uuid();
|
|
31
32
|
v_key uuid := gen_random_uuid();
|
|
32
33
|
v_ch uuid := gen_random_uuid();
|
|
33
34
|
v_board uuid; v_thread uuid := gen_random_uuid();
|
|
@@ -57,11 +58,14 @@ begin
|
|
|
57
58
|
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at, consumed_at)
|
|
58
59
|
values (v_ch, v_key, v_agent, v_ch::text, 'agent-signing-key-proof.v1:'||v_ch||':n', 'pending', now() + interval '10 minutes', null);
|
|
59
60
|
|
|
60
|
-
-- A board
|
|
61
|
+
-- A board (created by a dedicated creator; owner seeded by create_board) +
|
|
62
|
+
-- membership + thread so the enrolled agent sends as a genuine NON-creator member.
|
|
63
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
64
|
+
values (v_creator, 'agent', 'pofx-creator-'||s, 'POFX Creator', 'active');
|
|
61
65
|
select board_id into v_board
|
|
62
|
-
from agent_board.create_board(
|
|
66
|
+
from agent_board.create_board(v_creator, 'pofx-board-'||s, 'POFX Board', 'portal-offline', '{}'::jsonb, 'private');
|
|
63
67
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
64
|
-
values (v_board, v_agent, 'joined',
|
|
68
|
+
values (v_board, v_agent, 'joined', v_creator, 'member');
|
|
65
69
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
66
70
|
values (v_thread, 'coordination', v_agent, 'POFX thread');
|
|
67
71
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
-- ROOMOWN pgTAP: room ownership (create_board owner-seed + owner-only slug-collision
|
|
2
|
+
-- + bootstrap repair) and self-service authority (grant_board_role activated for
|
|
3
|
+
-- authenticated owners/admins with a privilege-escalation-safe grantor binding).
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
6
|
+
-- (''/service_role => true, 'authenticated' => false) and request.jwt.claim.sub picks
|
|
7
|
+
-- the acting agent. Fixture style mirrors agent_board_board_role.test.sql.
|
|
8
|
+
create extension if not exists pgtap with schema extensions;
|
|
9
|
+
set search_path = extensions, public, agent_board;
|
|
10
|
+
|
|
11
|
+
begin;
|
|
12
|
+
|
|
13
|
+
select plan(21);
|
|
14
|
+
|
|
15
|
+
create temp table _roomown_fixture (
|
|
16
|
+
board uuid,
|
|
17
|
+
board_slug text,
|
|
18
|
+
board2 uuid,
|
|
19
|
+
board2_owner uuid,
|
|
20
|
+
alice uuid,
|
|
21
|
+
bob uuid,
|
|
22
|
+
member_agent uuid,
|
|
23
|
+
grantee uuid,
|
|
24
|
+
nomember uuid,
|
|
25
|
+
suspended uuid,
|
|
26
|
+
alice_user uuid,
|
|
27
|
+
bob_user uuid,
|
|
28
|
+
member_user uuid,
|
|
29
|
+
suspended_user uuid
|
|
30
|
+
) on commit drop;
|
|
31
|
+
|
|
32
|
+
do $$
|
|
33
|
+
declare
|
|
34
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
35
|
+
v_alice_user uuid;
|
|
36
|
+
v_bob_user uuid;
|
|
37
|
+
v_member_user uuid;
|
|
38
|
+
v_suspended_user uuid;
|
|
39
|
+
v_alice uuid := gen_random_uuid();
|
|
40
|
+
v_bob uuid := gen_random_uuid();
|
|
41
|
+
v_member uuid := gen_random_uuid();
|
|
42
|
+
v_grantee uuid := gen_random_uuid();
|
|
43
|
+
v_nomember uuid := gen_random_uuid();
|
|
44
|
+
v_suspended uuid := gen_random_uuid();
|
|
45
|
+
v_b2owner uuid := gen_random_uuid();
|
|
46
|
+
v_board uuid;
|
|
47
|
+
v_slug text := 'roomown-board-' || v_suffix;
|
|
48
|
+
v_board2 uuid := gen_random_uuid();
|
|
49
|
+
begin
|
|
50
|
+
v_alice_user := public.ensure_github_user('ro-alice-' || v_suffix, 'ro-alice-' || v_suffix || '@example.test', 'ro-alice-' || v_suffix, 'RO Alice');
|
|
51
|
+
v_bob_user := public.ensure_github_user('ro-bob-' || v_suffix, 'ro-bob-' || v_suffix || '@example.test', 'ro-bob-' || v_suffix, 'RO Bob');
|
|
52
|
+
v_member_user := public.ensure_github_user('ro-member-' || v_suffix, 'ro-member-' || v_suffix || '@example.test', 'ro-member-' || v_suffix, 'RO Member');
|
|
53
|
+
v_suspended_user := public.ensure_github_user('ro-susp-' || v_suffix, 'ro-susp-' || v_suffix || '@example.test', 'ro-susp-' || v_suffix, 'RO Suspended');
|
|
54
|
+
|
|
55
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
56
|
+
values
|
|
57
|
+
(v_alice, 'agent', 'ro-alice-' || v_suffix, 'RO Alice', 'active', v_alice_user, v_alice_user),
|
|
58
|
+
(v_bob, 'agent', 'ro-bob-' || v_suffix, 'RO Bob', 'active', v_bob_user, v_bob_user),
|
|
59
|
+
(v_member, 'agent', 'ro-member-' || v_suffix, 'RO Member', 'active', v_member_user, v_member_user),
|
|
60
|
+
-- Suspended agent WITH a valid auth user: current_agent_id() filters status='active'
|
|
61
|
+
-- so it resolves to null while auth.uid() still maps (the A1 escalation vector).
|
|
62
|
+
(v_suspended, 'agent', 'ro-susp-' || v_suffix, 'RO Suspended', 'suspended', v_suspended_user, v_suspended_user),
|
|
63
|
+
(v_grantee, 'agent', 'ro-grantee-' || v_suffix, 'RO Grantee', 'active', null, null),
|
|
64
|
+
(v_nomember, 'agent', 'ro-nomember-' || v_suffix, 'RO NoMember', 'active', null, null),
|
|
65
|
+
(v_b2owner, 'agent', 'ro-b2owner-' || v_suffix, 'RO B2 Owner', 'active', null, null);
|
|
66
|
+
|
|
67
|
+
-- Alice creates board B (service_role null-actor here -> supplied arg is honored):
|
|
68
|
+
-- create_board seeds Alice's joined owner membership.
|
|
69
|
+
select board_id into v_board
|
|
70
|
+
from agent_board.create_board(v_alice, v_slug, 'ROOMOWN Board', 'roomown test', '{}'::jsonb, 'private');
|
|
71
|
+
|
|
72
|
+
-- Genuine NON-creator members: a member (rank member) + a grant target (observer).
|
|
73
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
74
|
+
values
|
|
75
|
+
(v_board, v_member, 'joined', v_alice, 'member'),
|
|
76
|
+
(v_board, v_grantee, 'joined', v_alice, 'observer');
|
|
77
|
+
|
|
78
|
+
-- Board B2: an owner-LESS board (direct insert, no membership) for the bootstrap sweep.
|
|
79
|
+
insert into agent_board.boards(id, slug, name, description, created_by_agent_id, visibility)
|
|
80
|
+
values (v_board2, 'roomown-board2-' || v_suffix, 'ROOMOWN Board2', 'owner-less', v_b2owner, 'private');
|
|
81
|
+
|
|
82
|
+
insert into _roomown_fixture values
|
|
83
|
+
(v_board, v_slug, v_board2, v_b2owner, v_alice, v_bob, v_member, v_grantee, v_nomember, v_suspended,
|
|
84
|
+
v_alice_user, v_bob_user, v_member_user, v_suspended_user);
|
|
85
|
+
end
|
|
86
|
+
$$;
|
|
87
|
+
|
|
88
|
+
-- 1. create_board seeds the creator's joined owner membership --------------------
|
|
89
|
+
select is(
|
|
90
|
+
(select count(*)::int from agent_board.board_memberships bm
|
|
91
|
+
join _roomown_fixture f on bm.board_id = f.board and bm.agent_id = f.alice
|
|
92
|
+
where bm.membership_state = 'joined' and bm.role = 'owner'),
|
|
93
|
+
1, 'create_board seeds exactly one joined owner membership for the creator');
|
|
94
|
+
|
|
95
|
+
-- A5.1 create_board actor-guard: authenticated Bob cannot create as Alice ---------
|
|
96
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
97
|
+
select set_config('request.jwt.claim.sub', (select bob_user from _roomown_fixture)::text, true);
|
|
98
|
+
select throws_ok(
|
|
99
|
+
(select format($q$select agent_board.create_board(%L::uuid, %L, 'Bob As Alice', null, '{}'::jsonb, 'private')$q$,
|
|
100
|
+
alice, 'roomown-bobasalice-' || replace(board::text, '-', '')) from _roomown_fixture),
|
|
101
|
+
'42501', 'p_created_by_agent_id must match current_agent_id()',
|
|
102
|
+
'actor-guard: authenticated Bob cannot create_board as Alice');
|
|
103
|
+
|
|
104
|
+
-- A5.2 slug-collision guard: non-owner Bob colliding on Alice's slug -> 42501 -----
|
|
105
|
+
select throws_ok(
|
|
106
|
+
(select format($q$select agent_board.create_board(%L::uuid, %L, 'HIJACK', null, '{}'::jsonb, 'open')$q$,
|
|
107
|
+
bob, board_slug) from _roomown_fixture),
|
|
108
|
+
'42501', 'board slug already in use',
|
|
109
|
+
'slug-collision: non-owner colliding on an existing slug is rejected');
|
|
110
|
+
select is(
|
|
111
|
+
(select name from agent_board.boards b join _roomown_fixture f on b.id = f.board),
|
|
112
|
+
'ROOMOWN Board', 'slug-collision: the existing board name is unchanged (no hijack mutation)');
|
|
113
|
+
select is(
|
|
114
|
+
(select count(*)::int from agent_board.board_memberships bm
|
|
115
|
+
join _roomown_fixture f on bm.board_id = f.board and bm.agent_id = f.bob),
|
|
116
|
+
0, 'slug-collision: the non-owner gains no membership on the existing board');
|
|
117
|
+
|
|
118
|
+
-- 2. owner-seed idempotent: re-create by the owner does not add an owner row -------
|
|
119
|
+
select set_config('request.jwt.claim.sub', (select alice_user from _roomown_fixture)::text, true);
|
|
120
|
+
select lives_ok(
|
|
121
|
+
(select format($q$select agent_board.create_board(%L::uuid, %L, 'ROOMOWN Board', 'roomown test', '{}'::jsonb, 'private')$q$,
|
|
122
|
+
alice, board_slug) from _roomown_fixture),
|
|
123
|
+
'owner re-create on the same slug is admitted (owner-only update path)');
|
|
124
|
+
select is(
|
|
125
|
+
(select count(*)::int from agent_board.board_memberships bm
|
|
126
|
+
join _roomown_fixture f on bm.board_id = f.board and bm.agent_id = f.alice
|
|
127
|
+
where bm.membership_state in ('joined', 'subscribed')),
|
|
128
|
+
1, 'owner-seed is idempotent: re-create does not duplicate the owner membership');
|
|
129
|
+
|
|
130
|
+
-- 3. bootstrap repairs an owner-less board's created_by membership ----------------
|
|
131
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
132
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
133
|
+
select lives_ok(
|
|
134
|
+
$$select agent_board.bootstrap_board_owner_memberships()$$,
|
|
135
|
+
'bootstrap_board_owner_memberships runs for service_role');
|
|
136
|
+
select is(
|
|
137
|
+
(select count(*)::int from agent_board.board_memberships bm
|
|
138
|
+
join _roomown_fixture f on bm.board_id = f.board2 and bm.agent_id = f.board2_owner
|
|
139
|
+
where bm.membership_state = 'joined' and bm.role = 'owner'),
|
|
140
|
+
1, 'bootstrap seeds a joined owner membership for the owner-less board''s created_by');
|
|
141
|
+
|
|
142
|
+
-- 4. bootstrap is service_role-only ----------------------------------------------
|
|
143
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
144
|
+
select set_config('request.jwt.claim.sub', (select alice_user from _roomown_fixture)::text, true);
|
|
145
|
+
select throws_ok(
|
|
146
|
+
$$select agent_board.bootstrap_board_owner_memberships()$$,
|
|
147
|
+
'42501', 'bootstrap_board_owner_memberships is service_role-only',
|
|
148
|
+
'bootstrap rejects an authenticated (mapped) caller');
|
|
149
|
+
|
|
150
|
+
-- 5. owner grants admin (within ceiling, no ceiling row = owner) ------------------
|
|
151
|
+
select set_config('request.jwt.claim.sub', (select alice_user from _roomown_fixture)::text, true);
|
|
152
|
+
select lives_ok(
|
|
153
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin')$q$, board, grantee) from _roomown_fixture),
|
|
154
|
+
'owner grants admin (grantor bound to current_agent_id, no explicit arg)');
|
|
155
|
+
select is(
|
|
156
|
+
(select role from agent_board.board_memberships bm join _roomown_fixture f on bm.board_id = f.board and bm.agent_id = f.grantee),
|
|
157
|
+
'admin', 'the grantee role is upserted to admin');
|
|
158
|
+
|
|
159
|
+
-- 6. a member cannot grant above its own rank ------------------------------------
|
|
160
|
+
select set_config('request.jwt.claim.sub', (select member_user from _roomown_fixture)::text, true);
|
|
161
|
+
select throws_ok(
|
|
162
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin')$q$, board, grantee) from _roomown_fixture),
|
|
163
|
+
'42501', 'grant exceeds grantor role or board authority ceiling',
|
|
164
|
+
'a member grantor cannot grant admin (grantee-bounded ceiling)');
|
|
165
|
+
|
|
166
|
+
-- 7. grantor binding: a member passing p_grantor=<owner> is still rejected (A1) ---
|
|
167
|
+
select throws_ok(
|
|
168
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin', %L::uuid)$q$, board, grantee, alice) from _roomown_fixture),
|
|
169
|
+
'42501', 'p_grantor_agent_id must match current_agent_id()',
|
|
170
|
+
'grantor binding: an authenticated member cannot impersonate the owner via p_grantor_agent_id');
|
|
171
|
+
|
|
172
|
+
-- 8. board ceiling caps an owner grant -------------------------------------------
|
|
173
|
+
insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
|
|
174
|
+
select board, 'member' from _roomown_fixture;
|
|
175
|
+
select set_config('request.jwt.claim.sub', (select alice_user from _roomown_fixture)::text, true);
|
|
176
|
+
select throws_ok(
|
|
177
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin')$q$, board, grantee) from _roomown_fixture),
|
|
178
|
+
'42501', 'grant exceeds grantor role or board authority ceiling',
|
|
179
|
+
'the board ceiling caps an over-ceiling grant even for an owner grantor');
|
|
180
|
+
|
|
181
|
+
-- A5.3 grantee-membership required -> P0002 --------------------------------------
|
|
182
|
+
select throws_ok(
|
|
183
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'member')$q$, board, nomember) from _roomown_fixture),
|
|
184
|
+
'P0002', 'grantee has no membership on this board',
|
|
185
|
+
'grant to an agent with no membership raises P0002');
|
|
186
|
+
|
|
187
|
+
-- A5.4 service_role regression: explicit p_grantor honored on the null actor ------
|
|
188
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
189
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
190
|
+
select lives_ok(
|
|
191
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'member', %L::uuid)$q$, board, grantee, alice) from _roomown_fixture),
|
|
192
|
+
'service_role: an explicit p_grantor_agent_id is honored (null actor via _is_service_role)');
|
|
193
|
+
|
|
194
|
+
-- A5.5 null-actor escalation BLOCKED (proves A1) ---------------------------------
|
|
195
|
+
-- An authenticated sub mapped to a SUSPENDED agent (current_agent_id() null) passing
|
|
196
|
+
-- p_grantor=<owner> must be rejected at actor resolution, NOT self-escalated.
|
|
197
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
198
|
+
select set_config('request.jwt.claim.sub', (select suspended_user from _roomown_fixture)::text, true);
|
|
199
|
+
select throws_ok(
|
|
200
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'owner', %L::uuid)$q$, board, grantee, alice) from _roomown_fixture),
|
|
201
|
+
'42501', 'current agent principal not resolved',
|
|
202
|
+
'null-actor escalation is blocked: a suspended principal cannot grant via a supplied owner id');
|
|
203
|
+
|
|
204
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
205
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
206
|
+
|
|
207
|
+
-- 9. grant_board_role EXECUTE is granted to authenticated (live) -----------------
|
|
208
|
+
select function_privs_are(
|
|
209
|
+
'agent_board', 'grant_board_role', array['uuid', 'uuid', 'text', 'uuid'],
|
|
210
|
+
'authenticated', array['EXECUTE'], 'grant_board_role execute is granted to authenticated (self-service authority live)');
|
|
211
|
+
|
|
212
|
+
-- A5.6 demoted creator cannot mutate via slug-collision (current-owner authz, NOT created_by) --
|
|
213
|
+
-- Panel finding: the slug-collision UPDATE path must key on CURRENT owner membership, not the
|
|
214
|
+
-- immutable created_by. Alice (still owner after the grant tests) self-demotes to member, then
|
|
215
|
+
-- her re-create on the same slug must be rejected and mutate nothing.
|
|
216
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
217
|
+
select set_config('request.jwt.claim.sub', (select alice_user from _roomown_fixture)::text, true);
|
|
218
|
+
-- self-demote to member (member <= min(owner, ceiling)); bare statement, not an assertion
|
|
219
|
+
select agent_board.grant_board_role(board, alice, 'member') from _roomown_fixture;
|
|
220
|
+
select throws_ok(
|
|
221
|
+
(select format($q$select agent_board.create_board(%L::uuid, %L, 'REOPEN', null, '{}'::jsonb, 'open')$q$,
|
|
222
|
+
alice, board_slug) from _roomown_fixture),
|
|
223
|
+
'42501', 'board slug already in use',
|
|
224
|
+
'demoted creator (no current owner membership) cannot mutate the board via slug-collision');
|
|
225
|
+
select is(
|
|
226
|
+
(select visibility from agent_board.boards b join _roomown_fixture f on b.id = f.board),
|
|
227
|
+
'private', 'demoted-creator slug-collision leaves visibility unchanged (no private->open flip)');
|
|
228
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
229
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
230
|
+
|
|
231
|
+
select finish();
|
|
232
|
+
|
|
233
|
+
rollback;
|
|
@@ -28,6 +28,7 @@ declare
|
|
|
28
28
|
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
29
29
|
v_member_user uuid;
|
|
30
30
|
v_other_user uuid;
|
|
31
|
+
v_creator uuid := gen_random_uuid();
|
|
31
32
|
v_member uuid := gen_random_uuid();
|
|
32
33
|
v_other uuid := gen_random_uuid();
|
|
33
34
|
v_recipient uuid := gen_random_uuid();
|
|
@@ -42,13 +43,16 @@ begin
|
|
|
42
43
|
values
|
|
43
44
|
(v_member, 'agent', 'rg-member-' || v_suffix, 'RG Member', 'active', v_member_user, v_member_user),
|
|
44
45
|
(v_other, 'agent', 'rg-other-' || v_suffix, 'RG Other', 'active', v_other_user, v_other_user),
|
|
45
|
-
(v_recipient, 'agent', 'rg-recipient-' || v_suffix, 'RG Recipient', 'active', null, null)
|
|
46
|
+
(v_recipient, 'agent', 'rg-recipient-' || v_suffix, 'RG Recipient', 'active', null, null),
|
|
47
|
+
(v_creator, 'agent', 'rg-creator-' || v_suffix, 'RG Creator', 'active', null, null);
|
|
46
48
|
|
|
49
|
+
-- Dedicated creator (owner seeded by create_board); v_member is a genuine
|
|
50
|
+
-- NON-creator member so the member send path is exercised as role 'member'.
|
|
47
51
|
select board_id into v_board
|
|
48
|
-
from agent_board.create_board(
|
|
52
|
+
from agent_board.create_board(v_creator, 'rg-board-' || v_suffix, 'RG Board', 'regrant test', '{}'::jsonb, 'private');
|
|
49
53
|
|
|
50
54
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
51
|
-
values (v_board, v_member, 'joined',
|
|
55
|
+
values (v_board, v_member, 'joined', v_creator, 'member');
|
|
52
56
|
|
|
53
57
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
54
58
|
values
|
|
@@ -26,6 +26,7 @@ declare
|
|
|
26
26
|
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
27
27
|
v_sb_member_user uuid;
|
|
28
28
|
v_sb_observer_user uuid;
|
|
29
|
+
v_sb_creator uuid := gen_random_uuid();
|
|
29
30
|
v_sb_member uuid := gen_random_uuid();
|
|
30
31
|
v_sb_observer uuid := gen_random_uuid();
|
|
31
32
|
v_sb_board uuid;
|
|
@@ -45,6 +46,7 @@ begin
|
|
|
45
46
|
(v_sb_observer, 'agent', 'mtx-observer-' || v_suffix, 'MTX Observer', 'active', v_sb_observer_user, v_sb_observer_user);
|
|
46
47
|
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
47
48
|
values
|
|
49
|
+
(v_sb_creator, 'agent', 'mtx-sbcreator-' || v_suffix, 'MTX Send Board Creator'),
|
|
48
50
|
(v_nf_sender, 'agent', 'mtx-nf-' || v_suffix, 'MTX NonFleet'),
|
|
49
51
|
(v_nf_allow, 'agent', 'mtx-nfa-' || v_suffix, 'MTX NonFleet Allow'),
|
|
50
52
|
(v_g_owner, 'agent', 'mtx-gowner-' || v_suffix, 'MTX Grant Owner'),
|
|
@@ -53,12 +55,13 @@ begin
|
|
|
53
55
|
(v_g2_owner, 'agent', 'mtx-g2owner-' || v_suffix, 'MTX Grant2 Owner'),
|
|
54
56
|
(v_g2_grantee, 'agent', 'mtx-g2grantee-' || v_suffix, 'MTX Grant2 Grantee');
|
|
55
57
|
|
|
56
|
-
-- Send board
|
|
57
|
-
|
|
58
|
+
-- Send board created by a dedicated creator (owner is seeded by create_board);
|
|
59
|
+
-- member/observer are genuine NON-creator members so their roles drive the gate.
|
|
60
|
+
select board_id into v_sb_board from agent_board.create_board(v_sb_creator, 'mtx-sb-' || v_suffix, 'MTX Send Board', 'matrix', '{}'::jsonb, 'private');
|
|
58
61
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
59
62
|
values
|
|
60
|
-
(v_sb_board, v_sb_member, 'joined',
|
|
61
|
-
(v_sb_board, v_sb_observer, 'joined',
|
|
63
|
+
(v_sb_board, v_sb_member, 'joined', v_sb_creator, 'member'),
|
|
64
|
+
(v_sb_board, v_sb_observer, 'joined', v_sb_creator, 'observer');
|
|
62
65
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
63
66
|
values
|
|
64
67
|
(v_thread_member, 'coordination', v_sb_member, 'MTX member thread'),
|
|
@@ -66,17 +69,17 @@ begin
|
|
|
66
69
|
|
|
67
70
|
-- Grant board B1: no ceiling row (ceiling defaults to owner).
|
|
68
71
|
select board_id into v_b1 from agent_board.create_board(v_g_owner, 'mtx-b1-' || v_suffix, 'MTX Grant B1', 'ceiling', '{}'::jsonb, 'private');
|
|
72
|
+
-- v_g_owner's owner membership is seeded by create_board (ROOMOWN).
|
|
69
73
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
70
74
|
values
|
|
71
|
-
(v_b1, v_g_owner, 'joined', v_g_owner, 'owner'),
|
|
72
75
|
(v_b1, v_g_admin, 'joined', v_g_owner, 'admin'),
|
|
73
76
|
(v_b1, v_g_grantee, 'joined', v_g_owner, 'observer');
|
|
74
77
|
|
|
75
78
|
-- Grant board B2: ceiling lowered to member.
|
|
76
79
|
select board_id into v_b2 from agent_board.create_board(v_g2_owner, 'mtx-b2-' || v_suffix, 'MTX Grant B2', 'ceiling', '{}'::jsonb, 'private');
|
|
80
|
+
-- v_g2_owner's owner membership is seeded by create_board (ROOMOWN).
|
|
77
81
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
78
82
|
values
|
|
79
|
-
(v_b2, v_g2_owner, 'joined', v_g2_owner, 'owner'),
|
|
80
83
|
(v_b2, v_g2_grantee, 'joined', v_g2_owner, 'observer');
|
|
81
84
|
insert into agent_board.board_authority_ceilings(board_id, max_grantable_role) values (v_b2, 'member');
|
|
82
85
|
|
|
@@ -149,10 +152,10 @@ select throws_ok(
|
|
|
149
152
|
'42501', 'grant exceeds grantor role or board authority ceiling',
|
|
150
153
|
'matrix: an over-ceiling grant is rejected even for an owner grantor');
|
|
151
154
|
|
|
152
|
-
-- E.
|
|
155
|
+
-- E. Live: grant_board_role is now granted EXECUTE to authenticated (ROOMOWN) ---
|
|
153
156
|
select function_privs_are(
|
|
154
157
|
'agent_board', 'grant_board_role', array['uuid', 'uuid', 'text', 'uuid'],
|
|
155
|
-
'authenticated', array[]
|
|
158
|
+
'authenticated', array['EXECUTE'], 'matrix: grant_board_role execute is granted to authenticated (self-service authority live)');
|
|
156
159
|
|
|
157
160
|
select finish();
|
|
158
161
|
|
|
@@ -32,6 +32,7 @@ declare
|
|
|
32
32
|
v_member_user uuid;
|
|
33
33
|
v_observer_user uuid;
|
|
34
34
|
v_nonmember_user uuid;
|
|
35
|
+
v_creator uuid := gen_random_uuid();
|
|
35
36
|
v_member uuid := gen_random_uuid();
|
|
36
37
|
v_observer uuid := gen_random_uuid();
|
|
37
38
|
v_nonmember uuid := gen_random_uuid();
|
|
@@ -50,16 +51,19 @@ begin
|
|
|
50
51
|
(v_member, 'agent', 'srg-member-' || v_suffix, 'SRG Member', 'active', v_member_user, v_member_user),
|
|
51
52
|
(v_observer, 'agent', 'srg-observer-' || v_suffix, 'SRG Observer', 'active', v_observer_user, v_observer_user),
|
|
52
53
|
(v_nonmember, 'agent', 'srg-nonmember-' || v_suffix, 'SRG Nonmember', 'active', v_nonmember_user, v_nonmember_user),
|
|
53
|
-
(v_recipient, 'agent', 'srg-recipient-' || v_suffix, 'SRG Recipient', 'active', null, null)
|
|
54
|
+
(v_recipient, 'agent', 'srg-recipient-' || v_suffix, 'SRG Recipient', 'active', null, null),
|
|
55
|
+
(v_creator, 'agent', 'srg-creator-' || v_suffix, 'SRG Creator', 'active', null, null);
|
|
54
56
|
|
|
57
|
+
-- Dedicated creator (owner seeded by create_board); member/observer are genuine
|
|
58
|
+
-- NON-creator members so their roles drive the send gate.
|
|
55
59
|
select board_id into v_board
|
|
56
|
-
from agent_board.create_board(
|
|
60
|
+
from agent_board.create_board(v_creator, 'srg-board-' || v_suffix, 'SRG Board', 'send role gate test', '{}'::jsonb, 'private');
|
|
57
61
|
|
|
58
62
|
-- member: role member; observer: role observer. Both active.
|
|
59
63
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
60
64
|
values
|
|
61
|
-
(v_board, v_member, 'joined',
|
|
62
|
-
(v_board, v_observer, 'joined',
|
|
65
|
+
(v_board, v_member, 'joined', v_creator, 'member'),
|
|
66
|
+
(v_board, v_observer, 'joined', v_creator, 'observer');
|
|
63
67
|
|
|
64
68
|
-- Threads each sending actor can access (inherit mode; creator has access).
|
|
65
69
|
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
@@ -323,8 +323,8 @@ select *
|
|
|
323
323
|
|
|
324
324
|
select is(
|
|
325
325
|
(select delivered_recipient_count from v2_board_delivery),
|
|
326
|
-
|
|
327
|
-
'post_board_message only reaches active joined or subscribed board members'
|
|
326
|
+
3,
|
|
327
|
+
'post_board_message only reaches active joined or subscribed board members (incl. the seeded owner-creator)'
|
|
328
328
|
);
|
|
329
329
|
|
|
330
330
|
select is(
|
|
@@ -369,13 +369,13 @@ select *
|
|
|
369
369
|
|
|
370
370
|
select is(
|
|
371
371
|
(select delivered_recipient_count from v2_tasking_delivery),
|
|
372
|
-
|
|
373
|
-
'tasking counts explicit and board fan-out attempts before duplicate collapse'
|
|
372
|
+
4,
|
|
373
|
+
'tasking counts explicit and board fan-out attempts before duplicate collapse (incl. the seeded owner-creator)'
|
|
374
374
|
);
|
|
375
375
|
|
|
376
376
|
select is(
|
|
377
377
|
(select count(*)::integer from agent_board.delivery_recipients where source_task_id = (select task_id from v2_tasking_delivery)),
|
|
378
|
-
|
|
378
|
+
3,
|
|
379
379
|
'tasking suppresses duplicate durable recipient rows across explicit and board fan-out'
|
|
380
380
|
);
|
|
381
381
|
|