@consiliency/agent-board-schema 1.59.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +80 -0
  2. package/fixtures/already-applied.json +16 -0
  3. package/migrations/20261010000100_agent_board_own_post_not_delivered.sql +637 -0
  4. package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
  5. package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
  6. package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
  7. package/package.json +1 -1
  8. package/schema-manifest.json +39 -3
  9. package/src/tooling.test.mjs +9 -2
  10. package/tests/agent_board_addressing.test.sql +3 -1
  11. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -4
  12. package/tests/agent_board_consent_emission.test.sql +30 -11
  13. package/tests/agent_board_consent_gating.test.sql +16 -2
  14. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  15. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  16. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  18. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  19. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  20. package/tests/agent_board_fk_lock_edges.test.sql +74 -14
  21. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  22. package/tests/agent_board_inbox_rpcs.test.sql +5 -2
  23. package/tests/agent_board_invite_no_downgrade.test.sql +10 -8
  24. package/tests/agent_board_ledger_authority.test.sql +70 -12
  25. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  26. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  27. package/tests/agent_board_mint_harden.test.sql +38 -27
  28. package/tests/agent_board_own_post_not_delivered.test.sql +490 -0
  29. package/tests/agent_board_rolereset.test.sql +8 -2
  30. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  31. package/tests/agent_board_rowid.test.sql +3 -1
  32. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  33. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  34. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  35. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  36. package/tests/agent_board_session_identity_class.test.sql +118 -0
  37. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  38. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  39. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  40. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  41. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  42. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  43. package/tests/agent_board_session_identity_link.test.sql +201 -0
  44. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  45. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  46. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  47. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  48. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  49. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  50. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  51. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  52. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  53. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  54. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  55. package/tests/agent_board_v2_invariants.test.sql +4 -4
  56. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  57. package/tests/catalog/function_execute_privileges.json +218 -2
  58. package/tests/catalog/p_metadata_parameters.json +98 -2
  59. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  60. package/tests/isolation/expected/session_identity_limits.out +165 -0
  61. package/tests/isolation/expected/session_identity_links.out +235 -0
  62. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  63. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  64. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  65. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  66. package/tests/isolation/specs/grant_target_role_race.spec +207 -0
  67. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  68. package/tests/isolation/specs/session_identity_links.spec +80 -0
  69. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  70. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  71. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  72. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +108 -0
  73. package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
