@consiliency/agent-board-schema 1.23.0 → 1.25.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/README.md +75 -1
- package/fixtures/already-applied.json +12 -0
- package/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
- package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
- package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/check.js +23 -0
- package/src/index.js +20 -0
- package/src/plan.js +79 -5
- package/src/tooling.test.mjs +237 -4
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +124 -112
- package/tests/agent_board_claimkey.test.sql +25 -3
- package/tests/agent_board_consent_emission.test.sql +17 -13
- package/tests/agent_board_consent_gating.test.sql +55 -11
- package/tests/agent_board_delegation_policies.test.sql +3 -1
- package/tests/agent_board_enroll_resumption.test.sql +412 -0
- package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
- package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
- package/tests/agent_board_mint_harden.test.sql +39 -4
- package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
- package/tests/agent_board_rejectsem.test.sql +1 -1
- package/tests/agent_board_rolereset.test.sql +23 -2
- package/tests/agent_board_rowid.test.sql +24 -13
- package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
- package/tests/agent_board_signed_authority_provenance.test.sql +454 -0
- package/tests/agent_board_source_strip_helper.test.sql +285 -0
- package/tests/agent_board_wake_recovery.test.sql +58 -40
- package/tests/catalog/p_metadata_parameters.json +689 -0
- package/tests/fixtures/test-prereqs.sql +313 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
|
@@ -16,7 +16,7 @@ set search_path = extensions, public, agent_board;
|
|
|
16
16
|
|
|
17
17
|
begin;
|
|
18
18
|
|
|
19
|
-
select plan(
|
|
19
|
+
select plan(26);
|
|
20
20
|
|
|
21
21
|
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
22
22
|
select set_config('request.jwt.claim.sub', '', true);
|
|
@@ -219,11 +219,33 @@ select is(
|
|
|
219
219
|
join pg_namespace n on n.oid = p.pronamespace
|
|
220
220
|
where n.nspname = 'agent_board'
|
|
221
221
|
and p.prokind = 'f'
|
|
222
|
-
and
|
|
223
|
-
|
|
222
|
+
and agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+(agent_board\.)?claims\M'
|
|
223
|
+
-- message-board#196 Bucket B: read RAW, one comment naming _canonical_claim_key
|
|
224
|
+
-- dropped a bypass writer out of this set and the count went to 0 green.
|
|
225
|
+
-- anchored to the CALL shape: a bare-substring `!~` is satisfied by any
|
|
226
|
+
-- identifier containing the name, e.g. an argument `p_canonical_claim_key`.
|
|
227
|
+
-- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
|
|
228
|
+
-- IDENTITY: `\m_canonical_claim_key\s*\(` also matches
|
|
229
|
+
-- `zz_other._canonical_claim_key(...)` (measured true). Every live call
|
|
230
|
+
-- site in agent_board is `agent_board.`-qualified, so requiring the
|
|
231
|
+
-- qualification costs nothing today and is fail-CLOSED if someone later writes
|
|
232
|
+
-- an unqualified call (it reads as NOT calling). Overload count is pinned
|
|
233
|
+
-- separately, so schema + name is the identity.
|
|
234
|
+
and agent_board_test.stripped_body(p.oid) !~* '\magent_board\._canonical_claim_key\s*\('),
|
|
224
235
|
0,
|
|
225
236
|
'every catalog-enumerated claims writer routes its resource_key through _canonical_claim_key');
|
|
226
237
|
|
|
238
|
+
|
|
239
|
+
-- The schema-qualified call anchor above treats `agent_board._canonical_claim_key` as an
|
|
240
|
+
-- IDENTITY. That is only true while the name resolves to exactly one function:
|
|
241
|
+
-- a second overload would be a different implementation the same anchor accepts.
|
|
242
|
+
-- Catalog-derived, so a new overload fails here and the count names itself.
|
|
243
|
+
select is(
|
|
244
|
+
(select count(*)::int from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
245
|
+
where n.nspname = 'agent_board' and p.proname = '_canonical_claim_key'),
|
|
246
|
+
1,
|
|
247
|
+
'agent_board._canonical_claim_key has exactly ONE overload, so the schema-qualified call anchor identifies it uniquely');
|
|
248
|
+
|
|
227
249
|
select * from finish();
|
|
228
250
|
|
|
229
251
|
rollback;
|
|
@@ -222,21 +222,18 @@ select is(
|
|
|
222
222
|
-- anywhere in the body is fail-open twice over: the name in a comment or a dead branch would
|
|
223
223
|
-- satisfy it, and calling the emitter TWICE would too -- while this suite claims exactly one
|
|
224
224
|
-- event per transition.
|
|
225
|
-
--
|
|
226
|
-
--
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
regexp_replace(pg_get_functiondef(p_oid), '/\*.*?\*/', '', 'gs'),
|
|
230
|
-
'--[^\n]*', '', 'g');
|
|
231
|
-
$$;
|
|
225
|
+
-- The stripped reader is agent_board_test.stripped_body(), shared by the whole
|
|
226
|
+
-- suite from tests/fixtures/test-prereqs.sql. The local version this replaces
|
|
227
|
+
-- stripped both comment forms with a regex -- which still could not see a NESTED
|
|
228
|
+
-- block comment (message-board#199) or a string literal.
|
|
232
229
|
|
|
233
230
|
select is(
|
|
234
231
|
(select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
|
|
235
232
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
236
233
|
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
237
234
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
238
|
-
and
|
|
239
|
-
and
|
|
235
|
+
and agent_board_test.stripped_body(p.oid) ~* '(insert into|update)[[:space:]]+agent_board\.board_memberships'
|
|
236
|
+
and agent_board_test.stripped_body(p.oid) !~* 'perform agent_board\._emit_membership_event'
|
|
240
237
|
and p.proname <> 'grant_board_role'),
|
|
241
238
|
'',
|
|
242
239
|
'COMPLETENESS: every function writing board_memberships emits, or is a stated exemption');
|
|
@@ -244,14 +241,21 @@ select is(
|
|
|
244
241
|
-- ...and none of them emits MORE than once, which would break one-event-per-transition.
|
|
245
242
|
select is(
|
|
246
243
|
(select coalesce(string_agg(p.proname || '=' ||
|
|
247
|
-
(length(
|
|
244
|
+
(length(agent_board_test.stripped_body(p.oid)) - length(replace(lower(agent_board_test.stripped_body(p.oid)),
|
|
248
245
|
'perform agent_board._emit_membership_event', '')))
|
|
249
246
|
/ length('perform agent_board._emit_membership_event'), ', ' order by p.proname), '')
|
|
250
247
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
251
248
|
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
252
249
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
253
|
-
and
|
|
254
|
-
|
|
250
|
+
-- CASE-INSENSITIVE, and that is load-bearing rather than tidiness. SQL folds
|
|
251
|
+
-- unquoted identifiers, but `prosrc` is stored VERBATIM, so a writer spelling
|
|
252
|
+
-- the same call `AGENT_BOARD.…` or the same column `FINGERPRINT` is identical
|
|
253
|
+
-- to PostgreSQL and invisible to a case-sensitive scan. For a MEMBERSHIP
|
|
254
|
+
-- filter that is the fail-OPEN direction: the offender simply leaves the
|
|
255
|
+
-- class and the expected-empty assertion stays green. (Same mechanism
|
|
256
|
+
-- agent_board_wake_recovery.test.sql records for its own WR-7 filter.)
|
|
257
|
+
and agent_board_test.stripped_body(p.oid) ~* 'perform agent_board\._emit_membership_event'
|
|
258
|
+
and (length(agent_board_test.stripped_body(p.oid)) - length(replace(lower(agent_board_test.stripped_body(p.oid)),
|
|
255
259
|
'perform agent_board._emit_membership_event', '')))
|
|
256
260
|
/ length('perform agent_board._emit_membership_event') <> 1),
|
|
257
261
|
'',
|
|
@@ -259,7 +263,7 @@ select is(
|
|
|
259
263
|
|
|
260
264
|
-- ...and the one exemption is genuinely state-blind: membership_state never appears in a SET.
|
|
261
265
|
select ok(
|
|
262
|
-
(select
|
|
266
|
+
(select agent_board_test.stripped_body(p.oid) !~* 'set[^;]*membership_state[[:space:]]*='
|
|
263
267
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
264
268
|
where n.nspname = 'agent_board' and p.proname = 'grant_board_role'),
|
|
265
269
|
'COMPLETENESS: the exemption holds -- grant_board_role never SETs membership_state');
|
|
@@ -17,7 +17,7 @@ set search_path = extensions, public, agent_board;
|
|
|
17
17
|
|
|
18
18
|
begin;
|
|
19
19
|
|
|
20
|
-
select plan(
|
|
20
|
+
select plan(54);
|
|
21
21
|
|
|
22
22
|
create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
|
|
23
23
|
begin
|
|
@@ -298,14 +298,44 @@ select is(
|
|
|
298
298
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
299
299
|
and has_function_privilege('authenticated', p.oid, 'EXECUTE')
|
|
300
300
|
-- reaches an active membership, directly or through the shared sink
|
|
301
|
-
and
|
|
301
|
+
and agent_board_test.stripped_body(p.oid)
|
|
302
302
|
-- Includes update_board_membership: a one-hop WRAPPER calling it with 'joined'
|
|
303
303
|
-- reaches an active membership without ever naming the sink, and would otherwise
|
|
304
304
|
-- evade this class assertion entirely.
|
|
305
|
-
|
|
306
|
-
--
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
-- CASE-INSENSITIVE, and that is load-bearing rather than tidiness. SQL folds
|
|
306
|
+
-- unquoted identifiers, but `prosrc` is stored VERBATIM, so a writer spelling
|
|
307
|
+
-- the same call `AGENT_BOARD.…` or the same column `FINGERPRINT` is identical
|
|
308
|
+
-- to PostgreSQL and invisible to a case-sensitive scan. For a MEMBERSHIP
|
|
309
|
+
-- filter that is the fail-OPEN direction: the offender simply leaves the
|
|
310
|
+
-- class and the expected-empty assertion stays green. (Same mechanism
|
|
311
|
+
-- agent_board_wake_recovery.test.sql records for its own WR-7 filter.)
|
|
312
|
+
~* '(_upsert_board_membership_state|_redeem_invite_for_agent|update_board_membership|insert into agent_board\.board_memberships)'
|
|
313
|
+
-- gated: consults the predicate, or refuses a non-subject actor structurally.
|
|
314
|
+
--
|
|
315
|
+
-- SPLIT IN TWO, by what kind of thing each marker IS. The single `!~` over a
|
|
316
|
+
-- three-way alternation could not be read correctly in either mode: the
|
|
317
|
+
-- helper name is CODE and the other two are raise MESSAGES, so
|
|
318
|
+
-- literal-stripping deleted the messages (every route then looked ungated)
|
|
319
|
+
-- and literal-keeping left the messages maskable by any string.
|
|
320
|
+
--
|
|
321
|
+
-- The message half is therefore ANCHORED TO THE STATEMENT SHAPE
|
|
322
|
+
-- (message-board#196 remedy 2) rather than to the bare message: the text has
|
|
323
|
+
-- to follow a `raise exception` to count. Measured before the split: a
|
|
324
|
+
-- `raise debug 'gate: %', '_membership_consent_present';` planted in an
|
|
325
|
+
-- ungated update_board_membership took this class assertion GREEN with the
|
|
326
|
+
-- gate genuinely removed. It does not any more.
|
|
327
|
+
-- anchored to the CALL shape, for the same reason the message half below is
|
|
328
|
+
-- anchored to the raise shape: a bare substring is satisfied by a NAME.
|
|
329
|
+
-- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
|
|
330
|
+
-- IDENTITY: `\m_membership_consent_present\s*\(` also matches
|
|
331
|
+
-- `zz_other._membership_consent_present(...)` (measured true). Every live call
|
|
332
|
+
-- site in agent_board is `agent_board.`-qualified, so requiring the
|
|
333
|
+
-- qualification costs nothing today and is fail-CLOSED if someone later writes
|
|
334
|
+
-- an unqualified call (it reads as NOT calling). Overload count is pinned
|
|
335
|
+
-- separately, so schema + name is the identity.
|
|
336
|
+
and agent_board_test.stripped_body(p.oid) !~* '\magent_board\._membership_consent_present\s*\('
|
|
337
|
+
and agent_board_test.stripped_body(p.oid, true)
|
|
338
|
+
!~ 'raise[[:space:]]+exception[^;]{0,250}(actor mismatch|requires an act by that agent)'
|
|
309
339
|
-- stated exemptions, each with a reason recorded in the migration header
|
|
310
340
|
and p.proname not in (
|
|
311
341
|
'create_board', -- the actor IS the subject: it seeds the creator's own seat
|
|
@@ -329,7 +359,9 @@ select is(
|
|
|
329
359
|
from pg_proc p
|
|
330
360
|
join pg_namespace n on n.oid = p.pronamespace
|
|
331
361
|
cross join lateral regexp_matches(
|
|
332
|
-
|
|
362
|
+
-- LITERALS KEPT: the marker is the ARGUMENT ''joined''/''subscribed'' passed
|
|
363
|
+
-- to the sink -- a string literal in the code being matched.
|
|
364
|
+
agent_board_test.stripped_body(p.oid, true),
|
|
333
365
|
'(?:_upsert_board_membership_state|update_board_membership)\s*\([^;]*?''(joined|subscribed)''',
|
|
334
366
|
'g') m
|
|
335
367
|
where n.nspname = 'agent_board'
|
|
@@ -343,7 +375,8 @@ select is(
|
|
|
343
375
|
from pg_proc p
|
|
344
376
|
join pg_namespace n on n.oid = p.pronamespace
|
|
345
377
|
cross join lateral regexp_matches(
|
|
346
|
-
|
|
378
|
+
-- LITERALS KEPT: same shape as above -- the state being written is a literal.
|
|
379
|
+
agent_board_test.stripped_body(p.oid, true),
|
|
347
380
|
'(?:_upsert_board_membership_state|update_board_membership)\s*\([^;]*?''(invited|requested|left)''',
|
|
348
381
|
'g') m
|
|
349
382
|
where n.nspname = 'agent_board'
|
|
@@ -389,7 +422,7 @@ select is(
|
|
|
389
422
|
-- silently reopening the race -- the classic wrong "fix" for lock-wait reports. `nowait`
|
|
390
423
|
-- is excluded too (it at least fails loudly).
|
|
391
424
|
select matches(
|
|
392
|
-
(select
|
|
425
|
+
(select agent_board_test.stripped_body(p.oid)
|
|
393
426
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
394
427
|
where n.nspname = 'agent_board' and p.proname = 'resolve_board_request'),
|
|
395
428
|
'from agent_board\.board_memberships[[:space:]]+bm[[:space:]]+where bm\.board_id = p_board_id[[:space:]]+and bm\.agent_id = p_agent_id[[:space:]]+order by bm\.id[[:space:]]+for update;',
|
|
@@ -399,14 +432,14 @@ select matches(
|
|
|
399
432
|
-- a lock taken after the check protects nothing.
|
|
400
433
|
select ok(
|
|
401
434
|
(select position('for update' in body) < position('_membership_consent_present' in body)
|
|
402
|
-
from (select
|
|
435
|
+
from (select agent_board_test.stripped_body(p.oid) as body
|
|
403
436
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
404
437
|
where n.nspname = 'agent_board' and p.proname = 'resolve_board_request') s),
|
|
405
438
|
'LOCK: ...and reads consent AFTER taking it, not before');
|
|
406
439
|
|
|
407
440
|
-- The sink acts on the row the CALLER named, rather than reselecting (#109).
|
|
408
441
|
select matches(
|
|
409
|
-
(select
|
|
442
|
+
(select agent_board_test.stripped_body(p.oid)
|
|
410
443
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
411
444
|
where n.nspname = 'agent_board' and p.proname = '_upsert_board_membership_state'),
|
|
412
445
|
'if p_membership_id is not null then',
|
|
@@ -485,5 +518,16 @@ select is(
|
|
|
485
518
|
select is((select version from agent_board.schema_version), 8,
|
|
486
519
|
'schema_version stays 8 -- additive');
|
|
487
520
|
|
|
521
|
+
|
|
522
|
+
-- The schema-qualified call anchor above treats `agent_board._membership_consent_present` as an
|
|
523
|
+
-- IDENTITY. That is only true while the name resolves to exactly one function:
|
|
524
|
+
-- a second overload would be a different implementation the same anchor accepts.
|
|
525
|
+
-- Catalog-derived, so a new overload fails here and the count names itself.
|
|
526
|
+
select is(
|
|
527
|
+
(select count(*)::int from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
528
|
+
where n.nspname = 'agent_board' and p.proname = '_membership_consent_present'),
|
|
529
|
+
1,
|
|
530
|
+
'agent_board._membership_consent_present has exactly ONE overload, so the schema-qualified call anchor identifies it uniquely');
|
|
531
|
+
|
|
488
532
|
select finish();
|
|
489
533
|
rollback;
|
|
@@ -94,7 +94,9 @@ select is(
|
|
|
94
94
|
false, 'over-ceiling grant is rejected (owner cannot grant admin when ceiling is member)');
|
|
95
95
|
|
|
96
96
|
-- #13 owner-settable ceiling: set_board_authority_ceiling is owner-gated. ---------
|
|
97
|
-
|
|
97
|
+
-- #247 added the delegable p_actor_agent_id (null default => the pre-#247 path for every
|
|
98
|
+
-- role), so the identity signature is (uuid, text, uuid).
|
|
99
|
+
select has_function('agent_board', 'set_board_authority_ceiling', array['uuid','text','uuid'],
|
|
98
100
|
'set_board_authority_ceiling exists');
|
|
99
101
|
|
|
100
102
|
set local role authenticated;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
-- Consiliency/message-board#176 pgTAP: OWNER-SCOPED ENROLL RESUMPTION.
|
|
2
|
+
--
|
|
3
|
+
-- The live failure: /enroll committed an identity, post-enroll provisioning failed, the
|
|
4
|
+
-- client reported total failure, and every retry was then refused forever with
|
|
5
|
+
-- `23505 this signing key is already enrolled`. 20260901000100's replay wrapper resumes
|
|
6
|
+
-- only rows carrying its own `metadata.federation.replay` STAMP -- which the trapped
|
|
7
|
+
-- enrollments, created before it shipped, do not have. 20260906000100 decides resumption
|
|
8
|
+
-- from evidence written at the ORIGINAL enrollment instead, so an unstamped row resumes
|
|
9
|
+
-- too.
|
|
10
|
+
--
|
|
11
|
+
-- Both directions are pinned here, and the SECURITY direction is the one that matters:
|
|
12
|
+
-- * same verified (issuer, subject) -> the existing identity is RETURNED (R-1, R-2);
|
|
13
|
+
-- * a DIFFERENT principal -> still raises, and the message names no handle (R-3, R-4).
|
|
14
|
+
-- Every allow-side assertion is `lives_ok(...)` followed by a separate state check, never
|
|
15
|
+
-- a bare `is((select ... from rpc(...)), ...)`, which aborts instead of failing (#113).
|
|
16
|
+
--
|
|
17
|
+
-- Failing calls are executed through pg_temp.try(), a BEGIN/EXCEPTION subtransaction that
|
|
18
|
+
-- records (sqlstate, message) into `_err` and records an explicit '00000 / (no error
|
|
19
|
+
-- raised)' row when the call SUCCEEDS -- so "the raise disappeared" fails an assertion
|
|
20
|
+
-- rather than silently matching nothing.
|
|
21
|
+
--
|
|
22
|
+
-- EC-1/EC-2 are the class assertions: the route class is enumerated from pg_proc, not
|
|
23
|
+
-- from a list written here, so a new fingerprint-accepting route or a new signing-key
|
|
24
|
+
-- creator fails and NAMES ITSELF.
|
|
25
|
+
create extension if not exists pgtap with schema extensions;
|
|
26
|
+
set search_path = extensions, public, agent_board;
|
|
27
|
+
|
|
28
|
+
begin;
|
|
29
|
+
|
|
30
|
+
select plan(35);
|
|
31
|
+
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
-- Fixtures
|
|
34
|
+
-- ---------------------------------------------------------------------------
|
|
35
|
+
create temp table _v (name text primary key, val text) on commit drop;
|
|
36
|
+
create temp table _e (
|
|
37
|
+
label text, agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
|
|
38
|
+
granted text[], key_id text, key_version int, handle text, invite_redeemed boolean
|
|
39
|
+
) on commit drop;
|
|
40
|
+
create temp table _err (label text primary key, state text, msg text) on commit drop;
|
|
41
|
+
|
|
42
|
+
-- The four resumption RESULTS, declared UP FRONT with the RETURNS TABLE shape rather
|
|
43
|
+
-- than created by `create temp table ... as <the call>`. If the call under test raises,
|
|
44
|
+
-- `lives_ok` records the failure and the CTAS form would leave the table MISSING -- so
|
|
45
|
+
-- every following assertion would abort the transaction instead of failing, and pgTAP
|
|
46
|
+
-- would emit fewer assertions than the plan with no `not ok` to read (#113). Declared
|
|
47
|
+
-- empty, those assertions return NULL and fail honestly.
|
|
48
|
+
create temp table _r1 (
|
|
49
|
+
agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
|
|
50
|
+
granted_scopes text[], key_id text, key_version int, agent_handle text,
|
|
51
|
+
invite_redeemed boolean
|
|
52
|
+
) on commit drop;
|
|
53
|
+
create temp table _r2 (like _r1) on commit drop;
|
|
54
|
+
create temp table _r6 (like _r1) on commit drop;
|
|
55
|
+
create temp table _r13 (like _r1) on commit drop;
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
create function pg_temp.v(p_name text) returns text language sql stable as
|
|
59
|
+
$$ select val from _v where name = p_name $$;
|
|
60
|
+
|
|
61
|
+
-- The exact /enroll call, as SQL text. `p_actor_sub` is null and the kind is 'agent'
|
|
62
|
+
-- throughout, so the only inputs that vary across cases are the ones under test.
|
|
63
|
+
create function pg_temp.enroll_sql(
|
|
64
|
+
p_iss text, p_sub text, p_slug text, p_pub text, p_fp text, p_scopes text default '{board:send}'
|
|
65
|
+
) returns text language sql stable as
|
|
66
|
+
$$
|
|
67
|
+
select format(
|
|
68
|
+
$q$select * from agent_board.enroll_federated_identity(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent',%L,'{}')$q$,
|
|
69
|
+
p_iss, p_sub,
|
|
70
|
+
jsonb_build_object('iss', p_iss, 'aud', 'message-board', 'sub', p_sub)::text,
|
|
71
|
+
p_slug, p_pub, p_fp, p_scopes)
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
-- Run a call that is EXPECTED to raise, capturing sqlstate + message. A call that does
|
|
75
|
+
-- NOT raise records '00000', so a vanished raise fails loudly instead of matching an
|
|
76
|
+
-- absent row.
|
|
77
|
+
create function pg_temp.try(p_label text, p_sql text) returns void language plpgsql as
|
|
78
|
+
$$
|
|
79
|
+
begin
|
|
80
|
+
begin
|
|
81
|
+
execute p_sql;
|
|
82
|
+
insert into _err values (p_label, '00000', '(no error raised)');
|
|
83
|
+
exception when others then
|
|
84
|
+
insert into _err values (p_label, sqlstate, sqlerrm);
|
|
85
|
+
end;
|
|
86
|
+
end
|
|
87
|
+
$$;
|
|
88
|
+
|
|
89
|
+
do $$
|
|
90
|
+
declare
|
|
91
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
92
|
+
v_iss1 text := 'https://res-a-' || replace(substr(gen_random_uuid()::text, 1, 8), '-', '') || '.test';
|
|
93
|
+
v_iss2 text;
|
|
94
|
+
v_pub text;
|
|
95
|
+
i int;
|
|
96
|
+
begin
|
|
97
|
+
v_iss1 := 'https://res-a-' || s || '.test';
|
|
98
|
+
v_iss2 := 'https://res-b-' || s || '.test';
|
|
99
|
+
|
|
100
|
+
insert into agent_board.identity_providers
|
|
101
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
102
|
+
values
|
|
103
|
+
(v_iss1, 'message-board', true, true, '{board:send,board:read}', '{board:x}', '{agent,human}'),
|
|
104
|
+
(v_iss2, 'message-board', true, true, '{board:send,board:read}', '{board:x}', '{agent,human}');
|
|
105
|
+
|
|
106
|
+
insert into _v values
|
|
107
|
+
('s', s), ('iss1', v_iss1), ('iss2', v_iss2),
|
|
108
|
+
('suba', 'portal|a-' || s), ('subb', 'portal|b-' || s);
|
|
109
|
+
|
|
110
|
+
-- Eight real Ed25519-shaped keys: 32 random bytes re-encoded through the SAME
|
|
111
|
+
-- canonical base64url form the enroll contract requires, and their in-DB fingerprints.
|
|
112
|
+
for i in 1..8 loop
|
|
113
|
+
v_pub := rtrim(translate(encode(gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
|
|
114
|
+
insert into _v values ('pub' || i, v_pub), ('fp' || i, agent_board._raw_key_fingerprint(v_pub));
|
|
115
|
+
end loop;
|
|
116
|
+
end
|
|
117
|
+
$$;
|
|
118
|
+
|
|
119
|
+
-- Eight enrollments. E1/E3..E8 are then stripped of their replay stamp, which is exactly
|
|
120
|
+
-- the state every identity enrolled before 20260901000100 is in -- the #176 victim's
|
|
121
|
+
-- state. E2 keeps its stamp.
|
|
122
|
+
do $$
|
|
123
|
+
declare
|
|
124
|
+
i int;
|
|
125
|
+
v_slug text;
|
|
126
|
+
begin
|
|
127
|
+
for i in 1..8 loop
|
|
128
|
+
v_slug := 'res-' || i || '-' || pg_temp.v('s');
|
|
129
|
+
-- E2 is the STAMPED fixture and the only one that needs two scopes: the frozen
|
|
130
|
+
-- contract compares them as JSONB, which preserves ORDER, and R-14 pins that.
|
|
131
|
+
execute format('insert into _e select %L, * from (%s) t', 'E' || i,
|
|
132
|
+
pg_temp.enroll_sql(pg_temp.v('iss1'), pg_temp.v('suba'), v_slug,
|
|
133
|
+
pg_temp.v('pub' || i), pg_temp.v('fp' || i),
|
|
134
|
+
case when i = 2 then '{board:send,board:read}' else '{board:send}' end));
|
|
135
|
+
end loop;
|
|
136
|
+
|
|
137
|
+
update agent_board.agents a
|
|
138
|
+
set metadata = a.metadata #- '{federation,replay}'
|
|
139
|
+
where a.id in (select agent_id from _e where label <> 'E2');
|
|
140
|
+
|
|
141
|
+
-- E3: owned, unstamped, but the agent is revoked -> must NOT be revived by resumption.
|
|
142
|
+
update agent_board.agents a set status = 'revoked' where a.id = (select agent_id from _e where label = 'E3');
|
|
143
|
+
|
|
144
|
+
-- E4: evidence (i) broken -- owner_user_id repointed at an unrelated auth user.
|
|
145
|
+
update agent_board.agents a
|
|
146
|
+
set owner_user_id = agent_board._ensure_agent_auth_user('res-unrelated-owner-' || pg_temp.v('s'))
|
|
147
|
+
where a.id = (select agent_id from _e where label = 'E4');
|
|
148
|
+
|
|
149
|
+
-- E5: evidence (ii) broken -- the creation-time federation subject is gone.
|
|
150
|
+
update agent_board.agents a
|
|
151
|
+
set metadata = a.metadata #- '{federation,subject}'
|
|
152
|
+
where a.id = (select agent_id from _e where label = 'E5');
|
|
153
|
+
|
|
154
|
+
-- E6: evidence (iii) broken -- the original enrollment's audit row is gone.
|
|
155
|
+
delete from agent_board.enrollment_federation_audit e
|
|
156
|
+
where e.agent_id = (select agent_id from _e where label = 'E6');
|
|
157
|
+
|
|
158
|
+
-- E7: audit row present but recording no granted scopes -> ownership holds, the
|
|
159
|
+
-- identity is not resumable. This is what makes branch (b) reachable rather than
|
|
160
|
+
-- implied by the ownership conjunction.
|
|
161
|
+
update agent_board.enrollment_federation_audit e
|
|
162
|
+
set granted_scopes = null
|
|
163
|
+
where e.agent_id = (select agent_id from _e where label = 'E7');
|
|
164
|
+
|
|
165
|
+
-- E8: the ONE exemption in the ownership conjunction, modelled on the atomic-fleet
|
|
166
|
+
-- backfill fixture -- an agent that predates the owner model and carries NO
|
|
167
|
+
-- owner_user_id. Evidence (i) is unavailable there rather than contradicted.
|
|
168
|
+
update agent_board.agents a set owner_user_id = null
|
|
169
|
+
where a.id = (select agent_id from _e where label = 'E8');
|
|
170
|
+
end
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
-- ---------------------------------------------------------------------------
|
|
174
|
+
-- EC-1/EC-2 -- the class, enumerated from the catalog
|
|
175
|
+
-- ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
-- Every route a client can re-present an enrolled key to, taken from pg_proc.proargnames.
|
|
178
|
+
-- A fourth such route added later fails this and names itself in the diff.
|
|
179
|
+
select is(
|
|
180
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
181
|
+
from pg_proc p
|
|
182
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
183
|
+
where n.nspname = 'agent_board'
|
|
184
|
+
and p.proargnames && array['p_agent_public_key_fingerprint']),
|
|
185
|
+
'_enroll_federated_identity_once, enroll_federated_identity, onboard_federated_fleet_agent',
|
|
186
|
+
'EC-1: exactly three agent_board routes accept an enrolled key fingerprint (pg_proc-derived)');
|
|
187
|
+
|
|
188
|
+
-- ...and only two functions in the whole schema can CREATE a signing key, so
|
|
189
|
+
-- onboard_federated_fleet_agent necessarily reaches an already-enrolled key through
|
|
190
|
+
-- enroll_federated_identity rather than deciding the duplicate case itself.
|
|
191
|
+
select is(
|
|
192
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
193
|
+
from pg_proc p
|
|
194
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
195
|
+
where n.nspname = 'agent_board'
|
|
196
|
+
and p.prokind = 'f'
|
|
197
|
+
and agent_board_test.strip_sql(p.prosrc) ~* 'insert into agent_board\.agent_signing_keys'),
|
|
198
|
+
'_enroll_federated_identity_once, mint_delegated_child',
|
|
199
|
+
'EC-2: only the enroll creator and delegated-child mint insert signing keys (strip_sql-derived)');
|
|
200
|
+
|
|
201
|
+
-- ---------------------------------------------------------------------------
|
|
202
|
+
-- R-1 -- THE #176 FIX: an unstamped enrollment resumes for its owner
|
|
203
|
+
-- ---------------------------------------------------------------------------
|
|
204
|
+
select lives_ok(
|
|
205
|
+
(select 'insert into _r1 ' || pg_temp.enroll_sql(
|
|
206
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-1-' || pg_temp.v('s'),
|
|
207
|
+
pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')),
|
|
208
|
+
'R-1: re-presenting an already-enrolled key as the same verified (issuer, subject) SUCCEEDS (unstamped row)');
|
|
209
|
+
select is(
|
|
210
|
+
(select agent_id from _r1),
|
|
211
|
+
(select agent_id from _e where label = 'E1'),
|
|
212
|
+
'R-1: the resumption returns the EXISTING agent identity');
|
|
213
|
+
select is(
|
|
214
|
+
(select signing_key_id from _r1),
|
|
215
|
+
(select signing_key_id from _e where label = 'E1'),
|
|
216
|
+
'R-1: the resumption returns the EXISTING signing key, not a new one');
|
|
217
|
+
select is(
|
|
218
|
+
(select agent_handle from _r1),
|
|
219
|
+
(select handle from _e where label = 'E1'),
|
|
220
|
+
'R-1: the resumption returns the server-assigned handle the client lost');
|
|
221
|
+
select is(
|
|
222
|
+
(select provider_id from _r1),
|
|
223
|
+
(select provider_id from _e where label = 'E1'),
|
|
224
|
+
'R-1: provider_id survives the unstamped path (it is read from the creation-time federation object)');
|
|
225
|
+
select is(
|
|
226
|
+
(select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = pg_temp.v('fp1')),
|
|
227
|
+
1, 'R-1: the resumption enrolled no second key for that fingerprint');
|
|
228
|
+
select is(
|
|
229
|
+
(select count(*)::int from agent_board.agents a where a.id in (select agent_id from _e) or a.handle like 'res-1-%'),
|
|
230
|
+
8, 'R-1: the resumption created no ninth agent');
|
|
231
|
+
|
|
232
|
+
-- ---------------------------------------------------------------------------
|
|
233
|
+
-- R-2 -- an unstamped resumption cannot ESCALATE
|
|
234
|
+
-- ---------------------------------------------------------------------------
|
|
235
|
+
select lives_ok(
|
|
236
|
+
(select 'insert into _r2 ' || pg_temp.enroll_sql(
|
|
237
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-1-different-slug',
|
|
238
|
+
pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send,board:read}')),
|
|
239
|
+
'R-2: an unstamped resumption tolerates a different slug and a wider scope request');
|
|
240
|
+
select is(
|
|
241
|
+
(select granted_scopes from _r2),
|
|
242
|
+
'{board:send}'::text[],
|
|
243
|
+
'R-2: it returns the ORIGINAL granted scopes -- the wider request grants nothing');
|
|
244
|
+
select is(
|
|
245
|
+
(select agent_id from _r2),
|
|
246
|
+
(select agent_id from _e where label = 'E1'),
|
|
247
|
+
'R-2: and it still returns the original identity, not a second one');
|
|
248
|
+
|
|
249
|
+
-- ---------------------------------------------------------------------------
|
|
250
|
+
-- R-3 / R-4 -- THE SECURITY DIRECTION: a different principal is still refused
|
|
251
|
+
-- ---------------------------------------------------------------------------
|
|
252
|
+
select pg_temp.try('squat-subject', (select pg_temp.enroll_sql(
|
|
253
|
+
pg_temp.v('iss1'), pg_temp.v('subb'), 'res-squat-' || pg_temp.v('s'),
|
|
254
|
+
pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')));
|
|
255
|
+
-- sqlstate ALONE does not pin this: with the ownership refusal bypassed the request
|
|
256
|
+
-- falls through to the state gate, which raises the SAME 23505 for its own reason. The
|
|
257
|
+
-- assertion therefore requires the OWNERSHIP refusal specifically.
|
|
258
|
+
select ok(
|
|
259
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
260
|
+
from _err where label = 'squat-subject'),
|
|
261
|
+
'R-3: a key owned by a DIFFERENT subject at the same issuer is refused AS a different principal (23505)');
|
|
262
|
+
select ok(
|
|
263
|
+
(select position(h in msg) = 0 from _err, (select handle h from _e where label = 'E1') x where label = 'squat-subject'),
|
|
264
|
+
'R-3: the refusal names NO handle -- a squatter learns nothing about who holds the key');
|
|
265
|
+
select is(
|
|
266
|
+
(select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = pg_temp.v('fp1')),
|
|
267
|
+
1, 'R-3: the refused squat left exactly one key on that fingerprint');
|
|
268
|
+
|
|
269
|
+
select pg_temp.try('squat-issuer', (select pg_temp.enroll_sql(
|
|
270
|
+
pg_temp.v('iss2'), pg_temp.v('suba'), 'res-squat2-' || pg_temp.v('s'),
|
|
271
|
+
pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')));
|
|
272
|
+
select ok(
|
|
273
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
274
|
+
from _err where label = 'squat-issuer'),
|
|
275
|
+
'R-4: the same subject at a DIFFERENT approved issuer is refused as a different principal');
|
|
276
|
+
select ok(
|
|
277
|
+
(select position(h in msg) = 0 from _err, (select handle h from _e where label = 'E1') x where label = 'squat-issuer'),
|
|
278
|
+
'R-4: that refusal names no handle either');
|
|
279
|
+
|
|
280
|
+
-- ---------------------------------------------------------------------------
|
|
281
|
+
-- R-5 / R-6 -- a STAMPED enrollment keeps its frozen contract; only the message improves
|
|
282
|
+
-- ---------------------------------------------------------------------------
|
|
283
|
+
select pg_temp.try('frozen', (select pg_temp.enroll_sql(
|
|
284
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-renamed',
|
|
285
|
+
pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:send,board:read}')));
|
|
286
|
+
select is((select state from _err where label = 'frozen'), '23505',
|
|
287
|
+
'R-5: a stamped enrollment re-presented by its owner with a DIFFERENT slug still raises (frozen contract intact)');
|
|
288
|
+
select ok(
|
|
289
|
+
(select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E2') x where label = 'frozen'),
|
|
290
|
+
'R-5: but the message now NAMES the existing handle');
|
|
291
|
+
select ok(
|
|
292
|
+
(select msg like '%need not be repeated%' from _err where label = 'frozen'),
|
|
293
|
+
'R-5: and says enrollment need not be repeated -- the actionable half of the ask');
|
|
294
|
+
|
|
295
|
+
select lives_ok(
|
|
296
|
+
(select 'insert into _r6 ' || pg_temp.enroll_sql(
|
|
297
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-' || pg_temp.v('s'),
|
|
298
|
+
pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:send,board:read}')),
|
|
299
|
+
'R-6: an EXACT stamped replay still succeeds (the released path is unregressed)');
|
|
300
|
+
select is(
|
|
301
|
+
(select agent_id from _r6),
|
|
302
|
+
(select agent_id from _e where label = 'E2'),
|
|
303
|
+
'R-6: and still returns the original identity');
|
|
304
|
+
|
|
305
|
+
-- ---------------------------------------------------------------------------
|
|
306
|
+
-- R-7 -- resumption never revives an identity that is not usable
|
|
307
|
+
-- ---------------------------------------------------------------------------
|
|
308
|
+
select pg_temp.try('revoked', (select pg_temp.enroll_sql(
|
|
309
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-3-' || pg_temp.v('s'),
|
|
310
|
+
pg_temp.v('pub3'), pg_temp.v('fp3'), '{board:send}')));
|
|
311
|
+
select is((select state from _err where label = 'revoked'), '23505',
|
|
312
|
+
'R-7: an owned but REVOKED identity is not resumable');
|
|
313
|
+
select ok(
|
|
314
|
+
(select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E3') x where label = 'revoked'),
|
|
315
|
+
'R-7: its owner is told which handle is stuck');
|
|
316
|
+
|
|
317
|
+
-- ---------------------------------------------------------------------------
|
|
318
|
+
-- R-8/R-9/R-10 -- each of the three ownership evidences is load-bearing
|
|
319
|
+
-- ---------------------------------------------------------------------------
|
|
320
|
+
select pg_temp.try('no-owner-match', (select pg_temp.enroll_sql(
|
|
321
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-4-' || pg_temp.v('s'),
|
|
322
|
+
pg_temp.v('pub4'), pg_temp.v('fp4'), '{board:send}')));
|
|
323
|
+
select ok(
|
|
324
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
325
|
+
from _err where label = 'no-owner-match'),
|
|
326
|
+
'R-8: evidence (i) -- owner_user_id not equal to the derived owner reads as NOT owned');
|
|
327
|
+
|
|
328
|
+
select pg_temp.try('no-creation-stamp', (select pg_temp.enroll_sql(
|
|
329
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-5-' || pg_temp.v('s'),
|
|
330
|
+
pg_temp.v('pub5'), pg_temp.v('fp5'), '{board:send}')));
|
|
331
|
+
select ok(
|
|
332
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
333
|
+
from _err where label = 'no-creation-stamp'),
|
|
334
|
+
'R-9: evidence (ii) -- a missing creation-time federation subject reads as NOT owned (null is not a pass)');
|
|
335
|
+
|
|
336
|
+
select pg_temp.try('no-audit-row', (select pg_temp.enroll_sql(
|
|
337
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-6-' || pg_temp.v('s'),
|
|
338
|
+
pg_temp.v('pub6'), pg_temp.v('fp6'), '{board:send}')));
|
|
339
|
+
select ok(
|
|
340
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
341
|
+
from _err where label = 'no-audit-row'),
|
|
342
|
+
'R-10: evidence (iii) -- a missing enrollment audit row reads as NOT owned');
|
|
343
|
+
|
|
344
|
+
-- ---------------------------------------------------------------------------
|
|
345
|
+
-- R-11 -- branch (b) is reachable on its own, not merely implied by ownership
|
|
346
|
+
-- ---------------------------------------------------------------------------
|
|
347
|
+
select pg_temp.try('no-granted-scopes', (select pg_temp.enroll_sql(
|
|
348
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-7-' || pg_temp.v('s'),
|
|
349
|
+
pg_temp.v('pub7'), pg_temp.v('fp7'), '{board:send}')));
|
|
350
|
+
select is((select state from _err where label = 'no-granted-scopes'), '23505',
|
|
351
|
+
'R-11: an audit row recording NO granted scopes is owned but not resumable');
|
|
352
|
+
select ok(
|
|
353
|
+
(select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E7') x where label = 'no-granted-scopes'),
|
|
354
|
+
'R-11: and because ownership IS proven there, the owner is told the handle');
|
|
355
|
+
|
|
356
|
+
-- ---------------------------------------------------------------------------
|
|
357
|
+
-- R-12 -- the internal creator stays a hard refusal, but an actionable one
|
|
358
|
+
-- ---------------------------------------------------------------------------
|
|
359
|
+
select pg_temp.try('once-direct', (select format(
|
|
360
|
+
$q$select * from agent_board._enroll_federated_identity_once(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
361
|
+
pg_temp.v('iss1'), pg_temp.v('suba'),
|
|
362
|
+
jsonb_build_object('iss', pg_temp.v('iss1'), 'aud', 'message-board', 'sub', pg_temp.v('suba'))::text,
|
|
363
|
+
'res-once-' || pg_temp.v('s'), pg_temp.v('pub1'), pg_temp.v('fp1'))));
|
|
364
|
+
select is((select state from _err where label = 'once-direct'), '23505',
|
|
365
|
+
'R-12: _enroll_federated_identity_once still refuses an already-enrolled key outright (no twin resumption)');
|
|
366
|
+
select ok(
|
|
367
|
+
(select position(h in msg) > 0 and msg like '%need not be repeated%'
|
|
368
|
+
from _err, (select handle h from _e where label = 'E1') x where label = 'once-direct'),
|
|
369
|
+
'R-12: and its refusal now names the holder and says enrollment need not be repeated');
|
|
370
|
+
|
|
371
|
+
-- ---------------------------------------------------------------------------
|
|
372
|
+
-- R-13 -- the null-owner exemption, asserted POSITIVELY and in BOTH directions
|
|
373
|
+
-- ---------------------------------------------------------------------------
|
|
374
|
+
select lives_ok(
|
|
375
|
+
(select 'insert into _r13 ' || pg_temp.enroll_sql(
|
|
376
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-8-' || pg_temp.v('s'),
|
|
377
|
+
pg_temp.v('pub8'), pg_temp.v('fp8'), '{board:send}')),
|
|
378
|
+
'R-13: a row with NO owner_user_id (pre-owner-model, the atomic-fleet backfill shape) still resumes for its owner');
|
|
379
|
+
select is(
|
|
380
|
+
(select agent_id from _r13),
|
|
381
|
+
(select agent_id from _e where label = 'E8'),
|
|
382
|
+
'R-13: and it resumes to that same pre-owner-model identity');
|
|
383
|
+
select pg_temp.try('null-owner-squat', (select pg_temp.enroll_sql(
|
|
384
|
+
pg_temp.v('iss1'), pg_temp.v('subb'), 'res-8-squat-' || pg_temp.v('s'),
|
|
385
|
+
pg_temp.v('pub8'), pg_temp.v('fp8'), '{board:send}')));
|
|
386
|
+
select ok(
|
|
387
|
+
(select state = '23505' and msg like '%to a different principal%'
|
|
388
|
+
from _err where label = 'null-owner-squat'),
|
|
389
|
+
'R-13: the exemption does NOT open a squat -- the same null-owner row refuses a different subject as one');
|
|
390
|
+
|
|
391
|
+
-- ---------------------------------------------------------------------------
|
|
392
|
+
-- R-14 -- the frozen scope comparison is ORDER-SENSITIVE, and that is asserted
|
|
393
|
+
-- rather than assumed
|
|
394
|
+
-- ---------------------------------------------------------------------------
|
|
395
|
+
-- `v_replay -> 'requestedScopes' is distinct from to_jsonb(v_requested_scopes)` is a
|
|
396
|
+
-- JSONB comparison and a jsonb array preserves order, so the SAME scopes in a different
|
|
397
|
+
-- order are a DIFFERENT request. That is pre-existing released behaviour of the frozen
|
|
398
|
+
-- contract (20260901000100), not something #176 introduced, and this migration
|
|
399
|
+
-- deliberately does not loosen it. It is pinned here because the offline MockEnroll
|
|
400
|
+
-- sorted its scope arrays and so disagreed with production silently; the client now
|
|
401
|
+
-- asserts the same property, and neither side can drift without one of them going red.
|
|
402
|
+
select pg_temp.try('frozen-scope-order', (select pg_temp.enroll_sql(
|
|
403
|
+
pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-' || pg_temp.v('s'),
|
|
404
|
+
pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:read,board:send}')));
|
|
405
|
+
select ok(
|
|
406
|
+
(select state = '23505' and position(h in msg) > 0
|
|
407
|
+
from _err, (select handle h from _e where label = 'E2') x
|
|
408
|
+
where label = 'frozen-scope-order'),
|
|
409
|
+
'R-14: the SAME frozen scopes REORDERED are a different frozen contract and raise, naming the handle');
|
|
410
|
+
|
|
411
|
+
select * from finish();
|
|
412
|
+
rollback;
|