@consiliency/agent-board-schema 1.22.0 → 1.23.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 +36 -0
- package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +84 -3
- package/src/tooling.test.mjs +2 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +46 -2
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_inbox_reads.test.sql +16 -19
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
- package/tests/agent_board_roomlink.test.sql +2 -2
- package/tests/agent_board_wake_recovery.test.sql +756 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -47,7 +47,7 @@ begin
|
|
|
47
47
|
|
|
48
48
|
insert into _p values (
|
|
49
49
|
'https://fed-ok-'||s||'.test', 'message-board', 'portal|'||s, 'fed-agent-'||s,
|
|
50
|
-
jsonb_build_object('sub', 'portal|'||s, 'act', jsonb_build_object('sub', 'act|'||s), 'name', 'Fed '||s),
|
|
50
|
+
jsonb_build_object('iss','https://fed-ok-'||s||'.test','aud','message-board','sub', 'portal|'||s, 'act', jsonb_build_object('sub', 'act|'||s), 'name', 'Fed '||s),
|
|
51
51
|
'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0',
|
|
52
52
|
'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
53
53
|
'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk',
|
|
@@ -162,7 +162,7 @@ select is(
|
|
|
162
162
|
-- Provider / scope / kind rejection matrix still raises ------------------------------
|
|
163
163
|
select throws_ok(
|
|
164
164
|
(select format($q$select agent_board.enroll_federated_identity('https://nope.test',%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
165
|
-
aud, sub, claim::text, 'x-'||slug, pub1, fp1) from _p),
|
|
165
|
+
aud, sub, (claim || jsonb_build_object('iss','https://nope.test'))::text, 'x-'||slug, pub1, fp1) from _p),
|
|
166
166
|
'42501', null, 'unknown (issuer,audience) is rejected');
|
|
167
167
|
select throws_ok(
|
|
168
168
|
(select format($q$select agent_board.enroll_federated_identity(%L,%L,NULL,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
@@ -198,7 +198,7 @@ begin
|
|
|
198
198
|
into v_agent, v_kid, v_kver, v_handle
|
|
199
199
|
from agent_board.enroll_federated_identity(
|
|
200
200
|
'https://fed-b1-'||s||'.test','message-board','portal|'||s,
|
|
201
|
-
jsonb_build_object('sub','portal|'||s), 'b1-'||s, v_pub,'raw_ed25519_base64url', v_fp,
|
|
201
|
+
jsonb_build_object('iss','https://fed-b1-'||s||'.test','aud','message-board','sub','portal|'||s), 'b1-'||s, v_pub,'raw_ed25519_base64url', v_fp,
|
|
202
202
|
true, null, 'agent', '{board:send}', '{}') e;
|
|
203
203
|
insert into _b1 values (v_agent, v_kid, v_kver, v_handle);
|
|
204
204
|
end $$;
|
|
@@ -15,7 +15,7 @@ set search_path = extensions, public, agent_board;
|
|
|
15
15
|
|
|
16
16
|
begin;
|
|
17
17
|
|
|
18
|
-
select plan(
|
|
18
|
+
select plan(20);
|
|
19
19
|
|
|
20
20
|
-- Client<->DB fingerprint round-trip (B2 raw-byte scheme): the DB derivation over the
|
|
21
21
|
-- DECODED 32 key bytes must equal the constant an independent tool (Node crypto,
|
|
@@ -41,7 +41,7 @@ begin
|
|
|
41
41
|
values ('https://fed-pop-'||s||'.test', 'message-board', true, true, '{board:send}', '{}', '{agent,human}');
|
|
42
42
|
insert into _p values (
|
|
43
43
|
'https://fed-pop-'||s||'.test', 'message-board', 'portal|'||s,
|
|
44
|
-
jsonb_build_object('sub','portal|'||s,'act',jsonb_build_object('sub','act|'||s)),
|
|
44
|
+
jsonb_build_object('iss','https://fed-pop-'||s||'.test','aud','message-board','sub','portal|'||s,'act',jsonb_build_object('sub','act|'||s)),
|
|
45
45
|
'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0', 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
46
46
|
'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk', 'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA');
|
|
47
47
|
end
|
|
@@ -58,7 +58,17 @@ select is(
|
|
|
58
58
|
(select claim->'act'->>'sub' from _p),
|
|
59
59
|
'the act.sub is captured (audit-only) in the enrollment claim snapshot');
|
|
60
60
|
|
|
61
|
-
--
|
|
61
|
+
-- Exact replay: rotating presentation claims do not create a second identity.
|
|
62
|
+
select lives_ok(
|
|
63
|
+
(select format($q$create temp table _exact_replay as select * from agent_board.enroll_federated_identity(%L,%L,%L,(%L::jsonb || '{"iat":200,"exp":9999999999,"jti":"rotated"}'::jsonb),%L,%L,'raw_ed25519_base64url',%L,true,%L,'agent','{board:send}','{}')$q$,
|
|
64
|
+
iss, aud, sub, claim::text, 'pop-ok', pub1, fp1, 'act|present') from _p),
|
|
65
|
+
'an exact stable-input replay with rotating JWT presentation claims succeeds');
|
|
66
|
+
select is(
|
|
67
|
+
(select agent_id from _exact_replay),
|
|
68
|
+
(select a.id from agent_board.agents a where a.handle='pop-ok'),
|
|
69
|
+
'exact replay returns the original server-assigned identity');
|
|
70
|
+
|
|
71
|
+
-- A replay mutation (slug and independent actor_sub) fails closed with 23505.
|
|
62
72
|
select throws_ok(
|
|
63
73
|
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
64
74
|
iss, aud, sub, claim::text, 'pop-dup', pub1, fp1) from _p),
|
|
@@ -18,7 +18,7 @@ do $$
|
|
|
18
18
|
declare
|
|
19
19
|
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
20
20
|
v_agent uuid;
|
|
21
|
-
v_claim jsonb := jsonb_build_object('sub','portal|'||s,'name','Fed '||s,'groups',jsonb_build_array('eng','fed'));
|
|
21
|
+
v_claim jsonb := jsonb_build_object('iss','https://mock-portal-'||s||'.test','aud','message-board','sub','portal|'||s,'name','Fed '||s,'groups',jsonb_build_array('eng','fed'));
|
|
22
22
|
-- A valid unique 43-char base64url key + its matching (RPC-recomputed) fingerprint.
|
|
23
23
|
v_pub text := rtrim(translate(encode(extensions.digest(convert_to('k-acfx-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
24
24
|
v_fp text;
|
|
@@ -65,8 +65,8 @@ select is(
|
|
|
65
65
|
select has_column('agent_board', 'enrollment_federation_audit', 'human_identity_claim', 'audit stores human_identity_claim');
|
|
66
66
|
select has_column('agent_board', 'enrollment_federation_audit', 'enrolled_at', 'audit stores enrolled_at (enrollment-time)');
|
|
67
67
|
|
|
68
|
-
-- No write-path RPC reads enrollment_federation_audit.
|
|
69
|
-
--
|
|
68
|
+
-- No send/session write-path RPC reads enrollment_federation_audit. Enrollment
|
|
69
|
+
-- writes it and total human revocation reads stable principal evidence from it. ----
|
|
70
70
|
select is(
|
|
71
71
|
(select count(*)::int from (
|
|
72
72
|
select pg_get_functiondef(p.oid) as def, p.proname
|
|
@@ -79,10 +79,14 @@ select is(
|
|
|
79
79
|
-- The federation enroll RPCs WRITE the audit (enroll_federated_human added by
|
|
80
80
|
-- FEDSESSION, F-6); the invariant is that nothing on the send/session read path
|
|
81
81
|
-- touches it (asserted specifically in the next case).
|
|
82
|
-
and p.proname not in (
|
|
82
|
+
and p.proname not in (
|
|
83
|
+
'enroll_federated_identity', 'enroll_federated_human',
|
|
84
|
+
'_enroll_federated_identity_once', '_enroll_federated_human_after_revocation_gate',
|
|
85
|
+
'revoke_human_subtree'
|
|
86
|
+
)
|
|
83
87
|
) d
|
|
84
88
|
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
85
|
-
0, 'no agent_board RPC other than
|
|
89
|
+
0, 'no agent_board RPC other than federation enrollment and total human revocation references enrollment_federation_audit');
|
|
86
90
|
select is(
|
|
87
91
|
(select count(*)::int from (
|
|
88
92
|
select pg_get_functiondef(p.oid) as def
|
|
@@ -115,10 +119,10 @@ begin
|
|
|
115
119
|
values ('https://rls-portal-'||s||'.test','message-board',true,true,'{board:send}','{board:x}','{agent,human}');
|
|
116
120
|
select e.agent_id, e.auth_user_id into v_a_agent, v_a_user
|
|
117
121
|
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|a-'||s,
|
|
118
|
-
jsonb_build_object('sub','portal|a-'||s,'name','A'), 'rls-a-'||s, v_pub_a,'raw_ed25519_base64url',v_fp_a,true,null,'human','{board:send}','{board:x}') e;
|
|
122
|
+
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|a-'||s,'name','A'), 'rls-a-'||s, v_pub_a,'raw_ed25519_base64url',v_fp_a,true,null,'human','{board:send}','{board:x}') e;
|
|
119
123
|
select e.agent_id into v_b_agent
|
|
120
124
|
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|b-'||s,
|
|
121
|
-
jsonb_build_object('sub','portal|b-'||s,'name','B'), 'rls-b-'||s, v_pub_b,'raw_ed25519_base64url',v_fp_b,true,null,'human','{board:send}','{board:x}') e;
|
|
125
|
+
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|b-'||s,'name','B'), 'rls-b-'||s, v_pub_b,'raw_ed25519_base64url',v_fp_b,true,null,'human','{board:send}','{board:x}') e;
|
|
122
126
|
insert into _rls values (v_a_agent, v_a_user, v_b_agent);
|
|
123
127
|
end
|
|
124
128
|
$$;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
-- Consiliency/message-board#194 pgTAP: the board/owner lock-order CLASS, derived
|
|
2
|
+
-- from pg_constraint FK edges + the pg_proc call graph, and the class-40
|
|
3
|
+
-- propagation contract of the invite-redemption subtransaction.
|
|
4
|
+
--
|
|
5
|
+
-- WHAT IS CATALOG-DERIVED HERE, AND WHAT IS NOT. Being exact about this matters
|
|
6
|
+
-- more than the green: an earlier header of this file claimed the assertion was
|
|
7
|
+
-- "catalog-derived and not textual", and that was false. Only the TABLE SET is
|
|
8
|
+
-- catalog-derived -- the direct FK children of agent_board.boards and auth.users,
|
|
9
|
+
-- read from pg_constraint, which is the part #194 needed because
|
|
10
|
+
-- enroll_federated_identity never NAMES boards (its lock arrives as FOR KEY SHARE
|
|
11
|
+
-- through board_memberships_board_id_fkey, inside a callee). ACQUISITION DETECTION
|
|
12
|
+
-- AND THE CALL GRAPH ARE REGEXES OVER prosrc. plpgsql bodies carry no
|
|
13
|
+
-- catalog-level table dependencies, so nothing stronger is available without a
|
|
14
|
+
-- parser, and the claim this file makes is scoped accordingly.
|
|
15
|
+
--
|
|
16
|
+
-- WHAT THE ANALYZER COVERS:
|
|
17
|
+
-- * dollar-quoted blocks (tag-matched with a backreference), single-quoted
|
|
18
|
+
-- literals with doubled quotes, line comments and block comments are all removed
|
|
19
|
+
-- in ONE alternation pass, left to right, which is the correct tokenization --
|
|
20
|
+
-- two passes are wrong in BOTH orders (comments-first eats a literal containing
|
|
21
|
+
-- '--'; literals-first eats a comment containing an apostrophe). Getting this
|
|
22
|
+
-- exactly right is load-bearing, not hygiene: an earlier version of this file
|
|
23
|
+
-- could not match a block comment whose body ended in '*', and one
|
|
24
|
+
-- `/* ... for update **/` line -- the kind of lock-order note every migration in
|
|
25
|
+
-- this repo carries -- made an INVERTED enroll_federated_human pass EC-3 with
|
|
26
|
+
-- 15/15 green. Membership did not change, so EC-2's exact-set backstop could not
|
|
27
|
+
-- fire either. The same version desynced on an apostrophe inside a dollar-quoted
|
|
28
|
+
-- string (agent_board.mark_retention_metadata has a $retention$ block with 64 of
|
|
29
|
+
-- them, and it re-synced only because that count is even -- working by luck).
|
|
30
|
+
-- Both shapes are now mutation-pinned by name;
|
|
31
|
+
-- * table and function names matched BOTH qualified and BARE, with each
|
|
32
|
+
-- function's own search_path (pg_proc.proconfig) deciding whether bare names
|
|
33
|
+
-- resolve into agent_board -- a route written `insert into board_memberships`
|
|
34
|
+
-- is visible, and so is `perform _redeem_invite_for_agent(...)`;
|
|
35
|
+
-- * three acquisition shapes: INSERT into a direct FK child, an UPDATE that
|
|
36
|
+
-- re-points the FK column, and an explicit row lock in any statement shape
|
|
37
|
+
-- (including `from boards b join ... for update of b`, which an earlier
|
|
38
|
+
-- `where`-anchored pattern could not see);
|
|
39
|
+
-- * a function pinning search_path WITHOUT agent_board (e.g.
|
|
40
|
+
-- _ensure_agent_auth_user's search_path='') is not credited with bare matches;
|
|
41
|
+
-- no proconfig at all is treated as bare-resolving, over-approximating
|
|
42
|
+
-- membership rather than under-approximating it.
|
|
43
|
+
--
|
|
44
|
+
-- WHAT IT DOES NOT COVER, stated so a green run is read for what it is:
|
|
45
|
+
-- * DYNAMIC SQL -- an EXECUTE of a computed string is unreadable here. EC-6 fails
|
|
46
|
+
-- the suite for any route that acquires EITHER a board or an owner lock visibly
|
|
47
|
+
-- and uses dynamic SQL, so the common shape (one side statically, the other
|
|
48
|
+
-- hidden in an EXECUTE) is fail-closed. Gating EC-6 on BOTH sides, as an earlier
|
|
49
|
+
-- version did, made it vacuous exactly where it was needed: a route whose board
|
|
50
|
+
-- acquisition lived inside the EXECUTE never became a class member, so EC-6 never
|
|
51
|
+
-- looked at it. A route that hides BOTH sides in dynamic SQL is still invisible;
|
|
52
|
+
-- that is the residual, and it has no visible acquisition to enumerate from;
|
|
53
|
+
-- * RUNTIME vs POSITIONAL order -- EC-3 is a first-acquisition claim over
|
|
54
|
+
-- statements as written, not a per-branch path analysis;
|
|
55
|
+
-- * locks taken by TRIGGERS on these tables, and overload resolution: call edges
|
|
56
|
+
-- are name-resolved, so every overload sharing a proname is one callee. That
|
|
57
|
+
-- over-approximates edges, which is safe for EC-2 (a real member cannot vanish)
|
|
58
|
+
-- but fail-open for EC-3 (a spurious early edge could lower board_pos). The
|
|
59
|
+
-- same direction applies to `for update of <other table>` in a statement that
|
|
60
|
+
-- also names boards: counted as an acquisition when it is not one.
|
|
61
|
+
-- * the ABSENCE class (#193 -- a route that skips its board lock because the row
|
|
62
|
+
-- it would lock does not exist yet) is positionally invisible by construction
|
|
63
|
+
-- and is pinned by tests/isolation/specs/fedsession_provider_absent_owner_lock.spec.
|
|
64
|
+
--
|
|
65
|
+
-- Every shape in the covered list has a mutant that makes a NAMED assertion fail;
|
|
66
|
+
-- three of them (bare INSERT, join-form lock, FK re-point) were invisible to the
|
|
67
|
+
-- first version of this file and are the reason it was rewritten.
|
|
68
|
+
create extension if not exists pgtap with schema extensions;
|
|
69
|
+
set search_path = extensions, public, agent_board;
|
|
70
|
+
|
|
71
|
+
begin;
|
|
72
|
+
|
|
73
|
+
select plan(15);
|
|
74
|
+
|
|
75
|
+
-- ---------------------------------------------------------------------------
|
|
76
|
+
-- The catalog derivation, materialized once.
|
|
77
|
+
-- ---------------------------------------------------------------------------
|
|
78
|
+
create temp table _lockclass on commit drop as
|
|
79
|
+
with recursive
|
|
80
|
+
-- prosrc (the BODY), with string literals and both comment forms removed in ONE
|
|
81
|
+
-- pass. Two passes are wrong in both orders: comments-first eats a literal
|
|
82
|
+
-- containing '--', literals-first eats a comment containing an apostrophe. The
|
|
83
|
+
-- alternation below consumes whichever token opens first, which is the correct
|
|
84
|
+
-- tokenization.
|
|
85
|
+
fns as (
|
|
86
|
+
select p.oid,
|
|
87
|
+
p.proname,
|
|
88
|
+
p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' as sig,
|
|
89
|
+
regexp_replace(p.prosrc,
|
|
90
|
+
$re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
|
|
91
|
+
' ', 'g') as body,
|
|
92
|
+
p.proconfig
|
|
93
|
+
from pg_proc p
|
|
94
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
95
|
+
where n.nspname = 'agent_board'
|
|
96
|
+
and p.prokind = 'f'
|
|
97
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
98
|
+
),
|
|
99
|
+
-- Resolve BARE names the way Postgres would: a function that pins search_path
|
|
100
|
+
-- WITHOUT agent_board (e.g. _ensure_agent_auth_user's search_path='') cannot reach
|
|
101
|
+
-- these tables unqualified, so bare matches are not attributed to it. A function
|
|
102
|
+
-- with no search_path setting at all is treated as if bare names DO resolve, which
|
|
103
|
+
-- over-approximates membership rather than under-approximating it.
|
|
104
|
+
sp as (
|
|
105
|
+
select f.oid,
|
|
106
|
+
case
|
|
107
|
+
when not exists (select 1 from unnest(coalesce(f.proconfig, '{}')) c
|
|
108
|
+
where c like 'search\_path=%') then true
|
|
109
|
+
else exists (select 1 from unnest(f.proconfig) c
|
|
110
|
+
where c like 'search\_path=%'
|
|
111
|
+
and ',' || replace(replace(split_part(c, '=', 2), ' ', ''), '"', '') || ','
|
|
112
|
+
like '%,agent\_board,%')
|
|
113
|
+
end as bare_ok
|
|
114
|
+
from fns f
|
|
115
|
+
),
|
|
116
|
+
-- DIRECT FK children only: inserting into a grandchild locks the child row, not
|
|
117
|
+
-- the grandparent, so transitive FK depth would manufacture false positives.
|
|
118
|
+
-- NOTE: regclass::text drops the schema for anything on search_path, and this file
|
|
119
|
+
-- puts agent_board on it -- so the parent/child identity is resolved by OID and
|
|
120
|
+
-- pg_namespace here, never by a rendered name.
|
|
121
|
+
kids as (
|
|
122
|
+
select c.confrelid as parent_oid, cl.relname as tbl
|
|
123
|
+
from pg_constraint c
|
|
124
|
+
join pg_class cl on cl.oid = c.conrelid
|
|
125
|
+
join pg_namespace ns on ns.oid = cl.relnamespace
|
|
126
|
+
where c.contype = 'f'
|
|
127
|
+
and c.confrelid in ('agent_board.boards'::regclass, 'auth.users'::regclass)
|
|
128
|
+
and ns.nspname = 'agent_board'
|
|
129
|
+
),
|
|
130
|
+
alt as (
|
|
131
|
+
select (select string_agg(distinct tbl, '|') from kids
|
|
132
|
+
where parent_oid = 'agent_board.boards'::regclass) as board_kids,
|
|
133
|
+
(select string_agg(distinct tbl, '|') from kids
|
|
134
|
+
where parent_oid = 'auth.users'::regclass) as owner_kids
|
|
135
|
+
),
|
|
136
|
+
pat as (
|
|
137
|
+
select f.oid, s.bare_ok,
|
|
138
|
+
case when s.bare_ok then '(?:agent_board\.)?' else 'agent_board\.' end as q
|
|
139
|
+
from fns f join sp s on s.oid = f.oid
|
|
140
|
+
),
|
|
141
|
+
own as (
|
|
142
|
+
select f.oid, f.proname, f.sig, f.body, p.bare_ok, p.q,
|
|
143
|
+
substring(f.body from
|
|
144
|
+
-- (i) INSERT into a direct FK child takes FOR KEY SHARE on the parent row
|
|
145
|
+
'(?:insert\s+into\s+(?:only\s+)?' || p.q || '\m(?:' || a.board_kids || ')\M)'
|
|
146
|
+
-- (ii) UPDATE that re-points the FK column re-checks it and locks the new parent
|
|
147
|
+
|| '|(?:update\s+(?:only\s+)?' || p.q || '\m(?:' || a.board_kids || ')\M[^;]{0,250}?\mset\M[^;]{0,250}?\mboard_id\M\s*=)'
|
|
148
|
+
-- (iii) an explicit row lock in ANY statement shape. Deliberately not
|
|
149
|
+
-- anchored on `where`: `from boards b join ... for update of b` is a real
|
|
150
|
+
-- acquisition and the anchored form could not see it.
|
|
151
|
+
|| '|(?:' || p.q || '\mboards\M[^;]{0,250}?for\s+(?:update|no\s+key\s+update|share|key\s+share))'
|
|
152
|
+
) as board_hit,
|
|
153
|
+
substring(f.body from
|
|
154
|
+
'(?:insert\s+into\s+(?:only\s+)?' || p.q || '\m(?:' || a.owner_kids || ')\M)'
|
|
155
|
+
|| '|(?:update\s+(?:only\s+)?' || p.q || '\m(?:' || a.owner_kids || ')\M[^;]{0,250}?\mset\M[^;]{0,250}?\m(?:owner_user_id|auth_user_id)\M\s*=)'
|
|
156
|
+
|| '|(?:auth\.users\M[^;]{0,250}?for\s+update)'
|
|
157
|
+
) as owner_hit
|
|
158
|
+
from fns f join pat p on p.oid = f.oid cross join alt a
|
|
159
|
+
),
|
|
160
|
+
edges as (
|
|
161
|
+
select c.oid as caller, e.oid as callee
|
|
162
|
+
from own c
|
|
163
|
+
join fns e on e.oid <> c.oid
|
|
164
|
+
where c.body ~ (c.q || '\m' || e.proname || '\M\s*\(')
|
|
165
|
+
),
|
|
166
|
+
acq_board(oid) as (
|
|
167
|
+
select oid from own where board_hit is not null
|
|
168
|
+
union
|
|
169
|
+
select e.caller from edges e join acq_board a on a.oid = e.callee
|
|
170
|
+
),
|
|
171
|
+
acq_owner(oid) as (
|
|
172
|
+
select oid from own where owner_hit is not null
|
|
173
|
+
union
|
|
174
|
+
select e.caller from edges e join acq_owner a on a.oid = e.callee
|
|
175
|
+
)
|
|
176
|
+
select o.sig,
|
|
177
|
+
(o.oid in (select oid from acq_board)) as reaches_board,
|
|
178
|
+
(o.oid in (select oid from acq_owner)) as reaches_owner,
|
|
179
|
+
(o.board_hit is not null) as own_board_lock,
|
|
180
|
+
(o.owner_hit is not null) as own_owner_lock,
|
|
181
|
+
(o.body ~* '\mexecute\M') as uses_dynamic_sql,
|
|
182
|
+
least(
|
|
183
|
+
case when o.board_hit is null then 2147483647 else strpos(o.body, o.board_hit) end,
|
|
184
|
+
coalesce((select min(strpos(o.body, substring(o.body from o.q || '\m' || f2.proname || '\M\s*\(')))
|
|
185
|
+
from fns f2
|
|
186
|
+
where f2.oid <> o.oid
|
|
187
|
+
and f2.oid in (select oid from acq_board)
|
|
188
|
+
and o.body ~ (o.q || '\m' || f2.proname || '\M\s*\(')), 2147483647)
|
|
189
|
+
) as board_pos,
|
|
190
|
+
least(
|
|
191
|
+
case when o.owner_hit is null then 2147483647 else strpos(o.body, o.owner_hit) end,
|
|
192
|
+
coalesce((select min(strpos(o.body, substring(o.body from o.q || '\m' || f2.proname || '\M\s*\(')))
|
|
193
|
+
from fns f2
|
|
194
|
+
where f2.oid <> o.oid
|
|
195
|
+
and f2.oid in (select oid from acq_owner)
|
|
196
|
+
and o.body ~ (o.q || '\m' || f2.proname || '\M\s*\(')), 2147483647)
|
|
197
|
+
) as owner_pos
|
|
198
|
+
from own o;
|
|
199
|
+
|
|
200
|
+
-- EC-1: the FK edges the derivation rests on. If either disappears (renamed table,
|
|
201
|
+
-- dropped constraint) the class silently empties and every other assertion here
|
|
202
|
+
-- passes vacuously, so assert the two load-bearing edges POSITIVELY.
|
|
203
|
+
select is(
|
|
204
|
+
(select string_agg(c.conname, ', ' order by c.conname)
|
|
205
|
+
from pg_constraint c
|
|
206
|
+
where c.contype = 'f'
|
|
207
|
+
and ((c.confrelid = 'agent_board.boards'::regclass
|
|
208
|
+
and c.conrelid = 'agent_board.board_memberships'::regclass)
|
|
209
|
+
or (c.confrelid = 'auth.users'::regclass
|
|
210
|
+
and c.conrelid = 'agent_board.agents'::regclass))),
|
|
211
|
+
'agents_auth_user_id_fkey, agents_owner_user_id_fkey, board_memberships_board_id_fkey',
|
|
212
|
+
'EC-1: the invisible FK edges (memberships->boards, agents->auth.users) both exist');
|
|
213
|
+
|
|
214
|
+
-- EC-2: the CLASS itself -- every agent_board function that acquires both a boards
|
|
215
|
+
-- row lock and an auth.users row lock. Asserted as an exact set: a new member fails
|
|
216
|
+
-- here and NAMES itself instead of joining an unexamined class.
|
|
217
|
+
select is(
|
|
218
|
+
(select string_agg(split_part(sig, '(', 1), ', ' order by split_part(sig, '(', 1))
|
|
219
|
+
from _lockclass where reaches_board and reaches_owner),
|
|
220
|
+
'_enroll_federated_human_after_revocation_gate, _enroll_federated_identity_once, '
|
|
221
|
+
|| 'enroll_federated_human, enroll_federated_identity, onboard_federated_fleet_agent',
|
|
222
|
+
'EC-2: the board+owner lock class is exactly the five federated write routes');
|
|
223
|
+
|
|
224
|
+
-- EC-3: THE ORDERING INVARIANT. Never acquire auth.users before boards. The failure
|
|
225
|
+
-- prints the offending signatures (pre-fix this named enroll_federated_identity and
|
|
226
|
+
-- _enroll_federated_identity_once).
|
|
227
|
+
select is(
|
|
228
|
+
(select coalesce(string_agg(split_part(sig, '(', 1), ', ' order by sig), '')
|
|
229
|
+
from _lockclass
|
|
230
|
+
where reaches_board and reaches_owner and board_pos >= owner_pos),
|
|
231
|
+
'',
|
|
232
|
+
'EC-3: every board+owner route acquires the board BEFORE the owner');
|
|
233
|
+
|
|
234
|
+
-- EC-4: positive exemption. revoke_human_subtree DOES take the auth.users owner
|
|
235
|
+
-- lock and is deliberately outside the class; assert its claim positively (it
|
|
236
|
+
-- reaches no boards acquisition at all), never by absence from a match.
|
|
237
|
+
select is(
|
|
238
|
+
(select own_owner_lock::text || '/' || reaches_board::text
|
|
239
|
+
from _lockclass where sig like 'revoke\_human\_subtree(%'),
|
|
240
|
+
'true/false',
|
|
241
|
+
'EC-4: revoke_human_subtree locks the owner and provably acquires no board');
|
|
242
|
+
|
|
243
|
+
-- EC-5: the two routes this migration moved take their board lock in their own
|
|
244
|
+
-- body (not merely through a callee), so a future refactor of the callee cannot
|
|
245
|
+
-- silently drop the ordering.
|
|
246
|
+
select is(
|
|
247
|
+
(select string_agg(split_part(sig, '(', 1), ', ' order by sig)
|
|
248
|
+
from _lockclass
|
|
249
|
+
where own_board_lock
|
|
250
|
+
and split_part(sig, '(', 1) in ('enroll_federated_identity', '_enroll_federated_identity_once')),
|
|
251
|
+
'_enroll_federated_identity_once, enroll_federated_identity',
|
|
252
|
+
'EC-5: both enroll routes name their own agent_board.boards row lock');
|
|
253
|
+
|
|
254
|
+
-- EC-6: no class member uses dynamic SQL. EC-3's positional claim reads statements
|
|
255
|
+
-- as written; an EXECUTE of a computed string is invisible to it. Rather than
|
|
256
|
+
-- leave that as a caveat in prose, fail the suite the day a member acquires one.
|
|
257
|
+
select is(
|
|
258
|
+
(select coalesce(string_agg(split_part(sig, '(', 1), ', ' order by sig), '')
|
|
259
|
+
from _lockclass
|
|
260
|
+
where (reaches_board or reaches_owner) and uses_dynamic_sql),
|
|
261
|
+
'',
|
|
262
|
+
'EC-6: no board+owner route uses dynamic SQL, which EC-3 could not read');
|
|
263
|
+
|
|
264
|
+
-- ---------------------------------------------------------------------------
|
|
265
|
+
-- Class-40 propagation. The redemption subtransaction must keep swallowing
|
|
266
|
+
-- deterministic failures (soft-honor) and must STOP swallowing serialization
|
|
267
|
+
-- failures, which reported enrollment SUCCESS with no membership (#194).
|
|
268
|
+
-- ---------------------------------------------------------------------------
|
|
269
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
270
|
+
values ('194a0000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
271
|
+
'lo194-owner', 'LO194 Owner', 'active');
|
|
272
|
+
|
|
273
|
+
select count(*) from agent_board.create_board(
|
|
274
|
+
'194a0000-0000-0000-0000-000000000001', 'lo194-board', 'LO194 Board',
|
|
275
|
+
'Consiliency/message-board#194 redemption-failure class', jsonb_build_object(), 'private');
|
|
276
|
+
|
|
277
|
+
insert into agent_board.identity_providers(
|
|
278
|
+
issuer, audience, supported, policy_approved,
|
|
279
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
280
|
+
values ('https://lockorder194.test', 'message-board', true, true,
|
|
281
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human']);
|
|
282
|
+
|
|
283
|
+
insert into agent_board.board_invite_tokens(
|
|
284
|
+
code, board_id, preset_role, preset_membership_state, max_uses, expires_at, created_by_agent_id)
|
|
285
|
+
select 'lo194-code-' || g, b.id, 'member', 'joined', 10,
|
|
286
|
+
now() + interval '1 hour', '194a0000-0000-0000-0000-000000000001'
|
|
287
|
+
from agent_board.boards b, generate_series(1, 3) g
|
|
288
|
+
where b.slug = 'lo194-board';
|
|
289
|
+
|
|
290
|
+
-- Capture tables are created EMPTY and filled inside lives_ok: if the call raises,
|
|
291
|
+
-- the assertion reports a named `not ok` and every later assertion still RUNS.
|
|
292
|
+
-- (`create temp table _e as select * from <rpc>` inside lives_ok leaves the table
|
|
293
|
+
-- missing on failure, which aborts the file with no further TAP output -- observed
|
|
294
|
+
-- while mutation-testing this very file.)
|
|
295
|
+
create temp table _e1 (agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
|
|
296
|
+
granted_scopes text[], key_id text, key_version int, agent_handle text, invite_redeemed boolean)
|
|
297
|
+
on commit drop;
|
|
298
|
+
create temp table _e2 (like _e1) on commit drop;
|
|
299
|
+
create temp table _e3 (like _e1) on commit drop;
|
|
300
|
+
|
|
301
|
+
-- R-1/R-2/R-3: uncontended redemption still works -- the allow side, so a later
|
|
302
|
+
-- "it raised" is not confused with "it never redeemed".
|
|
303
|
+
select lives_ok(
|
|
304
|
+
$$ insert into _e1
|
|
305
|
+
select * from agent_board.enroll_federated_identity(
|
|
306
|
+
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
307
|
+
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
308
|
+
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
309
|
+
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
310
|
+
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-1') $$,
|
|
311
|
+
'R-1: fresh federated enrollment with a usable invite succeeds');
|
|
312
|
+
select is((select invite_redeemed from _e1), true,
|
|
313
|
+
'R-2: the fresh enrollment reports the invite redeemed');
|
|
314
|
+
select is(
|
|
315
|
+
(select count(*)::int from agent_board.board_memberships m
|
|
316
|
+
join agent_board.boards b on b.id = m.board_id
|
|
317
|
+
where b.slug = 'lo194-board'
|
|
318
|
+
and m.agent_id = (select agent_id from _e1)
|
|
319
|
+
and m.membership_state = 'joined'),
|
|
320
|
+
1, 'R-3: the fresh enrollment landed a joined membership row on the board');
|
|
321
|
+
|
|
322
|
+
-- R-4/R-5: a DETERMINISTIC redemption failure (unusable code) is still soft-honored
|
|
323
|
+
-- -- the released contract, and the exemption the class-40 filter must not widen.
|
|
324
|
+
select lives_ok(
|
|
325
|
+
$$ insert into _e2
|
|
326
|
+
select * from agent_board.enroll_federated_identity(
|
|
327
|
+
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
328
|
+
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
329
|
+
'lo194-b', public.test_pubkey('lo194-k2'), 'raw_ed25519_base64url',
|
|
330
|
+
public.test_key_fingerprint('lo194-k2'), true, null, 'agent',
|
|
331
|
+
ARRAY[]::text[], ARRAY[]::text[], 'lo194-no-such-code') $$,
|
|
332
|
+
'R-4: an unusable invite code still enrolls (soft-honor preserved)');
|
|
333
|
+
select is((select invite_redeemed from _e2), false,
|
|
334
|
+
'R-5: the unusable code reports invite_redeemed=false rather than raising');
|
|
335
|
+
|
|
336
|
+
-- Fault injection: make the redemption raise a serialization failure exactly where a
|
|
337
|
+
-- deadlock victim would surface it (the ledger insert inside
|
|
338
|
+
-- _redeem_unbounded_invite_for_agent), with no concurrency required.
|
|
339
|
+
create function pg_temp._lo194_raise_40001() returns trigger
|
|
340
|
+
language plpgsql as $$
|
|
341
|
+
begin
|
|
342
|
+
raise exception 'simulated serialization failure during redemption' using errcode = '40001';
|
|
343
|
+
end;
|
|
344
|
+
$$;
|
|
345
|
+
create trigger _lo194_serialization_failure
|
|
346
|
+
before insert on agent_board.board_invite_redemptions
|
|
347
|
+
for each row execute function pg_temp._lo194_raise_40001();
|
|
348
|
+
|
|
349
|
+
-- R-6: the CREATE path (_enroll_federated_identity_once step 13). Pre-fix this
|
|
350
|
+
-- returned a row with invite_redeemed=false and no error.
|
|
351
|
+
select throws_ok(
|
|
352
|
+
$$ select * from agent_board.enroll_federated_identity(
|
|
353
|
+
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
354
|
+
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
355
|
+
'lo194-c', public.test_pubkey('lo194-k3'), 'raw_ed25519_base64url',
|
|
356
|
+
public.test_key_fingerprint('lo194-k3'), true, null, 'agent',
|
|
357
|
+
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-2') $$,
|
|
358
|
+
'40001', null,
|
|
359
|
+
'R-6: class 40 during the CREATE-path redemption propagates to the caller');
|
|
360
|
+
|
|
361
|
+
-- R-7: the REPLAY path (enroll_federated_identity own catch). Same key as R-1, so the
|
|
362
|
+
-- frozen replay contract matches and a second, unredeemed code is offered.
|
|
363
|
+
select throws_ok(
|
|
364
|
+
$$ select * from agent_board.enroll_federated_identity(
|
|
365
|
+
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
366
|
+
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
367
|
+
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
368
|
+
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
369
|
+
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-3') $$,
|
|
370
|
+
'40001', null,
|
|
371
|
+
'R-7: class 40 during the REPLAY-path redemption propagates to the caller');
|
|
372
|
+
|
|
373
|
+
drop trigger _lo194_serialization_failure on agent_board.board_invite_redemptions;
|
|
374
|
+
|
|
375
|
+
-- R-8/R-9: with the injection removed, the replay path still soft-honors a
|
|
376
|
+
-- deterministic failure -- the filter narrowed the catch, it did not remove it.
|
|
377
|
+
select lives_ok(
|
|
378
|
+
$$ insert into _e3
|
|
379
|
+
select * from agent_board.enroll_federated_identity(
|
|
380
|
+
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
381
|
+
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
382
|
+
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
383
|
+
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
384
|
+
ARRAY[]::text[], ARRAY[]::text[], 'lo194-no-such-code') $$,
|
|
385
|
+
'R-8: replay with an unusable code still succeeds');
|
|
386
|
+
select is((select invite_redeemed from _e3), false,
|
|
387
|
+
'R-9: replay with an unusable code reports invite_redeemed=false');
|
|
388
|
+
|
|
389
|
+
select * from finish();
|
|
390
|
+
rollback;
|
|
@@ -17,7 +17,7 @@ set search_path = extensions, public, agent_board;
|
|
|
17
17
|
|
|
18
18
|
begin;
|
|
19
19
|
|
|
20
|
-
select plan(
|
|
20
|
+
select plan(41);
|
|
21
21
|
|
|
22
22
|
-- ---------------------------------------------------------------------------
|
|
23
23
|
-- Phase 0 (superuser): function metadata + privileges
|
|
@@ -216,7 +216,7 @@ begin
|
|
|
216
216
|
v_fp := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
217
217
|
select e.agent_id into v_agent
|
|
218
218
|
from agent_board.enroll_federated_identity(
|
|
219
|
-
f.iss, 'message-board', f.sub_a, jsonb_build_object('sub', f.sub_a),
|
|
219
|
+
f.iss, 'message-board', f.sub_a, jsonb_build_object('iss',f.iss,'aud','message-board','sub', f.sub_a),
|
|
220
220
|
'idlink-'||f.s, v_pub, 'raw_ed25519_base64url', v_fp,
|
|
221
221
|
true, null, 'agent', '{board:send}', '{}') e;
|
|
222
222
|
insert into _idlink select a.owner_user_id from agent_board.agents a where a.id = v_agent;
|
|
@@ -311,6 +311,50 @@ select throws_ok(
|
|
|
311
311
|
iss, sub_a, jsonb_build_object('sub', sub_a)::text) from _f where sub_a like 'portal-user-%'),
|
|
312
312
|
'42501', null, 'a suspended (non-active) fed-human cannot re-provision a session (42501)');
|
|
313
313
|
|
|
314
|
+
-- Durable total-human revocation also closes the production /portal-session RPC seam,
|
|
315
|
+
-- including after auth.users deletion would otherwise recreate the deterministic owner.
|
|
316
|
+
create temp table _portal_revoked(
|
|
317
|
+
issuer text, subject text, agent_id uuid, auth_user_id uuid
|
|
318
|
+
) on commit drop;
|
|
319
|
+
do $$
|
|
320
|
+
declare
|
|
321
|
+
s4 text := replace(substr(gen_random_uuid()::text,1,8),'-','');
|
|
322
|
+
v_issuer text := 'https://fed-revoked-' || s4 || '.test';
|
|
323
|
+
v_subject text := 'portal-revoked-' || s4;
|
|
324
|
+
v_agent uuid;
|
|
325
|
+
v_auth uuid;
|
|
326
|
+
begin
|
|
327
|
+
insert into agent_board.identity_providers
|
|
328
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
329
|
+
values (v_issuer, 'message-board', true, true, '{}', '{}', '{human}');
|
|
330
|
+
select agent_id, auth_user_id into v_agent, v_auth
|
|
331
|
+
from agent_board.enroll_federated_human(
|
|
332
|
+
v_issuer, 'message-board', v_subject, jsonb_build_object('sub', v_subject), null);
|
|
333
|
+
insert into _portal_revoked values (v_issuer, v_subject, v_agent, v_auth);
|
|
334
|
+
perform * from agent_board.revoke_human_subtree(
|
|
335
|
+
v_auth, (select agent_id from _r where label='B'), 'portal-session durable revoke');
|
|
336
|
+
delete from auth.users where id = v_auth;
|
|
337
|
+
end $$;
|
|
338
|
+
-- 36
|
|
339
|
+
select ok(
|
|
340
|
+
pg_get_functiondef('agent_board.enroll_federated_human(text,text,text,jsonb,text)'::regprocedure)
|
|
341
|
+
like '%from agent_board.boards b%for update%from agent_board.identity_providers ip%for update%_ensure_agent_auth_user(v_handle)%from auth.users u where u.id = v_uid for update%fleet_human_revocations r%r.issuer = p_issuer%r.subject = p_subject%_enroll_federated_human_after_revocation_gate%',
|
|
342
|
+
'/portal-session locks default board then provider then owner and checks the durable marker before delegation');
|
|
343
|
+
-- 37
|
|
344
|
+
select throws_ok(
|
|
345
|
+
(select format($q$select * from agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
346
|
+
issuer, subject, jsonb_build_object('sub', subject)::text) from _portal_revoked),
|
|
347
|
+
'42501', null, '/portal-session cannot recreate a durably revoked human after auth-user deletion');
|
|
348
|
+
-- 38
|
|
349
|
+
select is(
|
|
350
|
+
(select count(*)::int from agent_board.agents
|
|
351
|
+
where id=(select agent_id from _portal_revoked) and status='active'),
|
|
352
|
+
0, 'blocked /portal-session leaves the original principal revoked');
|
|
353
|
+
-- 39
|
|
354
|
+
select is(
|
|
355
|
+
(select count(*)::int from auth.users where id=(select auth_user_id from _portal_revoked)),
|
|
356
|
+
0, 'blocked /portal-session rolls back deterministic auth-user recreation');
|
|
357
|
+
|
|
314
358
|
select finish();
|
|
315
359
|
|
|
316
360
|
rollback;
|