@@ -0,0 +1,118 @@
1
+ -- SIDSCHEMA catalog contract: these assertions enumerate the installed class
2
+ -- from pg_catalog so a newly reachable identity writer names itself.
3
+ create extension if not exists pgtap with schema extensions;
4
+ set search_path = extensions, public, agent_board;
5
+
6
+ begin;
7
+
8
+ select plan(21);
9
+
10
+ select has_table('agent_board', 'session_identities', 'EC-SIDSCHEMA-1: identities table exists');
11
+ select has_table('agent_board', 'session_id_aliases', 'EC-SIDSCHEMA-1: aliases table exists');
12
+ select ok((select relrowsecurity from pg_class where oid = 'agent_board.session_identities'::regclass),
13
+ 'EC-SIDSCHEMA-1: identity rows have RLS enabled');
14
+ select ok((select relrowsecurity from pg_class where oid = 'agent_board.session_id_aliases'::regclass),
15
+ 'EC-SIDSCHEMA-1: alias rows have RLS enabled');
16
+ select has_column('agent_board', 'session_identities', 'credential_epoch',
17
+ 'EC-SIDSCHEMA-1: identity lease epoch is stored');
18
+ select has_column('agent_board', 'session_identities', 'holder_secret_hash',
19
+ 'EC-SIDSCHEMA-1: only the holder-secret hash is stored');
20
+
21
+ select is(
22
+ (select count(*) from pg_constraint c
23
+ where c.conrelid = 'agent_board.session_id_aliases'::regclass
24
+ and c.contype = 'u'
25
+ and pg_get_constraintdef(c.oid) like '%root_agent_id, harness, session_id%'),
26
+ 1::bigint,
27
+ 'EC-SIDSCHEMA-1: alias uniqueness covers root, harness and session id');
28
+
29
+ select is(has_table_privilege('authenticated', 'agent_board.session_identities', 'SELECT'), false,
30
+ 'EC-SIDSCHEMA-1: authenticated cannot read identity secrets');
31
+ select is(has_table_privilege('authenticated', 'agent_board.session_id_aliases', 'INSERT'), false,
32
+ 'EC-SIDSCHEMA-1: authenticated cannot write aliases directly');
33
+
34
+ select is(has_function_privilege('service_role',
35
+ 'agent_board.link_session_alias(uuid,text,text)', 'EXECUTE'), true,
36
+ 'EC-SIDSCHEMA-1: service_role may invoke the verified alias linker');
37
+ select is(has_function_privilege('authenticated',
38
+ 'agent_board.link_session_alias(uuid,text,text)', 'EXECUTE'), false,
39
+ 'EC-SIDSCHEMA-1: authenticated cannot invoke the asserted-id alias linker');
40
+ select is(has_function_privilege('public',
41
+ 'agent_board.link_session_alias(uuid,text,text)', 'EXECUTE'), false,
42
+ 'EC-SIDSCHEMA-1: PUBLIC cannot invoke the asserted-id alias linker');
43
+
44
+ select ok(
45
+ (select agent_board_test.stripped_body('agent_board.current_agent_id()'::regprocedure, true)
46
+ like '%request.jwt.claim.identity_lease_epoch%'
47
+ and agent_board_test.stripped_body('agent_board.current_agent_id()'::regprocedure, true)
48
+ like '%request.jwt.claims%'),
49
+ 'EC-SIDSCHEMA-1: resolver reads the lease epoch from both JWT GUC forms');
50
+
51
+ select ok(
52
+ (select exists (
53
+ select 1 from pg_trigger t
54
+ where t.tgrelid = 'agent_board.session_identities'::regclass
55
+ and not t.tgisinternal
56
+ and pg_get_triggerdef(t.oid) like '%identity_kind%immutable%')),
57
+ 'EC-SIDSCHEMA-1: session identity kind has an immutable trigger');
58
+
59
+ select is(
60
+ (select coalesce(string_agg(p.oid::regprocedure::text, ', ' order by p.oid::regprocedure::text), '')
61
+ from pg_proc p
62
+ join pg_namespace n on n.oid = p.pronamespace
63
+ where n.nspname = 'agent_board'
64
+ and agent_board_test.stripped_body(p.oid, true) like '%session_identities%'
65
+ and (has_function_privilege('public', p.oid, 'EXECUTE')
66
+ or has_function_privilege('anon', p.oid, 'EXECUTE'))),
67
+ '',
68
+ 'EC-SIDSCHEMA-1: catalog-derived identity-writer class exposes no public or anon entrypoint');
69
+
70
+ -- Include every catalog-discovered identity/alias user and every named caller,
71
+ -- not merely the new RPC names. Dynamic SQL and variable callees are outside
72
+ -- fn_call_closure's documented literal-SQL model.
73
+ create temp view _sid_catalog_direct as
74
+ select p.oid, p.proname from pg_proc p
75
+ where p.pronamespace = 'agent_board'::regnamespace
76
+ and (agent_board_test.stripped_body(p.oid) ~ agent_board_test.re(
77
+ agent_board_test.qname_frag('session_identities', agent_board_test.bare_ok(p.oid)) || '|'
78
+ || agent_board_test.qname_frag('session_id_aliases', agent_board_test.bare_ok(p.oid)))
79
+ or p.oid in (select t.tgfoid from pg_trigger t where not t.tgisinternal
80
+ and t.tgrelid in ('agent_board.session_identities'::regclass, 'agent_board.session_id_aliases'::regclass)));
81
+ create temp view _sid_catalog_class as
82
+ select distinct p.oid, p.proname
83
+ from pg_proc p join agent_board_test.fn_call_closure() c on c.root_oid = p.oid
84
+ join _sid_catalog_direct d on d.oid = c.reached_oid;
85
+ select ok((select count(*) >= 12 from _sid_catalog_class),
86
+ 'EC-SIDSCHEMA-6-catalog-nonempty: the derived identity/alias RPC and caller population is not empty');
87
+ select is((select coalesce(string_agg(proname, ', ' order by proname), '') from _sid_catalog_class
88
+ where has_function_privilege('public', oid, 'EXECUTE') or has_function_privilege('anon', oid, 'EXECUTE')), '',
89
+ 'EC-SIDSCHEMA-6-catalog-acl: no discovered identity or alias RPC/caller is PUBLIC or anon executable');
90
+ -- Comments are the direct identity/alias API and trigger contract, not a new
91
+ -- documentation requirement on all historical callers of current_agent_id().
92
+ select is((select coalesce(string_agg(proname, ', ' order by proname), '') from _sid_catalog_direct
93
+ where nullif(obj_description(oid, 'pg_proc'), '') is null), '',
94
+ 'EC-SIDSCHEMA-6-catalog-comment: every discovered direct identity or alias RPC/trigger has a catalog contract comment');
95
+ select ok(has_function_privilege('service_role', 'agent_board.renew_session_identity(uuid,text)', 'EXECUTE')
96
+ and not has_function_privilege('authenticated', 'agent_board.renew_session_identity(uuid,text)', 'EXECUTE'),
97
+ 'EC-SIDSCHEMA-6-renew-service: fresh caller-asserted renewal attestation is service-role-only');
98
+ create function agent_board.sid_catalog_unsafe_probe(p_identity uuid)
99
+ returns void language sql security definer set search_path = '' as $probe$
100
+ UPDATE agent_board.session_identities SET resolution_state = 'active' WHERE agent_id = p_identity;
101
+ $probe$;
102
+ create function agent_board.sid_catalog_wrapper_probe(p_identity uuid)
103
+ returns void language sql security definer set search_path = '' as $probe$
104
+ SELECT agent_board.sid_catalog_unsafe_probe(/* commented wrapper */ p_identity);
105
+ $probe$;
106
+ select is((select string_agg(proname, ', ' order by proname) from _sid_catalog_class
107
+ where proname in ('sid_catalog_unsafe_probe', 'sid_catalog_wrapper_probe')
108
+ and has_function_privilege('public', oid, 'EXECUTE')),
109
+ 'sid_catalog_unsafe_probe, sid_catalog_wrapper_probe',
110
+ 'EC-SIDSCHEMA-6-catalog-acl-canary: new uppercase writer and commented wrapper expose themselves by their actual grants');
111
+ select is((select string_agg(proname, ', ' order by proname) from _sid_catalog_class
112
+ where proname in ('sid_catalog_unsafe_probe', 'sid_catalog_wrapper_probe')
113
+ and obj_description(oid, 'pg_proc') is null),
114
+ 'sid_catalog_unsafe_probe, sid_catalog_wrapper_probe',
115
+ 'EC-SIDSCHEMA-6-catalog-comment-canary: each uncommented new member names itself');
116
+
117
+ select finish();
118
+ rollback;
@@ -0,0 +1,271 @@
1
+ -- Fresh readings are recorded through trusted service wiring; approval actors
2
+ -- still come from the forwarded verified subject and epoch, never an actor id.
3
+ create extension if not exists pgtap with schema extensions;
4
+ set search_path = extensions, public, agent_board;
5
+ begin;
6
+ grant usage on schema extensions to authenticated, service_role;
7
+ select plan(60);
8
+ create temp table _sid_fp_root(root_id uuid, root_auth uuid, owner_auth uuid, suffix text);
9
+ create temp table _sid_fp_children(label text primary key, agent_id uuid, key_id uuid, auth_id uuid);
10
+ create temp table _sid_fp_claims(label text primary key, epoch bigint);
11
+ do $$
12
+ declare v_root uuid := gen_random_uuid(); v_owner uuid; v_auth uuid;
13
+ begin
14
+ v_owner := public.ensure_github_user('sid-fp-owner-' || v_root, 'sid-fp-owner-' || v_root || '@example.test', 'sid-fp-owner-' || v_root, 'Fingerprint owner');
15
+ v_auth := public.ensure_github_user('sid-fp-root-' || v_root, 'sid-fp-root-' || v_root || '@example.test', 'sid-fp-root-' || v_root, 'Fingerprint root');
16
+ insert into agent_board.agents(agent_type, handle, display_name, status, auth_user_id)
17
+ values ('human', 'sid-fp-owner-' || v_root, 'Fingerprint owner', 'active', v_owner);
18
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
19
+ values (v_root, 'agent', 'sid-fp-root-' || v_root, 'Fingerprint root', 'active', v_auth, v_owner);
20
+ insert into agent_board.session_identities(agent_id, root_agent_id, depth)
21
+ values (v_root, v_root, 0);
22
+ insert into _sid_fp_root values (v_root, v_auth, v_owner, substr(v_root::text, 1, 8));
23
+ end;
24
+ $$;
25
+ grant select on _sid_fp_root, _sid_fp_children, _sid_fp_claims to authenticated, service_role;
26
+ grant insert on _sid_fp_children, _sid_fp_claims to service_role;
27
+ create function pg_temp.sid_fp_subject(p_label text)
28
+ returns void language plpgsql as $$
29
+ begin
30
+ perform set_config('request.jwt.claim.sub', coalesce((select auth_id::text from _sid_fp_children where label = p_label),
31
+ case when p_label = 'owner' then (select owner_auth::text from _sid_fp_root) else (select root_auth::text from _sid_fp_root) end), true);
32
+ perform set_config('request.jwt.claim.identity_lease_epoch', coalesce((select epoch::text from _sid_fp_claims where label = p_label), ''), true);
33
+ end;
34
+ $$;
35
+ create function pg_temp.sid_fp_mint(p_label text, p_parent text default null)
36
+ returns void language plpgsql as $$
37
+ declare v_parent uuid; v_root record; v_result record; v_challenge record;
38
+ begin
39
+ select * into v_root from _sid_fp_root;
40
+ v_parent := coalesce((select agent_id from _sid_fp_children where label = p_parent), v_root.root_id);
41
+ select * into v_result from agent_board.mint_delegated_child(v_parent, 'sid-fp-' || p_label || '-' || v_root.suffix,
42
+ '["read"]', public.test_pubkey(v_root.suffix || p_label), interval '8 hours', false, 'claude', 'fp-' || p_label,
43
+ (select epoch::text from _sid_fp_claims where label = p_parent), '[]',
44
+ '{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"one"}}');
45
+ insert into _sid_fp_children select p_label, v_result.child_agent_id, v_result.signing_key_id, a.auth_user_id
46
+ from agent_board.agents a where a.id = v_result.child_agent_id;
47
+ select * into v_challenge from agent_board.issue_signing_key_challenge(v_result.child_agent_id, 'delegated-ephemeral');
48
+ insert into _sid_fp_claims select p_label, m.identity_lease_epoch from agent_board.mint_fleet_agent_session(
49
+ v_challenge.challenge_id, split_part(v_challenge.challenge, ':', 3), v_result.signing_key_id, v_result.child_agent_id, 900, null, 'fp-' || p_label) m;
50
+ end;
51
+ $$;
52
+ create function pg_temp.sid_fp_refuse(p_sql text)
53
+ returns void language plpgsql as $$
54
+ begin
55
+ execute p_sql;
56
+ raise exception 'expected refusal but RPC succeeded' using errcode = 'P0001';
57
+ end;
58
+ $$;
59
+ grant execute on function pg_temp.sid_fp_subject(text), pg_temp.sid_fp_refuse(text) to authenticated, service_role;
60
+ grant execute on function pg_temp.sid_fp_mint(text,text) to service_role;
61
+ select ok(has_function_privilege('service_role', 'agent_board.approve_identity_state(uuid,text,jsonb)', 'EXECUTE')
62
+ and not has_function_privilege('authenticated', 'agent_board.approve_identity_state(uuid,text,jsonb)', 'EXECUTE'),
63
+ 'EC-SIDSCHEMA-5-approval-boundary: measured fingerprint inputs are service-only, with the human/ancestor subject preserved');
64
+ select ok(has_function_privilege('service_role', 'agent_board.record_session_identity_state(uuid,jsonb)', 'EXECUTE')
65
+ and not has_function_privilege('authenticated', 'agent_board.record_session_identity_state(uuid,jsonb)', 'EXECUTE'),
66
+ 'EC-SIDSCHEMA-5-reading-boundary: an end caller cannot assert a fresh fingerprint reading');
67
+ select set_config('request.jwt.claims', '{}', true);
68
+ select set_config('request.jwt.claim.role', 'service_role', true);
69
+ set local role service_role;
70
+ select lives_ok($q$select agent_board.upsert_delegation_policy((select root_id from _sid_fp_root), 'root_minted', '["read"]', 200, interval '7 days', true)$q$,
71
+ 'EC-SIDSCHEMA-5-fp-policy: real service policy prepares the fixture');
72
+ reset role;
73
+ select is((select count(*) from agent_board.delegation_policies where root_agent_id = (select root_id from _sid_fp_root)), 1::bigint,
74
+ 'EC-SIDSCHEMA-5-fp-policy-state: one current policy exists');
75
+ set local role service_role;
76
+ select pg_temp.sid_fp_subject('root');
77
+ select lives_ok($q$select agent_board.record_session_identity_state((select root_id from _sid_fp_root), '{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"one"}}')$q$,
78
+ 'EC-SIDSCHEMA-5-root-reading: trusted wiring records the root fresh reading');
79
+ reset role;
80
+ select ok((select approved_risky_digest is null and current_risky_digest = 'approved' and resolution_state = 'frozen'
81
+ from agent_board.session_identities where agent_id = (select root_id from _sid_fp_root)),
82
+ 'EC-SIDSCHEMA-5-root-unapproved: a fresh root reading is never implicit human approval');
83
+ set local role service_role;
84
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select root_id from _sid_fp_root), 'approved', '{"resolution":"resolved"}')$q$),
85
+ '42501', 'identity approval requires the human owner', 'EC-SIDSCHEMA-5-depth-zero-agent: root agent cannot approve its own depth-zero state');
86
+ reset role;
87
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select root_id from _sid_fp_root)), null::text,
88
+ 'EC-SIDSCHEMA-5-depth-zero-state: agent refusal left human approval absent');
89
+ set local role service_role;
90
+ select pg_temp.sid_fp_subject('owner');
91
+ select lives_ok($q$select agent_board.approve_identity_state((select root_id from _sid_fp_root), 'approved', '{"resolution":"resolved"}')$q$,
92
+ 'EC-SIDSCHEMA-5-owner-root-control: verified human owner approves the resolved root');
93
+ reset role;
94
+ select ok((select resolution_state = 'active' and approved_risky_digest = 'approved' from agent_board.session_identities where agent_id = (select root_id from _sid_fp_root)),
95
+ 'EC-SIDSCHEMA-5-owner-root-state: explicit owner approval activates the root');
96
+ set local role service_role;
97
+ select lives_ok($q$select pg_temp.sid_fp_mint('parent'); select pg_temp.sid_fp_mint('sibling'); select pg_temp.sid_fp_mint('child', 'parent')$q$,
98
+ 'EC-SIDSCHEMA-5-fp-fixture: real matching child mints and claims create parent, sibling and depth-two subject');
99
+ reset role;
100
+ select is((select count(*) from agent_board.session_identities si join _sid_fp_children c on c.agent_id = si.agent_id
101
+ where si.approved_risky_digest = 'approved' and si.current_risky_digest = 'approved' and si.resolution_state = 'active'), 3::bigint,
102
+ 'EC-SIDSCHEMA-5-autoapprove-state: matching owner-approved parent digest is inherited by each real child');
103
+ set local role service_role;
104
+ select pg_temp.sid_fp_subject('parent');
105
+ select lives_ok($q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'approved', '{"resolution":"resolved"}')$q$,
106
+ 'EC-SIDSCHEMA-5-parent-control: non-frozen matching approved parent may approve its child');
107
+ reset role;
108
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
109
+ 'EC-SIDSCHEMA-5-parent-state: parent approval stores the matching digest');
110
+ set local role service_role;
111
+ select pg_temp.sid_fp_subject('sibling');
112
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'approved', '{"resolution":"resolved"}')$q$),
113
+ '42501', 'identity approval requires the owner or an approved ancestor', 'EC-SIDSCHEMA-5-sibling-approver: a live approved sibling is not an ancestor');
114
+ select pg_temp.sid_fp_subject('child');
115
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'approved', '{"resolution":"resolved"}')$q$),
116
+ '42501', 'identity approval requires the owner or an approved ancestor', 'EC-SIDSCHEMA-5-self-approver: a live approved child is not its own ancestor');
117
+ reset role;
118
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
119
+ 'EC-SIDSCHEMA-5-actor-refusal-state: refused sibling/self calls leave approval unchanged');
120
+ set local role service_role;
121
+ select pg_temp.sid_fp_subject('parent');
122
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'parent'), '{"resolution":"resolved","risky_digest":"parent-changed","recorded":{"version":"one"}}')$q$,
123
+ 'EC-SIDSCHEMA-5-parent-reading: a trusted risky change is recorded without aborting its transaction');
124
+ reset role;
125
+ select ok((select current_risky_digest = 'parent-changed' and approved_risky_digest = 'approved' and resolution_state = 'frozen'
126
+ from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'parent')),
127
+ 'EC-SIDSCHEMA-5-risky-freeze: committed risky reading freezes identity without replacing the approved digest');
128
+ set local role service_role;
129
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'parent'), '{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"one"}}')$q$,
130
+ 'EC-SIDSCHEMA-5-frozen-equal-control: restoring the old bytes records a reading, not a new approval');
131
+ reset role;
132
+ select ok((select resolution_state = 'frozen' and approved_risky_digest = current_risky_digest
133
+ from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'parent')),
134
+ 'EC-SIDSCHEMA-5-frozen-equal-state: frozen approver fixture has equal digests, isolating the freeze gate');
135
+ set local role service_role;
136
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'approved', '{"resolution":"resolved"}')$q$),
137
+ '42501', 'identity approval requires the owner or an approved ancestor', 'EC-SIDSCHEMA-5-frozen-approver: frozen parent cannot approve a child');
138
+ reset role;
139
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
140
+ 'EC-SIDSCHEMA-5-frozen-approver-state: child approval stayed unchanged');
141
+ set local role service_role;
142
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'parent'), '{"resolution":"resolved","risky_digest":"parent-changed","recorded":{}}')$q$,
143
+ 'EC-SIDSCHEMA-5-parent-current-control: changed bytes are read again before owner approval');
144
+ reset role;
145
+ select is((select current_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'parent')), 'parent-changed',
146
+ 'EC-SIDSCHEMA-5-parent-current-state: owner approval uses the latest actual reading');
147
+ set local role service_role;
148
+ select pg_temp.sid_fp_subject('owner');
149
+ select lives_ok($q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'parent'), 'parent-changed', '{"resolution":"resolved"}')$q$,
150
+ 'EC-SIDSCHEMA-5-owner-change-control: human owner can approve a new resolved risky digest');
151
+ reset role;
152
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'parent')), 'parent-changed',
153
+ 'EC-SIDSCHEMA-5-owner-change-state: parent now has different owner-approved authority');
154
+ set local role service_role;
155
+ select pg_temp.sid_fp_subject('parent');
156
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'approved', '{"resolution":"resolved"}')$q$),
157
+ '42501', 'identity approval requires the owner or an approved ancestor', 'EC-SIDSCHEMA-5-parent-digest: parent approval must match its own current owner-approved risky digest');
158
+ reset role;
159
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
160
+ 'EC-SIDSCHEMA-5-parent-digest-state: mismatched parent cannot replace child approval');
161
+ set local role service_role;
162
+ select pg_temp.sid_fp_subject('owner');
163
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"fabricated"}')$q$),
164
+ '42501', 'identity_lease_not_held', 'EC-SIDSCHEMA-5-reading-subject: another forwarded principal cannot record this identity state');
165
+ reset role;
166
+ select is((select current_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
167
+ 'EC-SIDSCHEMA-5-reading-subject-state: mismatched subject left current reading untouched');
168
+ set local role service_role;
169
+ select pg_temp.sid_fp_subject('child');
170
+ select set_config('request.jwt.claim.identity_lease_epoch', '99', true);
171
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"fabricated"}')$q$),
172
+ '42501', 'identity_lease_not_held', 'EC-SIDSCHEMA-5-reading-epoch: stale forwarded epoch cannot record a new reading');
173
+ reset role;
174
+ select is((select current_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
175
+ 'EC-SIDSCHEMA-5-reading-epoch-state: stale epoch left current reading untouched');
176
+ set local role service_role;
177
+ select pg_temp.sid_fp_subject('child');
178
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"two","model":"another"}}')$q$,
179
+ 'EC-SIDSCHEMA-5-recorded-control: fresh recorded-only inputs are accepted');
180
+ reset role;
181
+ select ok((select resolution_state = 'active' and approved_risky_digest = 'approved' and current_risky_digest = 'approved'
182
+ and recorded_fingerprint ->> 'version' = 'two' from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')),
183
+ 'EC-SIDSCHEMA-5-recorded-only: recorded change is stored without freezing or changing risky approval');
184
+ set local role service_role;
185
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"child-changed","recorded":{"version":"two"}}')$q$,
186
+ 'EC-SIDSCHEMA-5-child-reading: trusted reading persists the child risky change');
187
+ reset role;
188
+ select ok((select resolution_state = 'frozen' and approved_risky_digest = 'approved' and current_risky_digest = 'child-changed'
189
+ from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')),
190
+ 'EC-SIDSCHEMA-5-child-freeze: child risky change is durably frozen before the next protected action');
191
+ select set_config('request.jwt.claim.role', 'authenticated', true);
192
+ set local role authenticated;
193
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.heartbeat_session_identity((select agent_id from _sid_fp_children where label = 'child'))$q$),
194
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-fresh-action: real stored risky change blocks authenticated heartbeat');
195
+ reset role;
196
+ select is((select resolution_state from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'frozen',
197
+ 'EC-SIDSCHEMA-5-fresh-action-state: refusal cannot roll back the earlier stored reading');
198
+ select set_config('request.jwt.claim.role', 'service_role', true);
199
+ set local role service_role;
200
+ select pg_temp.sid_fp_subject('owner');
201
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed', '{"resolution":"unresolved"}')$q$),
202
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-input-unresolved: a fresh unresolved input refuses even when the previous reading was resolved');
203
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed')$q$),
204
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-input-missing: a missing fresh resolution refuses even with a resolved stored reading');
205
+ reset role;
206
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
207
+ 'EC-SIDSCHEMA-5-input-refusal-state: neither missing nor unresolved input approved new bytes');
208
+ set local role service_role;
209
+ select pg_temp.sid_fp_subject('child');
210
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"unresolved","risky_digest":"child-changed","recorded":{"version":"two"}}')$q$,
211
+ 'EC-SIDSCHEMA-5-unresolved-reading: unresolved hook or registration state is recorded without approval');
212
+ reset role;
213
+ select ok((select resolution_state = 'frozen' and recorded_fingerprint ->> 'resolution' = 'unresolved'
214
+ from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')),
215
+ 'EC-SIDSCHEMA-5-unresolved-state: unresolved reading remains frozen and explicitly recorded');
216
+ set local role service_role;
217
+ select pg_temp.sid_fp_subject('owner');
218
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed', '{"resolution":"unresolved"}')$q$),
219
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-unresolved-approval: human owner cannot approve unresolved inputs');
220
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed')$q$),
221
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-missing-resolution: omitted resolution never defaults to resolved');
222
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed', '{"resolution":"resolved"}')$q$),
223
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-stored-unresolved: stale unresolved reading cannot be approved by changing only approval metadata');
224
+ reset role;
225
+ select is((select approved_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'approved',
226
+ 'EC-SIDSCHEMA-5-resolution-refusal-state: all unresolved/missing refusals preserve prior approval');
227
+ set local role service_role;
228
+ select pg_temp.sid_fp_subject('child');
229
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"child-changed","recorded":{"version":"two"}}')$q$,
230
+ 'EC-SIDSCHEMA-5-resolved-control: corrected hook gets a fresh resolved reading');
231
+ reset role;
232
+ select is((select resolution_state from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'frozen',
233
+ 'EC-SIDSCHEMA-5-resolved-pending: resolving inputs alone does not approve a changed risky digest');
234
+ set local role service_role;
235
+ select pg_temp.sid_fp_subject('owner');
236
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'fabricated', '{"resolution":"resolved"}')$q$),
237
+ '42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-reading-digest: approval cannot substitute an unrecorded risky digest');
238
+ reset role;
239
+ select is((select current_risky_digest from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 'child-changed',
240
+ 'EC-SIDSCHEMA-5-reading-digest-state: refusal retains the actually recorded risky digest');
241
+ set local role service_role;
242
+ select lives_ok($q$select agent_board.approve_identity_state((select agent_id from _sid_fp_children where label = 'child'), 'child-changed', '{"resolution":"resolved"}')$q$,
243
+ 'EC-SIDSCHEMA-5-owner-child-control: human owner approves the now-resolved changed child');
244
+ reset role;
245
+ select ok((select resolution_state = 'active' and approved_risky_digest = current_risky_digest
246
+ from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')),
247
+ 'EC-SIDSCHEMA-5-owner-child-state: explicit resolved approval thaws the child');
248
+ set local role service_role;
249
+ select pg_temp.sid_fp_subject('parent');
250
+ select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'parent'), '{"resolution":"resolved","risky_digest":"parent-new","recorded":{}}')$q$,
251
+ 'EC-SIDSCHEMA-5-frozen-revoker-control: parent becomes frozen again through an actual new reading');
252
+ reset role;
253
+ select is((select resolution_state from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'parent')), 'frozen',
254
+ 'EC-SIDSCHEMA-5-frozen-revoker-state: actor is positively frozen, not merely named');
255
+ select set_config('request.jwt.claim.role', 'authenticated', true);
256
+ set local role authenticated;
257
+ select lives_ok($q$select agent_board.revoke_session_identity((select agent_id from _sid_fp_children where label = 'child'))$q$,
258
+ 'EC-SIDSCHEMA-5-frozen-ancestor-revoke: any ancestor may revoke even when approval is frozen');
259
+ reset role;
260
+ select ok((select tombstoned_at is not null from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')),
261
+ 'EC-SIDSCHEMA-5-frozen-ancestor-revoke-state: selected child was permanently tombstoned');
262
+ select set_config('request.jwt.claim.role', 'service_role', true);
263
+ set local role service_role;
264
+ select pg_temp.sid_fp_subject('child');
265
+ select throws_ok(format('select pg_temp.sid_fp_refuse(%L)', $q$select agent_board.record_session_identity_state((select agent_id from _sid_fp_children where label = 'child'), '{"resolution":"resolved","risky_digest":"child-changed"}')$q$),
266
+ '42501', 'identity_tombstoned', 'EC-SIDSCHEMA-5-reading-tombstone: recording cannot revive a revoked context');
267
+ reset role;
268
+ select is((select credential_epoch from agent_board.session_identities where agent_id = (select agent_id from _sid_fp_children where label = 'child')), 1::bigint,
269
+ 'EC-SIDSCHEMA-5-reading-tombstone-state: refused recording preserves the revocation epoch');
270
+ select finish();
271
+ rollback;
@@ -0,0 +1,205 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ grant usage on schema extensions to authenticated;
7
+ select plan(30);
8
+
9
+ insert into auth.users(id, instance_id, aud, role, email)
10
+ values
11
+ ('9a000000-0000-4000-8000-000000000001', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'sid-label-admin@test.invalid'),
12
+ ('9a000000-0000-4000-8000-000000000002', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'sid-label-member@test.invalid'),
13
+ ('9a000000-0000-4000-8000-000000000003', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'sid-label-outsider@test.invalid');
14
+
15
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
16
+ values
17
+ ('9b000000-0000-4000-8000-000000000001', 'agent', 'agent', 'sid-label-admin', 'Board admin', 'active', '9a000000-0000-4000-8000-000000000001'),
18
+ ('9b000000-0000-4000-8000-000000000002', 'agent', 'agent', 'sid-label-member', 'Member display', 'active', '9a000000-0000-4000-8000-000000000002'),
19
+ ('9b000000-0000-4000-8000-000000000003', 'agent', 'agent', 'sid-label-outsider', 'Outsider', 'active', '9a000000-0000-4000-8000-000000000003');
20
+
21
+ insert into agent_board.boards(id, name, slug, created_by_agent_id)
22
+ values
23
+ ('9c000000-0000-4000-8000-000000000001', 'SID label board', 'sid-label-board', '9b000000-0000-4000-8000-000000000001'),
24
+ ('9c000000-0000-4000-8000-000000000002', 'SID other board', 'sid-other-board', '9b000000-0000-4000-8000-000000000003');
25
+
26
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
27
+ values
28
+ ('9c000000-0000-4000-8000-000000000001', '9b000000-0000-4000-8000-000000000001', 'admin', 'joined', '9b000000-0000-4000-8000-000000000001'),
29
+ ('9c000000-0000-4000-8000-000000000001', '9b000000-0000-4000-8000-000000000002', 'member', 'joined', '9b000000-0000-4000-8000-000000000001'),
30
+ ('9c000000-0000-4000-8000-000000000002', '9b000000-0000-4000-8000-000000000003', 'owner', 'joined', '9b000000-0000-4000-8000-000000000003');
31
+
32
+ select set_config('request.jwt.claims', json_build_object(
33
+ 'role', 'authenticated', 'sub', '9a000000-0000-4000-8000-000000000001'
34
+ )::text, true);
35
+
36
+ create temp table _label_before on commit drop as
37
+ select count(*)::int as event_count from agent_board.board_membership_events
38
+ where board_id = '9c000000-0000-4000-8000-000000000001';
39
+
40
+ select lives_ok(
41
+ $$select agent_board.set_board_member_label(
42
+ '9c000000-0000-4000-8000-000000000001',
43
+ '9b000000-0000-4000-8000-000000000002', 'Local label')$$,
44
+ 'EC-SIDSCHEMA-6-label-authorized: a board admin may set a label on a membership in that board'
45
+ );
46
+
47
+ select is(
48
+ (select metadata ->> 'label' from agent_board.board_memberships
49
+ where board_id = '9c000000-0000-4000-8000-000000000001'
50
+ and agent_id = '9b000000-0000-4000-8000-000000000002'),
51
+ 'Local label',
52
+ 'EC-SIDSCHEMA-6-label-local: the label is stored on the selected board membership'
53
+ );
54
+ select throws_ok($$select agent_board.set_board_member_label(
55
+ '9c000000-0000-4000-8000-000000000001','9b000000-0000-4000-8000-000000000002',repeat('x',201))$$,
56
+ '22023','board label exceeds 200 characters','R1-label-length: oversized board-local label is refused');
57
+ select is((select metadata->>'label' from agent_board.board_memberships
58
+ where board_id='9c000000-0000-4000-8000-000000000001' and agent_id='9b000000-0000-4000-8000-000000000002'),
59
+ 'Local label','R1-label-length-refusal-state: refusal preserves the existing label');
60
+ select lives_ok($$select agent_board.set_board_member_label(
61
+ '9c000000-0000-4000-8000-000000000001','9b000000-0000-4000-8000-000000000002',repeat('x',200))$$,
62
+ 'R1-label-boundary-control: exact 200-character boundary is allowed');
63
+ select is((select length(metadata->>'label') from agent_board.board_memberships
64
+ where board_id='9c000000-0000-4000-8000-000000000001' and agent_id='9b000000-0000-4000-8000-000000000002'),
65
+ 200,'R1-label-boundary-state: exact boundary is stored');
66
+
67
+ select is(
68
+ (select membership_state || '/' || role from agent_board.board_memberships
69
+ where board_id = '9c000000-0000-4000-8000-000000000001'
70
+ and agent_id = '9b000000-0000-4000-8000-000000000002'),
71
+ 'joined/member',
72
+ 'EC-SIDSCHEMA-6-label-state: a label changes neither membership state nor role'
73
+ );
74
+
75
+ select is(
76
+ (select count(*)::int from agent_board.board_membership_events
77
+ where board_id = '9c000000-0000-4000-8000-000000000001'),
78
+ (select event_count from _label_before),
79
+ 'EC-SIDSCHEMA-6-label-no-consent-event: metadata-only labels create no membership transition event'
80
+ );
81
+
82
+ select is(
83
+ (select handle || '/' || display_name from agent_board.agents
84
+ where id = '9b000000-0000-4000-8000-000000000002'),
85
+ 'sid-label-member/Member display',
86
+ 'EC-SIDSCHEMA-6-label-no-global-name: a local label changes neither handle nor display name'
87
+ );
88
+
89
+ select throws_ok(
90
+ $$select agent_board.set_board_member_label(
91
+ '9c000000-0000-4000-8000-000000000001',
92
+ '9b000000-0000-4000-8000-000000000003', 'Cross-board')$$,
93
+ 'P0002', 'membership not found on this board',
94
+ 'EC-SIDSCHEMA-6-label-cross-board: an admin cannot label an identity with no membership on that board'
95
+ );
96
+
97
+ select set_config('request.jwt.claims', json_build_object(
98
+ 'role', 'authenticated', 'sub', '9a000000-0000-4000-8000-000000000002'
99
+ )::text, true);
100
+
101
+ select throws_ok(
102
+ $$select agent_board.set_board_member_label(
103
+ '9c000000-0000-4000-8000-000000000001',
104
+ '9b000000-0000-4000-8000-000000000002', 'Unauthorized')$$,
105
+ '42501', 'board label requires a board owner or admin',
106
+ 'EC-SIDSCHEMA-6-label-authority: a non-admin membership cannot set a local label'
107
+ );
108
+
109
+ create temp table _sid_labels(owner_auth uuid, other_auth uuid, target_auth uuid,
110
+ owner_id uuid, other_id uuid, target_id uuid, board_a uuid, board_b uuid, handle text);
111
+ do $$
112
+ declare r _sid_labels%rowtype; s text := gen_random_uuid()::text;
113
+ begin
114
+ r.owner_auth := public.ensure_github_user('sid-label-owner-' || s, 'owner-' || s || '@example.test', 'owner-' || s, 'Owner');
115
+ r.other_auth := public.ensure_github_user('sid-label-other-' || s, 'other-' || s || '@example.test', 'other-' || s, 'Other');
116
+ r.target_auth := public.ensure_github_user('sid-label-target-' || s, 'target-' || s || '@example.test', 'target-' || s, 'Target');
117
+ r.owner_id := gen_random_uuid(); r.other_id := gen_random_uuid(); r.target_id := gen_random_uuid();
118
+ r.board_a := gen_random_uuid(); r.board_b := gen_random_uuid(); r.handle := 'sid-label-target-' || s;
119
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
120
+ values (r.owner_id, 'human', 'sid-label-owner-' || s, 'Owner', 'active', r.owner_auth, null),
121
+ (r.other_id, 'human', 'sid-label-other-' || s, 'Other', 'active', r.other_auth, null),
122
+ (r.target_id, 'agent', r.handle, 'Original', 'active', r.target_auth, r.owner_auth);
123
+ insert into agent_board.boards(id, slug, name, created_by_agent_id)
124
+ values (r.board_a, 'sid-label-a-' || s, 'Label A', r.owner_id),
125
+ (r.board_b, 'sid-label-b-' || s, 'Label B', r.other_id);
126
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
127
+ values (r.board_a, r.owner_id, 'joined', 'admin'), (r.board_b, r.owner_id, 'joined', 'observer'),
128
+ (r.board_b, r.other_id, 'joined', 'admin'), (r.board_a, r.target_id, 'joined', 'member'),
129
+ (r.board_b, r.target_id, 'joined', 'member');
130
+ insert into _sid_labels select r.*;
131
+ end;
132
+ $$;
133
+ grant select on _sid_labels to authenticated;
134
+ select set_config('request.jwt.claim.role', 'authenticated', true);
135
+ select set_config('request.jwt.claim.sub', (select owner_auth::text from _sid_labels), true);
136
+ set local role authenticated;
137
+ select lives_ok('select agent_board.rename_agent((select target_id from _sid_labels), ''Owned name'')',
138
+ 'EC-SIDSCHEMA-6-rename-owner: human owner can rename the global display name');
139
+ reset role;
140
+ select is((select display_name from agent_board.agents where id = (select target_id from _sid_labels)), 'Owned name',
141
+ 'EC-SIDSCHEMA-6-rename-state: the requested global display name is stored');
142
+ select is((select handle from agent_board.agents where id = (select target_id from _sid_labels)), (select handle from _sid_labels),
143
+ 'EC-SIDSCHEMA-6-rename-handle: global rename never changes the unique handle');
144
+ select set_config('request.jwt.claim.sub', (select other_auth::text from _sid_labels), true);
145
+ set local role authenticated;
146
+ select throws_ok('select agent_board.rename_agent((select target_id from _sid_labels), ''Stolen name'')',
147
+ '42501', 'agent rename requires the human owner', 'EC-SIDSCHEMA-6-rename-nonowner: unrelated board admin cannot rename globally');
148
+ reset role;
149
+ select is((select display_name from agent_board.agents where id = (select target_id from _sid_labels)), 'Owned name',
150
+ 'EC-SIDSCHEMA-6-rename-refusal-state: non-owner refusal leaves the global name unchanged');
151
+ select set_config('request.jwt.claim.sub', (select target_auth::text from _sid_labels), true);
152
+ set local role authenticated;
153
+ select throws_ok('select agent_board.rename_agent((select target_id from _sid_labels), ''Self renamed'')',
154
+ '42501', 'agent rename requires the human owner', 'EC-SIDSCHEMA-6-rename-agent: agent credential cannot exercise human-owner rename');
155
+ reset role;
156
+ select set_config('request.jwt.claim.sub', (select owner_auth::text from _sid_labels), true);
157
+ set local role authenticated;
158
+ select throws_ok('select agent_board.rename_agent((select target_id from _sid_labels), '''')',
159
+ '22023', 'display name is required', 'EC-SIDSCHEMA-6-rename-empty: empty global display name is refused');
160
+ select lives_ok('select agent_board.set_board_member_label((select board_a from _sid_labels), (select target_id from _sid_labels), ''Local A'')',
161
+ 'EC-SIDSCHEMA-6-label-admin: active admin labels a member on its own board');
162
+ reset role;
163
+ select is((select metadata ->> 'label' from agent_board.board_memberships where board_id = (select board_a from _sid_labels) and agent_id = (select target_id from _sid_labels)), 'Local A',
164
+ 'EC-SIDSCHEMA-6-label-metadata-state: requested label is stored on the named membership');
165
+ select is((select metadata ->> 'label' from agent_board.board_memberships where board_id = (select board_b from _sid_labels) and agent_id = (select target_id from _sid_labels)), null::text,
166
+ 'EC-SIDSCHEMA-6-label-other-state: label never writes another board membership');
167
+ select ok((select display_name = 'Owned name' and handle = (select handle from _sid_labels) from agent_board.agents where id = (select target_id from _sid_labels)),
168
+ 'EC-SIDSCHEMA-6-label-global-state: board label never changes global name or handle');
169
+ set local role authenticated;
170
+ select throws_ok('select agent_board.set_board_member_label((select board_b from _sid_labels), (select target_id from _sid_labels), ''Cross board'')',
171
+ '42501', 'board label requires a board owner or admin', 'EC-SIDSCHEMA-6-label-cross-board-actor: admin on A cannot label on B as an observer');
172
+ reset role;
173
+ select is((select metadata ->> 'label' from agent_board.board_memberships where board_id = (select board_b from _sid_labels) and agent_id = (select target_id from _sid_labels)), null::text,
174
+ 'EC-SIDSCHEMA-6-label-refusal-state: cross-board refusal preserves the other membership');
175
+ select set_config('request.jwt.claim.sub', (select other_auth::text from _sid_labels), true);
176
+ set local role authenticated;
177
+ select lives_ok('select agent_board.set_board_member_label((select board_b from _sid_labels), (select target_id from _sid_labels), ''Local B'')',
178
+ 'EC-SIDSCHEMA-6-label-other-admin: B admin independently labels on B');
179
+ reset role;
180
+ select is((select metadata ->> 'label' from agent_board.board_memberships where board_id = (select board_b from _sid_labels) and agent_id = (select target_id from _sid_labels)), 'Local B',
181
+ 'EC-SIDSCHEMA-6-label-other-admin-state: independent board-local label is stored');
182
+ set local role authenticated;
183
+ select throws_ok('select agent_board.set_board_member_label((select board_a from _sid_labels), (select target_id from _sid_labels), ''Wrong board'')',
184
+ '42501', 'board label requires a board owner or admin', 'EC-SIDSCHEMA-6-label-unjoined: another board admin with no membership on A is refused');
185
+ reset role;
186
+ -- Historical inconsistent role state: install only in this rolled-back scratch
187
+ -- fixture, without the normal role-reset trigger repairing it before the gate.
188
+ alter table agent_board.board_memberships disable trigger user;
189
+ update agent_board.board_memberships set membership_state = 'left', left_at = now(), role = 'admin' where board_id = (select board_a from _sid_labels) and agent_id = (select owner_id from _sid_labels);
190
+ alter table agent_board.board_memberships enable trigger user;
191
+ select ok((select membership_state = 'left' and role = 'admin' from agent_board.board_memberships where board_id = (select board_a from _sid_labels) and agent_id = (select owner_id from _sid_labels)),
192
+ 'EC-SIDSCHEMA-6-label-inactive-fixture: retained admin role cannot stand in for active membership');
193
+ select set_config('request.jwt.claim.sub', (select owner_auth::text from _sid_labels), true);
194
+ set local role authenticated;
195
+ select throws_ok('select agent_board.set_board_member_label((select board_a from _sid_labels), (select target_id from _sid_labels), ''Inactive admin'')',
196
+ '42501', 'board label requires a board owner or admin', 'EC-SIDSCHEMA-6-label-inactive: departed admin cannot label using its retained role');
197
+ reset role;
198
+ update agent_board.agents set status = 'suspended' where id = (select owner_id from _sid_labels);
199
+ set local role authenticated;
200
+ select throws_ok('select agent_board.set_board_member_label((select board_a from _sid_labels), (select target_id from _sid_labels), ''Suspended admin'')',
201
+ '42501', 'board label requires a board owner or admin', 'EC-SIDSCHEMA-6-label-suspended: suspended principal has no label authority');
202
+ reset role;
203
+
204
+ select * from finish();
205
+ rollback;