@consiliency/agent-board-schema 1.56.0 → 1.57.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 +4 -0
- package/migrations/20261007000100_agent_board_onboard_requests.sql +139 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +5 -2
- package/tests/agent_board_membership_current_row_class.test.sql +3 -1
- package/tests/agent_board_onboard_requests.test.sql +353 -0
- package/tests/agent_board_rowid.test.sql +18 -5
- package/tests/catalog/function_execute_privileges.json +18 -0
- package/tests/catalog/p_metadata_parameters.json +8 -0
|
@@ -616,6 +616,10 @@
|
|
|
616
616
|
{
|
|
617
617
|
"id": "20261006000100_agent_board_room_roster_active_first",
|
|
618
618
|
"sha256": "637480feeca07dbb2fe7a4624dc6d063d0c1f23e6ea3c3ee9a946ad43de4e34e"
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"id": "20261007000100_agent_board_onboard_requests",
|
|
622
|
+
"sha256": "fbdbecfb1c78e2d1b16b37845a74082dcdf1241c61c0356b992e1e01baddc41c"
|
|
619
623
|
}
|
|
620
624
|
]
|
|
621
625
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
-- ONBOARD (v11) SL-2: the caller's own join requests (IF-0-ONBOARD-4) and a service-side
|
|
2
|
+
-- invite label lookup for the consent page (IF-0-ONBOARD-11). Additive: two new read-only
|
|
3
|
+
-- functions and one index. schema_version stays 8.
|
|
4
|
+
--
|
|
5
|
+
-- Sources read, each at its latest definition:
|
|
6
|
+
-- * agent_board.current_agent_id() -- 20260920000100 (the resolver
|
|
7
|
+
-- request_join_board, 20260810000100, calls);
|
|
8
|
+
-- * agent_board.board_membership_events -- 20260806000100 (the append-only
|
|
9
|
+
-- consent ledger; seq is the total order, created_at is wall-clock age);
|
|
10
|
+
-- * agent_board.boards.join_request_ttl_days -- 20260806000100;
|
|
11
|
+
-- * the emitter _emit_membership_event -- 20260807000100: a request made by the
|
|
12
|
+
-- subject through request_join_board is `target-authored` / `requested`;
|
|
13
|
+
-- * agent_board.board_invite_tokens -- 20260724000100. invite_board_for_display
|
|
14
|
+
-- checks only the frozen IF-0-ONBOARD-11 subset: not revoked, expires_at > now(), uses
|
|
15
|
+
-- remaining. The redeem core (latest 20260909000100) refuses MORE -- a suspended or
|
|
16
|
+
-- fleet-revoked creator, a lapsed ceiling, a removed agent, fleet-bounded invites -- so a
|
|
17
|
+
-- code that displays a board can still fail to redeem.
|
|
18
|
+
|
|
19
|
+
-- =============================================================================
|
|
20
|
+
-- 1. list_my_board_requests() -- IF-0-ONBOARD-4
|
|
21
|
+
-- =============================================================================
|
|
22
|
+
-- WHICH REQUESTS. The ledger, never the membership row. A request is a (board, caller) with
|
|
23
|
+
-- at least one event where agent_id is the caller, provenance = 'target-authored' and
|
|
24
|
+
-- result_state = 'requested'. board_memberships.created_by_agent_id is NOT a source: the
|
|
25
|
+
-- sink overwrites it on every later write (an approval names the approver), so it cannot say
|
|
26
|
+
-- who asked.
|
|
27
|
+
--
|
|
28
|
+
-- IDENTITY. From the session only, by the resolver request_join_board uses. There is no
|
|
29
|
+
-- parameter, and a null identity (service_role, anon, a revoked credential) returns nothing.
|
|
30
|
+
--
|
|
31
|
+
-- CURRENT STATE. The caller's membership row on that board, chosen active-first (MBROWID 3a,
|
|
32
|
+
-- the key every current-standing reader uses), or 'none' when no row exists. A rejected request
|
|
33
|
+
-- reads 'left' (REJECTSEM, #89: a refusal lands on the recoverable state).
|
|
34
|
+
--
|
|
35
|
+
-- EXPIRY. expires_at = the latest request event's created_at + the board's
|
|
36
|
+
-- join_request_ttl_days. expired mirrors the consent predicates exactly
|
|
37
|
+
-- (created_at < now() - ttl): an expired request no longer counts as consent. It is a property
|
|
38
|
+
-- of the REQUEST and is reported whatever the membership's current state.
|
|
39
|
+
create or replace function agent_board.list_my_board_requests()
|
|
40
|
+
returns table(
|
|
41
|
+
board_id uuid,
|
|
42
|
+
board_name text,
|
|
43
|
+
board_slug text,
|
|
44
|
+
requested_at timestamptz,
|
|
45
|
+
membership_state text,
|
|
46
|
+
expires_at timestamptz,
|
|
47
|
+
expired boolean
|
|
48
|
+
)
|
|
49
|
+
language plpgsql
|
|
50
|
+
stable
|
|
51
|
+
security definer
|
|
52
|
+
set search_path = agent_board, public
|
|
53
|
+
as $$
|
|
54
|
+
declare
|
|
55
|
+
v_actor uuid := agent_board.current_agent_id();
|
|
56
|
+
begin
|
|
57
|
+
if v_actor is null then
|
|
58
|
+
return;
|
|
59
|
+
end if;
|
|
60
|
+
|
|
61
|
+
return query
|
|
62
|
+
select
|
|
63
|
+
b.id,
|
|
64
|
+
b.name,
|
|
65
|
+
b.slug,
|
|
66
|
+
req.created_at,
|
|
67
|
+
coalesce(cur.membership_state, 'none'),
|
|
68
|
+
req.created_at + make_interval(days => b.join_request_ttl_days),
|
|
69
|
+
req.created_at < now() - make_interval(days => b.join_request_ttl_days)
|
|
70
|
+
from (
|
|
71
|
+
-- The latest request event per board, by seq (the ledger's total order).
|
|
72
|
+
select distinct on (e.board_id) e.board_id, e.created_at
|
|
73
|
+
from agent_board.board_membership_events e
|
|
74
|
+
where e.agent_id = v_actor
|
|
75
|
+
and e.provenance = 'target-authored'
|
|
76
|
+
and e.result_state = 'requested'
|
|
77
|
+
order by e.board_id, e.seq desc
|
|
78
|
+
) req
|
|
79
|
+
join agent_board.boards b on b.id = req.board_id
|
|
80
|
+
left join lateral (
|
|
81
|
+
select bm.membership_state
|
|
82
|
+
from agent_board.board_memberships bm
|
|
83
|
+
where bm.board_id = req.board_id
|
|
84
|
+
and bm.agent_id = v_actor
|
|
85
|
+
order by case when bm.membership_state in ('joined', 'subscribed') then 0 else 1 end,
|
|
86
|
+
bm.created_at desc, bm.id desc
|
|
87
|
+
limit 1
|
|
88
|
+
) cur on true
|
|
89
|
+
order by req.created_at desc, b.id desc;
|
|
90
|
+
end;
|
|
91
|
+
$$;
|
|
92
|
+
|
|
93
|
+
comment on function agent_board.list_my_board_requests() is
|
|
94
|
+
'IF-0-ONBOARD-4: the caller''s own join requests as they stand now. Source is the consent ledger (board_membership_events: agent_id = caller, provenance target-authored, result_state requested), never board_memberships.created_by_agent_id, which later writers overwrite. Identity is current_agent_id() only; there is no parameter and a null identity returns nothing, so service_role (no caller identity) is refused EXECUTE. Per board: id, name, slug, the latest request event''s time, the current membership state chosen active-first (MBROWID 3a) or none, expires_at = requested_at + join_request_ttl_days, and expired (the request no longer counts as consent). Requests made before the ledger existed carry only a board-authored backfill_neutral event and are not returned. No per-row history.';
|
|
95
|
+
|
|
96
|
+
revoke all on function agent_board.list_my_board_requests() from public, anon, service_role;
|
|
97
|
+
grant execute on function agent_board.list_my_board_requests() to authenticated;
|
|
98
|
+
|
|
99
|
+
-- =============================================================================
|
|
100
|
+
-- 2. invite_board_for_display(p_code_sha256) -- IF-0-ONBOARD-11
|
|
101
|
+
-- =============================================================================
|
|
102
|
+
-- The Portal's consent page shows which board an in-chat enrollment will join, and it holds
|
|
103
|
+
-- only the digest (the code itself never leaves the agent). The digest is the lowercase hex
|
|
104
|
+
-- sha256 of the code text; the argument is compared case-insensitively.
|
|
105
|
+
--
|
|
106
|
+
-- Only a code passing the IF-0-ONBOARD-11 subset resolves: not revoked, not expired, and uses
|
|
107
|
+
-- remaining. This is narrower than the redeem core (see the header), so display is not a promise
|
|
108
|
+
-- that redeem will succeed. Anything else -- including an unknown digest -- returns no row, one uniform
|
|
109
|
+
-- answer. Codes are 256-bit, so the digest is not a practical oracle; EXECUTE is service_role
|
|
110
|
+
-- only regardless, because the Portal calls it server-side.
|
|
111
|
+
create index if not exists idx_agent_board_board_invite_tokens_code_sha256
|
|
112
|
+
on agent_board.board_invite_tokens (encode(extensions.digest(code, 'sha256'), 'hex'));
|
|
113
|
+
|
|
114
|
+
create or replace function agent_board.invite_board_for_display(p_code_sha256 text)
|
|
115
|
+
returns table(
|
|
116
|
+
board_id uuid,
|
|
117
|
+
board_name text,
|
|
118
|
+
board_slug text,
|
|
119
|
+
preset_role text
|
|
120
|
+
)
|
|
121
|
+
language sql
|
|
122
|
+
stable
|
|
123
|
+
security definer
|
|
124
|
+
set search_path = agent_board, public
|
|
125
|
+
as $$
|
|
126
|
+
select b.id, b.name, b.slug, t.preset_role
|
|
127
|
+
from agent_board.board_invite_tokens t
|
|
128
|
+
join agent_board.boards b on b.id = t.board_id
|
|
129
|
+
where encode(extensions.digest(t.code, 'sha256'), 'hex') = lower(p_code_sha256)
|
|
130
|
+
and t.revoked_at is null
|
|
131
|
+
and t.expires_at > now()
|
|
132
|
+
and (t.max_uses is null or t.uses_count < t.max_uses);
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
comment on function agent_board.invite_board_for_display(text) is
|
|
136
|
+
'IF-0-ONBOARD-11: resolves an invite code''s digest (lowercase hex sha256 of the code, compared case-insensitively) to (board_id, board_name, board_slug, preset_role) for display on the Portal consent page. Only a usable code resolves -- not revoked, expires_at > now(), and max_uses null or uses_count < max_uses (a subset of the redeem core''s checks, so a displayed board can still fail to redeem); revoked, expired, exhausted and unknown codes return no row. Read-only. EXECUTE is service_role only: the Portal calls it server-side.';
|
|
137
|
+
|
|
138
|
+
revoke all on function agent_board.invite_board_for_display(text) from public, anon, authenticated;
|
|
139
|
+
grant execute on function agent_board.invite_board_for_display(text) to 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.57.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.57.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": 155,
|
|
50
|
+
"aggregateSha256": "347357b36b5ee37fbde189e005993c40df1e63a2d9d247b53823761304ae127e",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1434,6 +1434,15 @@
|
|
|
1434
1434
|
"sourceRef": "message-board",
|
|
1435
1435
|
"bytes": 5470,
|
|
1436
1436
|
"sha256": "637480feeca07dbb2fe7a4624dc6d063d0c1f23e6ea3c3ee9a946ad43de4e34e"
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
"order": 154,
|
|
1440
|
+
"id": "20261007000100_agent_board_onboard_requests",
|
|
1441
|
+
"filename": "20261007000100_agent_board_onboard_requests.sql",
|
|
1442
|
+
"origin": "message-board-forward",
|
|
1443
|
+
"sourceRef": "message-board",
|
|
1444
|
+
"bytes": 7655,
|
|
1445
|
+
"sha256": "fbdbecfb1c78e2d1b16b37845a74082dcdf1241c61c0356b992e1e01baddc41c"
|
|
1437
1446
|
}
|
|
1438
1447
|
],
|
|
1439
1448
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 155)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(155);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -343,6 +343,9 @@ describe('manifest integrity', () => {
|
|
|
343
343
|
// ROOMCONTRACT-ACTIVEFIRST (#542): list_board_members chooses current standing
|
|
344
344
|
// active-first (MBROWID 3a) at both its authorization and projection sites.
|
|
345
345
|
'20261006000100_agent_board_room_roster_active_first.sql',
|
|
346
|
+
// ONBOARD (v11): list_my_board_requests (IF-0-ONBOARD-4) and the service-only
|
|
347
|
+
// invite_board_for_display digest lookup (IF-0-ONBOARD-11).
|
|
348
|
+
'20261007000100_agent_board_onboard_requests.sql',
|
|
346
349
|
]) {
|
|
347
350
|
expect(names.has(f)).toBe(true);
|
|
348
351
|
}
|
|
@@ -309,6 +309,7 @@ select is(
|
|
|
309
309
|
'list_board_members/distinct/active-first',
|
|
310
310
|
'list_board_members/limit/active-first',
|
|
311
311
|
'list_board_pending_memberships/distinct/active-first',
|
|
312
|
+
'list_my_board_requests/limit/active-first',
|
|
312
313
|
'request_join_board/limit/filtered:''joined'',''subscribed''',
|
|
313
314
|
'resolve_board_request/limit/filtered:''joined'',''subscribed''',
|
|
314
315
|
'resolve_board_request/limit/filtered:''requested'',''invited''',
|
|
@@ -367,10 +368,11 @@ select is(
|
|
|
367
368
|
'list_board_members/distinct',
|
|
368
369
|
'list_board_members/limit',
|
|
369
370
|
'list_board_pending_memberships/distinct',
|
|
371
|
+
'list_my_board_requests/limit',
|
|
370
372
|
'update_board_membership/limit',
|
|
371
373
|
'update_board_membership/limit'
|
|
372
374
|
]::text[],
|
|
373
|
-
'CRC-4: these
|
|
375
|
+
'CRC-4: these eight sites carry the MBROWID 3a active-first key');
|
|
374
376
|
|
|
375
377
|
-- ---------------------------------------------------------------------------
|
|
376
378
|
-- CRC-5..14: behaviour rows over fixture text, through the same objects.
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
-- ONBOARD (v11) SL-2 pgTAP: list_my_board_requests() (IF-0-ONBOARD-4) and
|
|
2
|
+
-- invite_board_for_display(text) (IF-0-ONBOARD-11), migration 20261007000100.
|
|
3
|
+
--
|
|
4
|
+
-- Driven by the real operations only: request_join_board, resolve_board_request (approve and
|
|
5
|
+
-- reject), leave_board then request_join_board again, create_invite_link / redeem_invite_link /
|
|
6
|
+
-- revoke_invite_link, enroll_federated_human. Two fixtures are AGE only, because now() is
|
|
7
|
+
-- constant inside the test transaction and neither the TTL nor an invite expiry can be waited
|
|
8
|
+
-- out: one request event's created_at is moved 31 days back (OB-L13), and one invite's
|
|
9
|
+
-- expires_at is moved into the past (OB-I05). Both rows were first written by the real RPC.
|
|
10
|
+
--
|
|
11
|
+
-- Identity is set by the whole-JSON claim GUC only (the consent suite's method): SET LOCAL
|
|
12
|
+
-- ROLE is never used, so EXECUTE posture is asserted separately through
|
|
13
|
+
-- has_function_privilege, not by calling as a role.
|
|
14
|
+
--
|
|
15
|
+
-- Every call to a function under test is a lives_ok capture into a temp table, then separate
|
|
16
|
+
-- state checks (AGENTS.md rule 3): a missing or re-signatured function FAILS the capture and
|
|
17
|
+
-- leaves the checks to fail on an empty table, instead of aborting the file.
|
|
18
|
+
--
|
|
19
|
+
-- Mutation receipts (plans/evidence/onboard/mutation-receipts.md, "## SL-2"):
|
|
20
|
+
-- identity parameter added -> OB-L02
|
|
21
|
+
-- agent_id = caller dropped -> OB-L10, OB-L16
|
|
22
|
+
-- created_by_agent_id as the source -> OB-L10
|
|
23
|
+
-- service_role granted -> OB-L05
|
|
24
|
+
-- IF-11 granted to authenticated -> OB-I13
|
|
25
|
+
-- revoked / expired / exhausted predicate dropped -> OB-I04 / OB-I05 / OB-I07
|
|
26
|
+
--
|
|
27
|
+
-- HAND-COUNTED plan: OB-L01..L23 (23) + OB-I01..I16 with I03b (17) + OB-R01..R03 and the two
|
|
28
|
+
-- named redeem pins (5) = 45.
|
|
29
|
+
create extension if not exists pgtap with schema extensions;
|
|
30
|
+
set search_path = extensions, public, agent_board;
|
|
31
|
+
|
|
32
|
+
begin;
|
|
33
|
+
|
|
34
|
+
select plan(45);
|
|
35
|
+
|
|
36
|
+
create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
|
|
37
|
+
begin
|
|
38
|
+
perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
|
|
39
|
+
end $$;
|
|
40
|
+
create function pg_temp.service() returns void language plpgsql as $$
|
|
41
|
+
begin perform set_config('request.jwt.claims', '', true); end $$;
|
|
42
|
+
create function pg_temp.state(b uuid, a uuid) returns text language sql as $$
|
|
43
|
+
select membership_state from agent_board.board_memberships
|
|
44
|
+
where board_id = b and agent_id = a
|
|
45
|
+
order by case when membership_state in ('joined','subscribed') then 0 else 1 end,
|
|
46
|
+
created_at desc, id desc
|
|
47
|
+
limit 1;
|
|
48
|
+
$$;
|
|
49
|
+
create function pg_temp.sha(p_code text) returns text language sql as $$
|
|
50
|
+
select encode(extensions.digest(p_code, 'sha256'), 'hex');
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
-- ---------------------------------------------------------------------------
|
|
54
|
+
-- Fixture: principals and boards (service context; create_board seeds the owner).
|
|
55
|
+
-- ---------------------------------------------------------------------------
|
|
56
|
+
insert into auth.users(id, instance_id, aud, role, email)
|
|
57
|
+
select ('0b000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
|
|
58
|
+
'00000000-0000-0000-0000-000000000000','authenticated','authenticated','onb'||n||'@t.test'
|
|
59
|
+
from generate_series(1,4) n;
|
|
60
|
+
|
|
61
|
+
-- 1 = owner, 2 = requester R, 3 = another principal X, 4 = invite redeemer / exhauster Y.
|
|
62
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
63
|
+
select ('0b100000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
|
|
64
|
+
'agent','agent','onb-agent-'||n,'ONB Agent '||n,'active',
|
|
65
|
+
('0b000000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
|
|
66
|
+
from generate_series(1,4) n;
|
|
67
|
+
|
|
68
|
+
create temp table _ob (
|
|
69
|
+
owner uuid, r uuid, x uuid, y uuid, h uuid, h_user uuid,
|
|
70
|
+
b_approve uuid, b_reject uuid, b_rerequest uuid, b_expire uuid,
|
|
71
|
+
b_invite uuid, b_other uuid, b_human uuid, b_links uuid,
|
|
72
|
+
code_join text, code_live text, code_revoked text, code_expired text,
|
|
73
|
+
code_exhaust text, code_replay text, code_other text
|
|
74
|
+
) on commit drop;
|
|
75
|
+
|
|
76
|
+
do $$
|
|
77
|
+
declare
|
|
78
|
+
v_owner uuid := '0b100000-0000-4000-8000-000000000001';
|
|
79
|
+
v_iss text := 'https://onb-fed-' || replace(substr(gen_random_uuid()::text, 1, 8), '-', '') || '.test';
|
|
80
|
+
v_h uuid; v_h_user uuid;
|
|
81
|
+
v_ba uuid; v_bb uuid; v_bc uuid; v_bd uuid; v_be uuid; v_bg uuid; v_bf uuid; v_bk uuid;
|
|
82
|
+
begin
|
|
83
|
+
select board_id into v_ba from agent_board.create_board(v_owner, 'onb-approve', 'ONB Approve', null, '{}'::jsonb, 'open');
|
|
84
|
+
select board_id into v_bb from agent_board.create_board(v_owner, 'onb-reject', 'ONB Reject', null, '{}'::jsonb, 'open');
|
|
85
|
+
select board_id into v_bc from agent_board.create_board(v_owner, 'onb-rerequest', 'ONB Rerequest', null, '{}'::jsonb, 'open');
|
|
86
|
+
select board_id into v_bd from agent_board.create_board(v_owner, 'onb-expire', 'ONB Expire', null, '{}'::jsonb, 'invite_only');
|
|
87
|
+
select board_id into v_be from agent_board.create_board(v_owner, 'onb-invite', 'ONB Invite', null, '{}'::jsonb, 'open');
|
|
88
|
+
select board_id into v_bg from agent_board.create_board(v_owner, 'onb-other', 'ONB Other', null, '{}'::jsonb, 'open');
|
|
89
|
+
select board_id into v_bf from agent_board.create_board(v_owner, 'onb-human', 'ONB Human', null, '{}'::jsonb, 'open');
|
|
90
|
+
select board_id into v_bk from agent_board.create_board(v_owner, 'onb-links', 'ONB Links', null, '{}'::jsonb, 'private');
|
|
91
|
+
|
|
92
|
+
-- A Portal human: the federated provider, then the fed-human agent it provisions.
|
|
93
|
+
insert into agent_board.identity_providers
|
|
94
|
+
(issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
|
|
95
|
+
values (v_iss, 'message-board', true, true, '{human}', null);
|
|
96
|
+
select e.agent_id, e.auth_user_id into v_h, v_h_user
|
|
97
|
+
from agent_board.enroll_federated_human(
|
|
98
|
+
v_iss, 'message-board', 'onb-human-sub',
|
|
99
|
+
jsonb_build_object('sub', 'onb-human-sub', 'email_verified', false), 'ONB Human') e;
|
|
100
|
+
|
|
101
|
+
insert into _ob(owner, r, x, y, h, h_user,
|
|
102
|
+
b_approve, b_reject, b_rerequest, b_expire, b_invite, b_other, b_human, b_links)
|
|
103
|
+
values (v_owner,
|
|
104
|
+
'0b100000-0000-4000-8000-000000000002', '0b100000-0000-4000-8000-000000000003',
|
|
105
|
+
'0b100000-0000-4000-8000-000000000004', v_h, v_h_user,
|
|
106
|
+
v_ba, v_bb, v_bc, v_bd, v_be, v_bg, v_bf, v_bk);
|
|
107
|
+
|
|
108
|
+
update _ob set
|
|
109
|
+
code_join = (select code from agent_board.create_invite_link(v_be, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
|
|
110
|
+
code_live = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
|
|
111
|
+
code_revoked = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
|
|
112
|
+
code_expired = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
|
|
113
|
+
code_exhaust = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', 1, null, v_owner, '{}'::jsonb)),
|
|
114
|
+
code_replay = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb)),
|
|
115
|
+
code_other = (select code from agent_board.create_invite_link(v_bk, 'member', 'joined', null, null, v_owner, '{}'::jsonb));
|
|
116
|
+
end
|
|
117
|
+
$$;
|
|
118
|
+
|
|
119
|
+
create temp table _req (
|
|
120
|
+
who text, board_id uuid, board_name text, board_slug text, requested_at timestamptz,
|
|
121
|
+
membership_state text, expires_at timestamptz, expired boolean
|
|
122
|
+
) on commit drop;
|
|
123
|
+
create temp table _disp (
|
|
124
|
+
label text, board_id uuid, board_name text, board_slug text, preset_role text
|
|
125
|
+
) on commit drop;
|
|
126
|
+
|
|
127
|
+
-- ===========================================================================
|
|
128
|
+
-- list_my_board_requests: catalog posture
|
|
129
|
+
-- ===========================================================================
|
|
130
|
+
select ok(to_regprocedure('agent_board.list_my_board_requests()') is not null,
|
|
131
|
+
'OB-L01: list_my_board_requests() exists with no arguments');
|
|
132
|
+
select is(
|
|
133
|
+
(select string_agg(coalesce(pg_get_function_arguments(p.oid), ''), '|')
|
|
134
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
135
|
+
where n.nspname = 'agent_board' and p.proname = 'list_my_board_requests'),
|
|
136
|
+
'',
|
|
137
|
+
'OB-L02: exactly one list_my_board_requests overload, and it takes NO parameter (identity is never an argument)');
|
|
138
|
+
select is(
|
|
139
|
+
(select p.prosecdef::text || '/' || p.provolatile::text
|
|
140
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
141
|
+
where n.nspname = 'agent_board' and p.proname = 'list_my_board_requests'),
|
|
142
|
+
'true/s',
|
|
143
|
+
'OB-L03: list_my_board_requests is SECURITY DEFINER and STABLE');
|
|
144
|
+
select is(has_function_privilege('authenticated', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
|
|
145
|
+
true, 'OB-L04: authenticated holds EXECUTE on list_my_board_requests');
|
|
146
|
+
select is(has_function_privilege('service_role', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
|
|
147
|
+
false, 'OB-L05: service_role holds NO EXECUTE on list_my_board_requests (no caller identity)');
|
|
148
|
+
select is(has_function_privilege('anon', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
|
|
149
|
+
false, 'OB-L06: anon holds NO EXECUTE on list_my_board_requests');
|
|
150
|
+
select is(has_function_privilege('public', to_regprocedure('agent_board.list_my_board_requests()'), 'EXECUTE'),
|
|
151
|
+
false, 'OB-L07: PUBLIC holds NO EXECUTE on list_my_board_requests');
|
|
152
|
+
|
|
153
|
+
-- ===========================================================================
|
|
154
|
+
-- Real operations
|
|
155
|
+
-- ===========================================================================
|
|
156
|
+
-- R requests four boards.
|
|
157
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
|
|
158
|
+
do $$ declare o _ob; begin select * into o from _ob;
|
|
159
|
+
perform agent_board.request_join_board(o.b_approve, o.r);
|
|
160
|
+
perform agent_board.request_join_board(o.b_reject, o.r);
|
|
161
|
+
perform agent_board.request_join_board(o.b_rerequest, o.r);
|
|
162
|
+
perform agent_board.request_join_board(o.b_expire, o.r);
|
|
163
|
+
end $$;
|
|
164
|
+
|
|
165
|
+
-- X requests the approve board and a board R never asked for.
|
|
166
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000003');
|
|
167
|
+
do $$ declare o _ob; begin select * into o from _ob;
|
|
168
|
+
perform agent_board.request_join_board(o.b_approve, o.x);
|
|
169
|
+
perform agent_board.request_join_board(o.b_other, o.x);
|
|
170
|
+
end $$;
|
|
171
|
+
|
|
172
|
+
-- The owner approves A and C, rejects B. Passing the owner as p_created_by_agent_id makes the
|
|
173
|
+
-- sink overwrite created_by_agent_id on R's rows -- the reason that column is not a source.
|
|
174
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000001');
|
|
175
|
+
do $$ declare o _ob; begin select * into o from _ob;
|
|
176
|
+
perform agent_board.resolve_board_request(o.b_approve, o.r, 'joined', o.owner, '{}'::jsonb);
|
|
177
|
+
perform agent_board.resolve_board_request(o.b_reject, o.r, 'removed', o.owner, '{}'::jsonb);
|
|
178
|
+
perform agent_board.resolve_board_request(o.b_rerequest, o.r, 'joined', o.owner, '{}'::jsonb);
|
|
179
|
+
end $$;
|
|
180
|
+
|
|
181
|
+
-- R leaves C and requests it again; R joins E through an invite with no request.
|
|
182
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
|
|
183
|
+
do $$ declare o _ob; begin select * into o from _ob;
|
|
184
|
+
perform agent_board.leave_board(o.b_rerequest, o.r);
|
|
185
|
+
perform agent_board.request_join_board(o.b_rerequest, o.r);
|
|
186
|
+
perform agent_board.redeem_invite_link(o.code_join);
|
|
187
|
+
end $$;
|
|
188
|
+
|
|
189
|
+
-- The Portal human requests F from its own session.
|
|
190
|
+
select pg_temp.claims((select h_user::text from _ob));
|
|
191
|
+
do $$ declare o _ob; begin select * into o from _ob;
|
|
192
|
+
perform agent_board.request_join_board(o.b_human, o.h);
|
|
193
|
+
end $$;
|
|
194
|
+
|
|
195
|
+
-- AGE fixture: R's request on D is moved past the board's 30-day TTL.
|
|
196
|
+
select pg_temp.service();
|
|
197
|
+
update agent_board.board_membership_events e
|
|
198
|
+
set created_at = now() - interval '31 days'
|
|
199
|
+
from _ob
|
|
200
|
+
where e.board_id = _ob.b_expire and e.agent_id = _ob.r
|
|
201
|
+
and e.provenance = 'target-authored' and e.result_state = 'requested';
|
|
202
|
+
|
|
203
|
+
-- ===========================================================================
|
|
204
|
+
-- list_my_board_requests: behaviour
|
|
205
|
+
-- ===========================================================================
|
|
206
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
|
|
207
|
+
select lives_ok($$ insert into _req select 'R', * from agent_board.list_my_board_requests() $$,
|
|
208
|
+
'OB-L08: R lists its requests [call succeeds]');
|
|
209
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000003');
|
|
210
|
+
select lives_ok($$ insert into _req select 'X', * from agent_board.list_my_board_requests() $$,
|
|
211
|
+
'OB-L09: X lists its requests [call succeeds]');
|
|
212
|
+
|
|
213
|
+
select is(
|
|
214
|
+
(select string_agg(board_slug, ',' order by board_slug) from _req where who = 'R'),
|
|
215
|
+
'onb-approve,onb-expire,onb-reject,onb-rerequest',
|
|
216
|
+
'OB-L10: R sees exactly its four requested boards -- approved, rejected, re-requested and expired');
|
|
217
|
+
select is((select membership_state from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_approve),
|
|
218
|
+
'joined', 'OB-L11: an approved request reads its current state, joined');
|
|
219
|
+
select is((select membership_state from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_reject),
|
|
220
|
+
'left', 'OB-L12: a rejected request reads its current state, left (REJECTSEM)');
|
|
221
|
+
select is((select membership_state || '/' || expired::text from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_expire),
|
|
222
|
+
'requested/true', 'OB-L13: a request older than join_request_ttl_days is returned with expired = true');
|
|
223
|
+
select is((select count(*)::int from _req where who = 'R' and not expired),
|
|
224
|
+
3, 'OB-L14: the three requests inside the TTL are expired = false');
|
|
225
|
+
select is(
|
|
226
|
+
(select membership_state || '/' || (select count(*) from _req q where q.who = 'R' and q.board_id = r.board_id)::text
|
|
227
|
+
from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_rerequest),
|
|
228
|
+
'requested/1',
|
|
229
|
+
'OB-L15: leave then re-request reads requested, in ONE row for the board');
|
|
230
|
+
select is((select count(*)::int from _req r, _ob where r.who = 'R' and r.board_id in (_ob.b_other, _ob.b_human)),
|
|
231
|
+
0, 'OB-L16: another principal''s requests are invisible to R');
|
|
232
|
+
select is(
|
|
233
|
+
(select pg_temp.state(_ob.b_invite, _ob.r) || '/' ||
|
|
234
|
+
(select count(*) from _req r where r.who = 'R' and r.board_id = _ob.b_invite)::text from _ob),
|
|
235
|
+
'joined/0',
|
|
236
|
+
'OB-L17: an invite-only join (R is joined) is NOT returned as a request');
|
|
237
|
+
select is(
|
|
238
|
+
(select (r.board_name = 'ONB Approve' and r.expires_at = r.requested_at + interval '30 days')::text
|
|
239
|
+
from _req r, _ob where r.who = 'R' and r.board_id = _ob.b_approve),
|
|
240
|
+
'true', 'OB-L18: a row carries the board name and expires_at = requested_at + join_request_ttl_days');
|
|
241
|
+
select is(
|
|
242
|
+
(select string_agg(board_slug || '=' || membership_state, ',' order by board_slug) from _req where who = 'X'),
|
|
243
|
+
'onb-approve=requested,onb-other=requested',
|
|
244
|
+
'OB-L19: X sees only its own requests, with its own state on the shared board');
|
|
245
|
+
|
|
246
|
+
select pg_temp.claims((select h_user::text from _ob));
|
|
247
|
+
select lives_ok($$ insert into _req select 'H', * from agent_board.list_my_board_requests() $$,
|
|
248
|
+
'OB-L20: the Portal human session lists [call succeeds]');
|
|
249
|
+
select is((select string_agg(board_slug || '=' || membership_state, ',') from _req where who = 'H'),
|
|
250
|
+
'onb-human=requested',
|
|
251
|
+
'OB-L21: a Portal human session sees its fed-human agent''s request');
|
|
252
|
+
|
|
253
|
+
select pg_temp.service();
|
|
254
|
+
select lives_ok($$ insert into _req select 'S', * from agent_board.list_my_board_requests() $$,
|
|
255
|
+
'OB-L22: a session with no caller identity calls the function [call succeeds]');
|
|
256
|
+
select is((select count(*)::int from _req where who = 'S'),
|
|
257
|
+
0, 'OB-L23: a session with no caller identity lists nothing');
|
|
258
|
+
|
|
259
|
+
-- ===========================================================================
|
|
260
|
+
-- invite_board_for_display: catalog posture
|
|
261
|
+
-- ===========================================================================
|
|
262
|
+
select ok(to_regprocedure('agent_board.invite_board_for_display(text)') is not null,
|
|
263
|
+
'OB-I01: invite_board_for_display(text) exists');
|
|
264
|
+
select is(
|
|
265
|
+
(select count(*)::int from pg_indexes
|
|
266
|
+
where schemaname = 'agent_board' and tablename = 'board_invite_tokens'
|
|
267
|
+
and indexname = 'idx_agent_board_board_invite_tokens_code_sha256'
|
|
268
|
+
and indexdef like '%digest(code, ''sha256''%'),
|
|
269
|
+
1, 'OB-I02: board_invite_tokens has the functional digest index');
|
|
270
|
+
|
|
271
|
+
-- ===========================================================================
|
|
272
|
+
-- Real operations on the links board
|
|
273
|
+
-- ===========================================================================
|
|
274
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000001');
|
|
275
|
+
do $$ begin perform agent_board.revoke_invite_link((select code_revoked from _ob)); end $$;
|
|
276
|
+
select pg_temp.service();
|
|
277
|
+
-- AGE fixture: a code minted by create_invite_link is moved past its expiry.
|
|
278
|
+
update agent_board.board_invite_tokens t set expires_at = now() - interval '1 second'
|
|
279
|
+
from _ob where t.code = _ob.code_expired;
|
|
280
|
+
|
|
281
|
+
select lives_ok($$ insert into _disp select 'exhaust-before', * from agent_board.invite_board_for_display(pg_temp.sha((select code_exhaust from _ob))) $$,
|
|
282
|
+
'OB-I03: the capped code is looked up before its last use [call succeeds]');
|
|
283
|
+
|
|
284
|
+
-- Y spends the capped code's only use.
|
|
285
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000004');
|
|
286
|
+
do $$ begin perform agent_board.redeem_invite_link((select code_exhaust from _ob)); end $$;
|
|
287
|
+
select pg_temp.service();
|
|
288
|
+
|
|
289
|
+
select lives_ok($$
|
|
290
|
+
insert into _disp select 'live', * from agent_board.invite_board_for_display(pg_temp.sha((select code_live from _ob)));
|
|
291
|
+
insert into _disp select 'live-upper', * from agent_board.invite_board_for_display(upper(pg_temp.sha((select code_live from _ob))));
|
|
292
|
+
insert into _disp select 'revoked', * from agent_board.invite_board_for_display(pg_temp.sha((select code_revoked from _ob)));
|
|
293
|
+
insert into _disp select 'expired', * from agent_board.invite_board_for_display(pg_temp.sha((select code_expired from _ob)));
|
|
294
|
+
insert into _disp select 'exhausted', * from agent_board.invite_board_for_display(pg_temp.sha((select code_exhaust from _ob)));
|
|
295
|
+
insert into _disp select 'unknown', * from agent_board.invite_board_for_display(pg_temp.sha('onb-no-such-code'));
|
|
296
|
+
$$, 'OB-I03b: every code is looked up [call succeeds]');
|
|
297
|
+
|
|
298
|
+
select is((select count(*)::int from _disp where label = 'revoked'),
|
|
299
|
+
0, 'OB-I04: a revoked code returns no row');
|
|
300
|
+
select is((select count(*)::int from _disp where label = 'expired'),
|
|
301
|
+
0, 'OB-I05: an expired code returns no row');
|
|
302
|
+
select is(
|
|
303
|
+
(select uses_count::text || '/' || max_uses::text from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_exhaust),
|
|
304
|
+
'1/1', 'OB-I06: the capped code was exhausted by a real redeem (uses_count = max_uses)');
|
|
305
|
+
select is((select count(*)::int from _disp where label = 'exhausted'),
|
|
306
|
+
0, 'OB-I07: an exhausted code returns no row');
|
|
307
|
+
select is((select count(*)::int from _disp where label = 'exhaust-before'),
|
|
308
|
+
1, 'OB-I08: the same code resolved while a use remained (the exhausted check is discriminating)');
|
|
309
|
+
select is((select count(*)::int from _disp where label = 'unknown'),
|
|
310
|
+
0, 'OB-I09: an unknown digest returns no row');
|
|
311
|
+
select is(
|
|
312
|
+
(select (d.board_id = _ob.b_links)::text || '/' || d.board_name || '/' || d.board_slug || '/' || d.preset_role
|
|
313
|
+
from _disp d, _ob where d.label = 'live'),
|
|
314
|
+
'true/ONB Links/onb-links/member',
|
|
315
|
+
'OB-I10: a usable code resolves to its board id, name, slug and preset role');
|
|
316
|
+
select is((select count(*)::int from _disp where label = 'live-upper'),
|
|
317
|
+
1, 'OB-I11: the digest is compared case-insensitively');
|
|
318
|
+
select is(has_function_privilege('service_role', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
|
|
319
|
+
true, 'OB-I12: service_role holds EXECUTE on invite_board_for_display');
|
|
320
|
+
select is(has_function_privilege('authenticated', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
|
|
321
|
+
false, 'OB-I13: authenticated holds NO EXECUTE on invite_board_for_display');
|
|
322
|
+
select is(has_function_privilege('anon', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
|
|
323
|
+
false, 'OB-I14: anon holds NO EXECUTE on invite_board_for_display');
|
|
324
|
+
select is(has_function_privilege('public', to_regprocedure('agent_board.invite_board_for_display(text)'), 'EXECUTE'),
|
|
325
|
+
false, 'OB-I15: PUBLIC holds NO EXECUTE on invite_board_for_display');
|
|
326
|
+
select is(
|
|
327
|
+
(select p.prosecdef::text || '/' || p.provolatile::text
|
|
328
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
329
|
+
where n.nspname = 'agent_board' and p.proname = 'invite_board_for_display'),
|
|
330
|
+
'true/s',
|
|
331
|
+
'OB-I16: invite_board_for_display is SECURITY DEFINER and STABLE');
|
|
332
|
+
|
|
333
|
+
-- ===========================================================================
|
|
334
|
+
-- Redeem behaviour on the real RPC (EC-ONBOARD-1's pin)
|
|
335
|
+
-- ===========================================================================
|
|
336
|
+
select pg_temp.claims('0b000000-0000-0000-0000-000000000002');
|
|
337
|
+
select lives_ok($$ select agent_board.redeem_invite_link((select code_replay from _ob)) $$,
|
|
338
|
+
'OB-R01: R redeems an unlimited invite [call succeeds]');
|
|
339
|
+
select lives_ok($$ select agent_board.redeem_invite_link((select code_replay from _ob)) $$,
|
|
340
|
+
'OB-R02: R replays the same invite [call succeeds]');
|
|
341
|
+
select is(
|
|
342
|
+
(select uses_count from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_replay),
|
|
343
|
+
1, 'redeem_replay_no_use: replaying the same invite consumes no use');
|
|
344
|
+
select lives_ok($$ select agent_board.redeem_invite_link((select code_other from _ob)) $$,
|
|
345
|
+
'OB-R03: R, already a member, redeems a different invite [call succeeds]');
|
|
346
|
+
select pg_temp.service();
|
|
347
|
+
select is(
|
|
348
|
+
(select pg_temp.state(_ob.b_links, _ob.r) || '/' || t.uses_count::text
|
|
349
|
+
from agent_board.board_invite_tokens t, _ob where t.code = _ob.code_other),
|
|
350
|
+
'joined/1', 'redeem_other_invite_consumes: redeeming a different invite while already a member consumes one');
|
|
351
|
+
|
|
352
|
+
select * from finish();
|
|
353
|
+
rollback;
|
|
@@ -19,7 +19,7 @@ set search_path = extensions, public, agent_board;
|
|
|
19
19
|
|
|
20
20
|
begin;
|
|
21
21
|
|
|
22
|
-
select plan(
|
|
22
|
+
select plan(100);
|
|
23
23
|
|
|
24
24
|
create temp view _rowid_order_sites as
|
|
25
25
|
with defs as (
|
|
@@ -506,9 +506,10 @@ select is(
|
|
|
506
506
|
true,
|
|
507
507
|
'control: fixture and structural block really runs as service_role');
|
|
508
508
|
|
|
509
|
-
-- 2. Catalog-derived inventory:
|
|
510
|
-
-- readers. request_join_board joined the set in MBCONSENT-B(iii),
|
|
511
|
-
-- it in ROOMCONTRACT
|
|
509
|
+
-- 2. Catalog-derived inventory: nine membership readers plus the two task/message idempotency
|
|
510
|
+
-- readers. request_join_board joined the set in MBCONSENT-B(iii), list_board_members joined
|
|
511
|
+
-- it in ROOMCONTRACT, and list_my_board_requests joined it in ONBOARD (v11); all read
|
|
512
|
+
-- board_memberships with the created_at DESC + id DESC tiebreak.
|
|
512
513
|
select is(
|
|
513
514
|
(select array_agg(proname order by proname)
|
|
514
515
|
from _rowid_function_order_counts),
|
|
@@ -519,12 +520,13 @@ select is(
|
|
|
519
520
|
'join_board',
|
|
520
521
|
'list_board_members',
|
|
521
522
|
'list_board_pending_memberships',
|
|
523
|
+
'list_my_board_requests',
|
|
522
524
|
'post_board_message',
|
|
523
525
|
'request_join_board',
|
|
524
526
|
'resolve_board_request',
|
|
525
527
|
'update_board_membership'
|
|
526
528
|
]::text[],
|
|
527
|
-
'live catalog exposes
|
|
529
|
+
'live catalog exposes nine membership readers plus two task/message idempotency readers');
|
|
528
530
|
|
|
529
531
|
-- 3-10. COUNT-EQUALITY, never substring containment.
|
|
530
532
|
select is(
|
|
@@ -1313,6 +1315,17 @@ select is(
|
|
|
1313
1315
|
array[2, 2],
|
|
1314
1316
|
'list_board_members: BOTH created_at DESC sites (authorization + projection) have the id DESC tiebreak');
|
|
1315
1317
|
|
|
1318
|
+
-- 100. list_my_board_requests (ONBOARD v11, 20261007000100): one statement, two created_at
|
|
1319
|
+
-- DESC sites -- the lateral that picks the caller's current membership row (active-first),
|
|
1320
|
+
-- and the result order -- and both carry the id DESC tiebreak. The exact pair, as for 99,
|
|
1321
|
+
-- so a site that disappears is not read as agreement 0 = 0.
|
|
1322
|
+
select is(
|
|
1323
|
+
(select array[created_at_desc_count, total_order_count]
|
|
1324
|
+
from _rowid_function_order_counts
|
|
1325
|
+
where proname = 'list_my_board_requests'),
|
|
1326
|
+
array[2, 2],
|
|
1327
|
+
'list_my_board_requests: BOTH created_at DESC sites (current row + result order) have the id DESC tiebreak');
|
|
1328
|
+
|
|
1316
1329
|
select finish();
|
|
1317
1330
|
|
|
1318
1331
|
rollback;
|
|
@@ -1036,6 +1036,15 @@
|
|
|
1036
1036
|
"authenticated": true
|
|
1037
1037
|
}
|
|
1038
1038
|
},
|
|
1039
|
+
{
|
|
1040
|
+
"name": "invite_board_for_display",
|
|
1041
|
+
"signature": "invite_board_for_display(p_code_sha256 text)",
|
|
1042
|
+
"executable_by": {
|
|
1043
|
+
"anon": false,
|
|
1044
|
+
"service_role": true,
|
|
1045
|
+
"authenticated": false
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1039
1048
|
{
|
|
1040
1049
|
"name": "invite_board_member",
|
|
1041
1050
|
"signature": "invite_board_member(p_board_id uuid, p_agent_id uuid, p_created_by_agent_id uuid, p_metadata jsonb)",
|
|
@@ -1090,6 +1099,15 @@
|
|
|
1090
1099
|
"authenticated": true
|
|
1091
1100
|
}
|
|
1092
1101
|
},
|
|
1102
|
+
{
|
|
1103
|
+
"name": "list_my_board_requests",
|
|
1104
|
+
"signature": "list_my_board_requests()",
|
|
1105
|
+
"executable_by": {
|
|
1106
|
+
"anon": false,
|
|
1107
|
+
"service_role": false,
|
|
1108
|
+
"authenticated": true
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1093
1111
|
{
|
|
1094
1112
|
"name": "mark_read",
|
|
1095
1113
|
"signature": "mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)",
|
|
@@ -461,6 +461,10 @@
|
|
|
461
461
|
"signature": "heartbeat(p_agent_id uuid, p_thread_id uuid, p_availability text, p_activity_summary text, p_expires_in interval, p_metadata jsonb)",
|
|
462
462
|
"declares_p_metadata": true
|
|
463
463
|
},
|
|
464
|
+
{
|
|
465
|
+
"signature": "invite_board_for_display(p_code_sha256 text)",
|
|
466
|
+
"declares_p_metadata": false
|
|
467
|
+
},
|
|
464
468
|
{
|
|
465
469
|
"signature": "invite_board_member(p_board_id uuid, p_agent_id uuid, p_created_by_agent_id uuid, p_metadata jsonb)",
|
|
466
470
|
"declares_p_metadata": true
|
|
@@ -485,6 +489,10 @@
|
|
|
485
489
|
"signature": "list_board_pending_memberships(p_board_id uuid)",
|
|
486
490
|
"declares_p_metadata": false
|
|
487
491
|
},
|
|
492
|
+
{
|
|
493
|
+
"signature": "list_my_board_requests()",
|
|
494
|
+
"declares_p_metadata": false
|
|
495
|
+
},
|
|
488
496
|
{
|
|
489
497
|
"signature": "mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)",
|
|
490
498
|
"declares_p_metadata": false
|