@consiliency/agent-board-schema 1.60.0 → 1.64.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 +86 -0
- package/fixtures/already-applied.json +16 -0
- package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
- package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
- package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
- package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/tooling.test.mjs +10 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +15 -4
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
- package/tests/agent_board_consent_emission.test.sql +30 -11
- package/tests/agent_board_consent_gating.test.sql +16 -2
- package/tests/agent_board_delegate_role_class.test.sql +1 -1
- package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
- package/tests/agent_board_delegation_enforcement.test.sql +1 -1
- package/tests/agent_board_enroll_resumption.test.sql +22 -4
- package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
- package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
- package/tests/agent_board_fk_lock_edges.test.sql +77 -16
- package/tests/agent_board_grant_target_role.test.sql +1118 -0
- package/tests/agent_board_hosted_delegates.test.sql +5 -3
- package/tests/agent_board_invite_no_downgrade.test.sql +15 -11
- package/tests/agent_board_ledger_authority.test.sql +77 -12
- package/tests/agent_board_membership_current_row_class.test.sql +9 -1
- package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
- package/tests/agent_board_mint_harden.test.sql +38 -27
- package/tests/agent_board_role_guard.test.sql +319 -0
- package/tests/agent_board_rolereset.test.sql +25 -29
- package/tests/agent_board_roomdeleg.test.sql +8 -0
- package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
- package/tests/agent_board_roomown.test.sql +3 -1
- package/tests/agent_board_rowid.test.sql +7 -2
- package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
- package/tests/agent_board_session_identity_backfill.test.sql +82 -0
- package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
- package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
- package/tests/agent_board_session_identity_class.test.sql +118 -0
- package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
- package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
- package/tests/agent_board_session_identity_lease.test.sql +235 -0
- package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
- package/tests/agent_board_session_identity_limits.test.sql +208 -0
- package/tests/agent_board_session_identity_lineage.test.sql +64 -0
- package/tests/agent_board_session_identity_link.test.sql +201 -0
- package/tests/agent_board_session_identity_mint.test.sql +233 -0
- package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
- package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
- package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
- package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
- package/tests/agent_board_sidowner_leases.test.sql +107 -0
- package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
- package/tests/agent_board_sidowner_read_class.test.sql +171 -0
- package/tests/agent_board_sidowner_reads.test.sql +136 -0
- package/tests/agent_board_signer_lifecycle.test.sql +26 -4
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
- package/tests/agent_board_write_enforcement_class.test.sql +74 -15
- package/tests/catalog/function_execute_privileges.json +227 -2
- package/tests/catalog/p_metadata_parameters.json +102 -2
- package/tests/isolation/expected/grant_target_role_race.out +348 -0
- package/tests/isolation/expected/last_owner_departure.out +562 -0
- package/tests/isolation/expected/session_identity_limits.out +165 -0
- package/tests/isolation/expected/session_identity_links.out +235 -0
- package/tests/isolation/expected/session_identity_reclaim.out +253 -0
- package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
- package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
- package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
- package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
- package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
- package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
- package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
- package/tests/isolation/specs/consent_approval_lock.spec +0 -1
- package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
- package/tests/isolation/specs/grant_target_role_race.spec +206 -0
- package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
- package/tests/isolation/specs/last_owner_departure.spec +137 -0
- package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
- package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
- package/tests/isolation/specs/session_identity_limits.spec +81 -0
- package/tests/isolation/specs/session_identity_links.spec +82 -0
- package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
- package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
- package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
- package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +107 -0
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
- package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
-- Owner readmission uses real authenticated calls; pending-key proof and first
|
|
2
|
+
-- claim use the existing trusted verification route, never a caller PoP flag.
|
|
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
|
+
grant execute on function public.test_pubkey(text) to authenticated;
|
|
8
|
+
select plan(61);
|
|
9
|
+
create temp table _sid_readmit_root(root_id uuid, owner_auth uuid, root_auth uuid, board_id uuid, suffix text);
|
|
10
|
+
create temp table _sid_readmit_children(label text primary key, agent_id uuid, key_id uuid, public_key text);
|
|
11
|
+
create temp table _sid_readmit_result(child_agent_id uuid, grant_expires_at timestamptz, signing_key_id uuid);
|
|
12
|
+
create temp table _sid_readmit_claim(epoch bigint, secret text);
|
|
13
|
+
do $$
|
|
14
|
+
declare v_root uuid := gen_random_uuid(); v_board uuid := gen_random_uuid(); v_owner uuid; v_auth uuid;
|
|
15
|
+
begin
|
|
16
|
+
v_owner := public.ensure_github_user('sid-readmit-owner-' || v_root, 'sid-owner-' || v_root || '@example.test', 'sid-owner-' || v_root, 'SID owner');
|
|
17
|
+
v_auth := public.ensure_github_user('sid-readmit-root-' || v_root, 'sid-root-' || v_root || '@example.test', 'sid-root-' || v_root, 'SID root');
|
|
18
|
+
insert into agent_board.agents(agent_type, handle, display_name, status, auth_user_id)
|
|
19
|
+
values ('human', 'sid-owner-' || v_root, 'SID owner', 'active', v_owner);
|
|
20
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
21
|
+
values (v_root, 'agent', 'sid-root-' || v_root, 'SID root', 'active', v_auth, v_owner);
|
|
22
|
+
insert into agent_board.session_identities(agent_id, root_agent_id, depth, approved_risky_digest, current_risky_digest)
|
|
23
|
+
values (v_root, v_root, 0, 'approved', 'approved');
|
|
24
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id)
|
|
25
|
+
values (v_board, 'sid-readmit-' || v_root, 'Readmit room', v_root);
|
|
26
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
|
|
27
|
+
values (v_board, v_root, 'joined', 'owner');
|
|
28
|
+
insert into _sid_readmit_root values (v_root, v_owner, v_auth, v_board, substr(v_root::text, 1, 8));
|
|
29
|
+
end;
|
|
30
|
+
$$;
|
|
31
|
+
grant select on _sid_readmit_root, _sid_readmit_children, _sid_readmit_result to authenticated, service_role;
|
|
32
|
+
grant insert on _sid_readmit_children to service_role;
|
|
33
|
+
grant insert on _sid_readmit_result to authenticated;
|
|
34
|
+
grant insert on _sid_readmit_claim to service_role;
|
|
35
|
+
grant select (epoch) on _sid_readmit_claim to authenticated;
|
|
36
|
+
create function pg_temp.sid_readmit_mint(p_label text, p_fork text, p_role text)
|
|
37
|
+
returns void language plpgsql as $$
|
|
38
|
+
declare v_root uuid; v_board uuid; v_suffix text; v_fork uuid; v_public text;
|
|
39
|
+
begin
|
|
40
|
+
select root_id, board_id, suffix into v_root, v_board, v_suffix from _sid_readmit_root;
|
|
41
|
+
v_fork := (select agent_id from _sid_readmit_children where label = p_fork);
|
|
42
|
+
v_public := public.test_pubkey(v_suffix || p_label);
|
|
43
|
+
insert into _sid_readmit_children
|
|
44
|
+
select p_label, m.child_agent_id, m.signing_key_id, v_public from agent_board.mint_delegated_child(
|
|
45
|
+
v_root, 'sid-readmit-' || p_label || '-' || v_suffix, '["read"]', v_public, interval '8 hours', false,
|
|
46
|
+
'claude', 'readmit-' || p_label, null,
|
|
47
|
+
jsonb_build_array(jsonb_build_object('board_id', v_board, 'role', p_role)),
|
|
48
|
+
'{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"one"}}', v_fork) m;
|
|
49
|
+
end;
|
|
50
|
+
$$;
|
|
51
|
+
create function pg_temp.sid_readmit(p_target text, p_label text, p_scopes jsonb default '["read"]', p_role text default 'observer', p_key text default null)
|
|
52
|
+
returns void language plpgsql as $$
|
|
53
|
+
begin
|
|
54
|
+
insert into _sid_readmit_result select * from agent_board.readmit_session_identity(
|
|
55
|
+
(select agent_id from _sid_readmit_children where label = p_target),
|
|
56
|
+
'sid-readmit-new-' || p_label || '-' || (select suffix from _sid_readmit_root),
|
|
57
|
+
coalesce(p_key, public.test_pubkey('readmit-new-' || p_label || (select suffix from _sid_readmit_root))), p_scopes,
|
|
58
|
+
jsonb_build_array(jsonb_build_object('board_id', (select board_id from _sid_readmit_root), 'role', p_role)), interval '14 days');
|
|
59
|
+
end;
|
|
60
|
+
$$;
|
|
61
|
+
create function pg_temp.sid_readmit_refuse(p_call text)
|
|
62
|
+
returns void language plpgsql as $$
|
|
63
|
+
begin
|
|
64
|
+
execute p_call;
|
|
65
|
+
raise exception 'expected refusal but RPC succeeded' using errcode = 'P0001';
|
|
66
|
+
end;
|
|
67
|
+
$$;
|
|
68
|
+
create function pg_temp.sid_readmit_proof(p_prove boolean, p_session_id text default 'readmit-fork')
|
|
69
|
+
returns void language plpgsql as $$
|
|
70
|
+
declare v_child record; v_challenge record;
|
|
71
|
+
begin
|
|
72
|
+
select * into v_child from _sid_readmit_result;
|
|
73
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_child.child_agent_id, 'readmitted-ephemeral');
|
|
74
|
+
if p_prove then
|
|
75
|
+
perform * from agent_board.record_signing_key_proof_result(v_challenge.challenge_id, v_child.child_agent_id, 'readmitted-ephemeral', true);
|
|
76
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_child.child_agent_id, 'readmitted-ephemeral');
|
|
77
|
+
end if;
|
|
78
|
+
insert into _sid_readmit_claim
|
|
79
|
+
select m.identity_lease_epoch, m.holder_secret from agent_board.mint_fleet_agent_session(v_challenge.challenge_id,
|
|
80
|
+
split_part(v_challenge.challenge, ':', 3), v_child.signing_key_id, v_child.child_agent_id, 900, null, p_session_id) m;
|
|
81
|
+
end;
|
|
82
|
+
$$;
|
|
83
|
+
create function pg_temp.sid_readmit_bind()
|
|
84
|
+
returns void language plpgsql as $$
|
|
85
|
+
declare v_child record; v_challenge record; v_epoch bigint;
|
|
86
|
+
begin
|
|
87
|
+
select * into v_child from _sid_readmit_children where label = 'fork';
|
|
88
|
+
perform set_config('request.jwt.claim.sub', (select auth_user_id::text from agent_board.agents where id = v_child.agent_id), true);
|
|
89
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_child.agent_id, 'delegated-ephemeral');
|
|
90
|
+
select m.identity_lease_epoch into v_epoch from agent_board.mint_fleet_agent_session(v_challenge.challenge_id,
|
|
91
|
+
split_part(v_challenge.challenge, ':', 3), v_child.key_id, v_child.agent_id, 900, null, 'readmit-fork') m;
|
|
92
|
+
perform agent_board.link_session_alias(v_child.agent_id, v_epoch::text, 'readmit-fork-later');
|
|
93
|
+
end;
|
|
94
|
+
$$;
|
|
95
|
+
grant execute on function pg_temp.sid_readmit_mint(text, text, text), pg_temp.sid_readmit_proof(boolean, text), pg_temp.sid_readmit_bind() to service_role;
|
|
96
|
+
grant execute on function pg_temp.sid_readmit(text, text, jsonb, text, text), pg_temp.sid_readmit_refuse(text) to authenticated, service_role;
|
|
97
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
98
|
+
set local role service_role;
|
|
99
|
+
select lives_ok($q$select agent_board.upsert_delegation_policy((select root_id from _sid_readmit_root), 'root_minted', '["read","write"]', 200, interval '7 days', true)$q$,
|
|
100
|
+
'EC-SIDSCHEMA-5-readmit-policy: configure the root through the sole policy writer');
|
|
101
|
+
reset role;
|
|
102
|
+
select is((select count(*) from agent_board.delegation_policies where root_agent_id = (select root_id from _sid_readmit_root)), 1::bigint,
|
|
103
|
+
'EC-SIDSCHEMA-5-readmit-policy-state: one current policy exists');
|
|
104
|
+
set local role service_role;
|
|
105
|
+
select lives_ok($q$select pg_temp.sid_readmit_mint('source', null, 'admin'); select pg_temp.sid_readmit_mint('fork', 'source', 'admin'); select pg_temp.sid_readmit_mint('second', 'source', 'observer')$q$,
|
|
106
|
+
'EC-SIDSCHEMA-5-readmit-fixtures: real mint creates a source and two related sessions');
|
|
107
|
+
reset role;
|
|
108
|
+
select is((select count(*) from _sid_readmit_children), 3::bigint, 'EC-SIDSCHEMA-5-readmit-fixture-state: all three principals were returned');
|
|
109
|
+
select is((select count(*) from agent_board.session_identities si join _sid_readmit_children c on c.agent_id = si.agent_id
|
|
110
|
+
join agent_board.session_id_aliases a on a.id = si.lease_alias_id where a.identity_agent_id = si.agent_id and a.session_id = 'readmit-' || c.label), 3::bigint,
|
|
111
|
+
'EC-SIDSCHEMA-1-mint-reservation-pointer: every real session mint stores its specific reserved alias');
|
|
112
|
+
set local role service_role;
|
|
113
|
+
select lives_ok('select pg_temp.sid_readmit_bind()', 'EC-SIDSCHEMA-5-readmit-bind-control: proved holder links a new alias');
|
|
114
|
+
reset role;
|
|
115
|
+
select is((select a.session_id from agent_board.session_identities si join agent_board.session_id_aliases a on a.id = si.lease_alias_id
|
|
116
|
+
where si.agent_id = (select agent_id from _sid_readmit_children where label = 'fork')), 'readmit-fork-later',
|
|
117
|
+
'EC-SIDSCHEMA-5-readmit-new-binding: a new linked alias becomes the current binding');
|
|
118
|
+
set local role service_role;
|
|
119
|
+
select lives_ok($q$select agent_board.link_session_alias((select agent_id from _sid_readmit_children where label = 'fork'), '0', 'readmit-fork')$q$,
|
|
120
|
+
'EC-SIDSCHEMA-5-readmit-rebind-control: holder rebinds the older existing alias');
|
|
121
|
+
reset role;
|
|
122
|
+
select is((select a.session_id from agent_board.session_identities si join agent_board.session_id_aliases a on a.id = si.lease_alias_id
|
|
123
|
+
where si.agent_id = (select agent_id from _sid_readmit_children where label = 'fork')), 'readmit-fork',
|
|
124
|
+
'EC-SIDSCHEMA-5-readmit-last-bound: binding an existing alias updates the current binding');
|
|
125
|
+
-- Creation order deliberately differs from binding order, even within one transaction.
|
|
126
|
+
update agent_board.session_id_aliases set created_at = now() + interval '1 second'
|
|
127
|
+
where identity_agent_id = (select agent_id from _sid_readmit_children where label = 'fork') and session_id = 'readmit-fork-later';
|
|
128
|
+
select is((select session_id from agent_board.session_id_aliases where identity_agent_id = (select agent_id from _sid_readmit_children where label = 'fork')
|
|
129
|
+
order by created_at desc, id desc limit 1), 'readmit-fork-later', 'EC-SIDSCHEMA-5-readmit-created-control: newest-created alias is not the latest binding');
|
|
130
|
+
-- A removed higher-role row must not restore authority that the current row lost.
|
|
131
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_at, left_at)
|
|
132
|
+
select board_id, (select agent_id from _sid_readmit_children where label = 'second'), 'removed', 'owner', now() + interval '1 second', now()
|
|
133
|
+
from _sid_readmit_root;
|
|
134
|
+
select is((select count(*) from agent_board.board_memberships bm where bm.agent_id = (select agent_id from _sid_readmit_children where label = 'second')
|
|
135
|
+
and ((bm.membership_state = 'removed' and bm.role = 'owner') or (bm.membership_state = 'joined' and bm.role = 'observer'))), 2::bigint,
|
|
136
|
+
'EC-SIDSCHEMA-5-readmit-history-control: a removed higher role coexists with current observer standing');
|
|
137
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
138
|
+
select set_config('request.jwt.claim.sub', (select owner_auth::text from _sid_readmit_root), true);
|
|
139
|
+
set local role authenticated;
|
|
140
|
+
select lives_ok($q$select agent_board.revoke_session_identity((select agent_id from _sid_readmit_children where label = 'fork')); select agent_board.revoke_session_identity((select agent_id from _sid_readmit_children where label = 'second'))$q$,
|
|
141
|
+
'EC-SIDSCHEMA-5-readmit-revoke: human owner explicitly revokes selected forks');
|
|
142
|
+
reset role;
|
|
143
|
+
select is((select count(*) from agent_board.session_identities where agent_id in (select agent_id from _sid_readmit_children where label <> 'source') and tombstone_reason = 'revoked'), 2::bigint,
|
|
144
|
+
'EC-SIDSCHEMA-5-readmit-revoke-state: selected forks are tombstoned');
|
|
145
|
+
insert into agent_board.fleet_revocation_events(subject_kind, subject_id, agent_id, revoked_by_agent_id, revocation_reason)
|
|
146
|
+
select 'agent', a.id, a.id, a.id, 'sid-readmit-owner-ledger-control'
|
|
147
|
+
from agent_board.agents a where a.auth_user_id = (select owner_auth from _sid_readmit_root);
|
|
148
|
+
select is((select status from agent_board.agents where auth_user_id = (select owner_auth from _sid_readmit_root)), 'active',
|
|
149
|
+
'EC-SIDSCHEMA-5-readmit-owner-ledger-control: ledger-only revocation leaves the owner human active');
|
|
150
|
+
set local role authenticated;
|
|
151
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('fork', 'ledger-owner', '["read"]', 'admin')$q$),
|
|
152
|
+
'42501', 'fleet_revoked: readmit_session_identity is not permitted for a fleet-revoked agent',
|
|
153
|
+
'EC-SIDSCHEMA-5-readmit-owner-ledger: permanently revoked human owner cannot mint new board authority');
|
|
154
|
+
reset role;
|
|
155
|
+
select is((select count(*) from _sid_readmit_result), 0::bigint,
|
|
156
|
+
'EC-SIDSCHEMA-5-readmit-owner-ledger-state: late refusal rolled back the successor mint');
|
|
157
|
+
delete from agent_board.fleet_revocation_events where revocation_reason = 'sid-readmit-owner-ledger-control'
|
|
158
|
+
and subject_id = (select id from agent_board.agents where auth_user_id = (select owner_auth from _sid_readmit_root));
|
|
159
|
+
set local role authenticated;
|
|
160
|
+
select lives_ok($q$select pg_temp.sid_readmit('fork', 'allowed', '["read"]', 'admin')$q$,
|
|
161
|
+
'EC-SIDSCHEMA-5-readmit-owner: authenticated human owner mints the selected successor');
|
|
162
|
+
reset role;
|
|
163
|
+
select is((select count(*) from _sid_readmit_result), 1::bigint, 'EC-SIDSCHEMA-5-readmit-result: exactly one successor is returned');
|
|
164
|
+
select is((select provenance || '/' || actor_agent_id::text from agent_board.board_membership_events
|
|
165
|
+
where board_id = (select board_id from _sid_readmit_root) and agent_id = (select child_agent_id from _sid_readmit_result)
|
|
166
|
+
order by seq desc limit 1), 'board-authored/' || (select id::text from agent_board.agents where auth_user_id = (select owner_auth from _sid_readmit_root)),
|
|
167
|
+
'EC-SIDSCHEMA-5-readmit-consent: placement records the actual human owner, never the new subject consent');
|
|
168
|
+
select is((select count(*) from agent_board.board_membership_events
|
|
169
|
+
where board_id = (select board_id from _sid_readmit_root) and agent_id = (select child_agent_id from _sid_readmit_result)), 1::bigint,
|
|
170
|
+
'EC-SIDSCHEMA-5-readmit-consent-once: fresh membership transition emits exactly once despite the role assignment');
|
|
171
|
+
select isnt((select child_agent_id from _sid_readmit_result), (select agent_id from _sid_readmit_children where label = 'fork'),
|
|
172
|
+
'EC-SIDSCHEMA-5-readmit-fresh: readmission never revives the old principal in place');
|
|
173
|
+
select is((select status from agent_board.agents where id = (select agent_id from _sid_readmit_children where label = 'fork')), 'revoked',
|
|
174
|
+
'EC-SIDSCHEMA-5-readmit-old-principal: old principal stays revoked');
|
|
175
|
+
select is((select status || '/' || proof_state from agent_board.agent_signing_keys where id = (select key_id from _sid_readmit_children where label = 'fork')), 'revoked/revoked',
|
|
176
|
+
'EC-SIDSCHEMA-5-readmit-old-key: old key is never re-enabled');
|
|
177
|
+
select is((select status || '/' || proof_state from agent_board.agent_signing_keys where id = (select signing_key_id from _sid_readmit_result)), 'pending_proof/unproven',
|
|
178
|
+
'EC-SIDSCHEMA-5-readmit-pending-key: new key requires the existing proof flow');
|
|
179
|
+
select is((select readmitted_from from agent_board.session_identities where agent_id = (select child_agent_id from _sid_readmit_result)), (select agent_id from _sid_readmit_children where label = 'fork'),
|
|
180
|
+
'EC-SIDSCHEMA-5-readmit-linkage: successor records its predecessor');
|
|
181
|
+
select is((select forked_from from agent_board.session_identities where agent_id = (select child_agent_id from _sid_readmit_result)), (select agent_id from _sid_readmit_children where label = 'source'),
|
|
182
|
+
'EC-SIDSCHEMA-5-readmit-fork: successor preserves fork ancestry');
|
|
183
|
+
select ok((select holder_secret_hash is null and credential_epoch = 0 from agent_board.session_identities where agent_id = (select child_agent_id from _sid_readmit_result)),
|
|
184
|
+
'EC-SIDSCHEMA-5-readmit-unclaimed: successor is reserved but has no holder lease');
|
|
185
|
+
select is((select identity_agent_id from agent_board.session_id_aliases where root_agent_id = (select root_id from _sid_readmit_root) and session_id = 'readmit-fork'), (select child_agent_id from _sid_readmit_result),
|
|
186
|
+
'EC-SIDSCHEMA-5-readmit-alias: predecessor alias resolves to its successor');
|
|
187
|
+
select is((select a.session_id from agent_board.session_identities si join agent_board.session_id_aliases a on a.id = si.lease_alias_id
|
|
188
|
+
where si.agent_id = (select child_agent_id from _sid_readmit_result)), 'readmit-fork',
|
|
189
|
+
'EC-SIDSCHEMA-5-readmit-reserved: successor reserves the most recently bound alias, not the newest-created row');
|
|
190
|
+
select is((select count(*) from agent_board.session_id_aliases where identity_agent_id = (select child_agent_id from _sid_readmit_result)), 2::bigint,
|
|
191
|
+
'EC-SIDSCHEMA-5-readmit-all-aliases: all predecessor aliases resolve to the new principal');
|
|
192
|
+
select is((select grant_expires_at from _sid_readmit_result), now() + interval '24 hours',
|
|
193
|
+
'EC-SIDSCHEMA-5-readmit-ttl: unapproved fresh context is never awarded seven days');
|
|
194
|
+
select ok((select count(*) > 0 from agent_board.fleet_revocation_events where subject_id = (select agent_id from _sid_readmit_children where label = 'fork')),
|
|
195
|
+
'EC-SIDSCHEMA-5-readmit-ledger: permanent old revocation ledger remains');
|
|
196
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
197
|
+
set local role service_role;
|
|
198
|
+
select lives_ok($q$select agent_board.approve_identity_state((select child_agent_id from _sid_readmit_result), 'approved', '{"resolution":"resolved"}')$q$,
|
|
199
|
+
'EC-SIDSCHEMA-5-readmit-approve: trusted fresh reading preserves the verified human owner approval actor');
|
|
200
|
+
reset role;
|
|
201
|
+
select is((select resolution_state from agent_board.session_identities where agent_id = (select child_agent_id from _sid_readmit_result)), 'active',
|
|
202
|
+
'EC-SIDSCHEMA-5-readmit-approval-state: fresh approval is stored');
|
|
203
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
204
|
+
set local role service_role;
|
|
205
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit_proof(false)$q$),
|
|
206
|
+
'42501', 'key_unproven: session key requires proof', 'EC-SIDSCHEMA-5-readmit-unproved: pending key cannot claim a lease');
|
|
207
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit_proof(true, 'readmit-fork-later')$q$),
|
|
208
|
+
'42501', 'identity_continuity_refused', 'EC-SIDSCHEMA-1-readmit-first-reserved: another repointed historical alias cannot win first claim');
|
|
209
|
+
reset role;
|
|
210
|
+
select ok((select holder_secret_hash is null and credential_epoch = 0 from agent_board.session_identities where agent_id = (select child_agent_id from _sid_readmit_result)),
|
|
211
|
+
'EC-SIDSCHEMA-1-readmit-first-refusal-state: refused historical-alias claim leaves the reservation unclaimed');
|
|
212
|
+
set local role service_role;
|
|
213
|
+
select lives_ok('select pg_temp.sid_readmit_proof(true)', 'EC-SIDSCHEMA-5-readmit-proof: existing challenge/proof route permits first claim');
|
|
214
|
+
reset role;
|
|
215
|
+
select is((select count(*) from _sid_readmit_claim where epoch = 0 and length(secret) = 64), 1::bigint,
|
|
216
|
+
'EC-SIDSCHEMA-5-readmit-claim-state: proved key claims its reserved alias once');
|
|
217
|
+
-- Exercise the returned principal directly as well as through its new credential.
|
|
218
|
+
-- Reviving the predecessor in place reaches the unchanged permanent ledger here
|
|
219
|
+
-- and refuses fleet_revoked, rather than merely failing old-credential resolution.
|
|
220
|
+
set local role service_role;
|
|
221
|
+
select lives_ok($q$select agent_board.grant_board_role((select board_id from _sid_readmit_root), (select child_agent_id from _sid_readmit_result), 'admin', (select child_agent_id from _sid_readmit_result))$q$,
|
|
222
|
+
'EC-SIDSCHEMA-5-readmit-new-principal-authority: the returned fresh principal passes the unchanged permanent-ledger authority gate');
|
|
223
|
+
reset role;
|
|
224
|
+
select is((select role from agent_board.board_memberships where agent_id = (select child_agent_id from _sid_readmit_result) and board_id = (select board_id from _sid_readmit_root)), 'admin',
|
|
225
|
+
'EC-SIDSCHEMA-5-readmit-new-principal-state: explicit fresh-principal authority preserves the bounded admin role');
|
|
226
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
227
|
+
select set_config('request.jwt.claim.sub', (select auth_user_id::text from agent_board.agents where id = (select child_agent_id from _sid_readmit_result)), true);
|
|
228
|
+
select set_config('request.jwt.claim.identity_lease_epoch', (select epoch::text from _sid_readmit_claim), true);
|
|
229
|
+
set local role authenticated;
|
|
230
|
+
select lives_ok($q$select agent_board.grant_board_role((select board_id from _sid_readmit_root), (select child_agent_id from _sid_readmit_result), 'observer', null)$q$,
|
|
231
|
+
'EC-SIDSCHEMA-5-readmit-authority: readmitted principal can exercise board authority');
|
|
232
|
+
reset role;
|
|
233
|
+
select is((select role from agent_board.board_memberships where agent_id = (select child_agent_id from _sid_readmit_result) and board_id = (select board_id from _sid_readmit_root)), 'observer',
|
|
234
|
+
'EC-SIDSCHEMA-5-readmit-authority-state: board authority uses the fresh principal');
|
|
235
|
+
select set_config('request.jwt.claim.sub', (select auth_user_id::text from agent_board.agents where id = (select agent_id from _sid_readmit_children where label = 'fork')), true);
|
|
236
|
+
set local role authenticated;
|
|
237
|
+
select is(agent_board.current_agent_id(), null::uuid, 'EC-SIDSCHEMA-5-readmit-old-resolver: revoked old credential cannot resolve');
|
|
238
|
+
reset role;
|
|
239
|
+
-- The revoked principal cannot resolve as authenticated. Enter the permanent
|
|
240
|
+
-- ledger gate through the existing service forwarder's explicit grantor path.
|
|
241
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
242
|
+
set local role service_role;
|
|
243
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select agent_board.grant_board_role((select board_id from _sid_readmit_root), (select agent_id from _sid_readmit_children where label = 'fork'), 'observer', (select agent_id from _sid_readmit_children where label = 'fork'))$q$),
|
|
244
|
+
'42501', 'fleet_revoked: grant_board_role is not permitted for a fleet-revoked agent', 'EC-SIDSCHEMA-5-readmit-old-fleet: old principal still reports permanent fleet_revoked');
|
|
245
|
+
reset role;
|
|
246
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
247
|
+
select set_config('request.jwt.claim.sub', (select owner_auth::text from _sid_readmit_root), true);
|
|
248
|
+
set local role authenticated;
|
|
249
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('fork', 'again')$q$),
|
|
250
|
+
'42501', 'identity_already_readmitted', 'EC-SIDSCHEMA-5-readmit-once: a predecessor cannot have a second successor');
|
|
251
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'widen-scopes', '["read","write"]')$q$),
|
|
252
|
+
'42501', 'scope_exceeded: readmission cannot widen original or parent scopes', 'EC-SIDSCHEMA-5-readmit-scope-bound: widened parent cannot re-grant lost scopes');
|
|
253
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'widen-role', '["read"]', 'admin')$q$),
|
|
254
|
+
'42501', 'room_authority_exceeded', 'EC-SIDSCHEMA-5-readmit-role-bound: widened parent cannot re-grant lost roles');
|
|
255
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'old-key', '["read"]', 'observer', (select public_key from _sid_readmit_children where label = 'second'))$q$),
|
|
256
|
+
'42501', 'readmission cannot reuse the revoked key', 'EC-SIDSCHEMA-5-readmit-key-fresh: old signing key cannot be reused');
|
|
257
|
+
reset role;
|
|
258
|
+
update agent_board.session_identities set resolution_state = 'frozen' where agent_id = (select root_id from _sid_readmit_root);
|
|
259
|
+
set local role authenticated;
|
|
260
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'frozen-ancestor')$q$),
|
|
261
|
+
'42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-5-readmit-ancestor: readmission inherits the ancestor freeze gate');
|
|
262
|
+
reset role;
|
|
263
|
+
update agent_board.session_identities set resolution_state = 'active' where agent_id = (select root_id from _sid_readmit_root);
|
|
264
|
+
update agent_board.agents set status = 'suspended' where id = (select root_id from _sid_readmit_root);
|
|
265
|
+
set local role authenticated;
|
|
266
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'inactive-parent')$q$),
|
|
267
|
+
'42501', 'identity_parent_inactive', 'EC-SIDSCHEMA-5-readmit-parent: original parent must still be active');
|
|
268
|
+
reset role;
|
|
269
|
+
update agent_board.agents set status = 'active' where id = (select root_id from _sid_readmit_root);
|
|
270
|
+
update agent_board.session_identities set tombstone_reason = 'superseded_unclaimed' where agent_id = (select agent_id from _sid_readmit_children where label = 'second');
|
|
271
|
+
set local role authenticated;
|
|
272
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'superseded')$q$),
|
|
273
|
+
'42501', 'identity readmission requires a revoked fork', 'EC-SIDSCHEMA-5-readmit-superseded: superseded unclaimed identity cannot be readmitted');
|
|
274
|
+
reset role;
|
|
275
|
+
select is((select count(*) from agent_board.session_identities where readmitted_from in (select agent_id from _sid_readmit_children)), 1::bigint,
|
|
276
|
+
'EC-SIDSCHEMA-5-readmit-refusal-state: refused readmissions leave no extra successors');
|
|
277
|
+
select is((select count(*) from agent_board.delegation_grants where root_agent_id = (select root_id from _sid_readmit_root)), 4::bigint,
|
|
278
|
+
'EC-SIDSCHEMA-5-readmit-count: readmission adds a real mint to hourly accounting');
|
|
279
|
+
-- Historical legacy grants put the mixed active total exactly at 200. Their
|
|
280
|
+
-- timestamps are outside the rolling hour; source/fork reservations stay hourly-only.
|
|
281
|
+
create temp table _sid_readmit_limit_ids(agent_id uuid);
|
|
282
|
+
insert into _sid_readmit_limit_ids select gen_random_uuid() from generate_series(1, 199);
|
|
283
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id, parent_agent_id, root_agent_id)
|
|
284
|
+
select q.agent_id, 'agent', 'sid-readmit-limit-' || q.agent_id, 'Limit fixture', 'active', r.owner_auth, r.root_id, r.root_id
|
|
285
|
+
from _sid_readmit_limit_ids q cross join _sid_readmit_root r;
|
|
286
|
+
insert into agent_board.session_identities(agent_id, parent_agent_id, root_agent_id, depth)
|
|
287
|
+
select q.agent_id, r.root_id, r.root_id, 1 from _sid_readmit_limit_ids q cross join _sid_readmit_root r;
|
|
288
|
+
insert into agent_board.delegation_grants(child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id, granted_scopes, grant_expires_at, created_at)
|
|
289
|
+
select q.agent_id, r.root_id, r.root_id, dp.id, '[]'::jsonb, now() + interval '1 hour', now() - interval '2 hours'
|
|
290
|
+
from _sid_readmit_limit_ids q cross join _sid_readmit_root r join agent_board.delegation_policies dp on dp.root_agent_id = r.root_id;
|
|
291
|
+
update agent_board.session_identities set tombstone_reason = 'revoked' where agent_id = (select agent_id from _sid_readmit_children where label = 'second');
|
|
292
|
+
set local role authenticated;
|
|
293
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', $q$select pg_temp.sid_readmit('second', 'active-cap')$q$),
|
|
294
|
+
'42501', 'identity_limit_active_exceeded', 'EC-SIDSCHEMA-5-readmit-active-limit: readmission recounts mixed legacy/session active authority');
|
|
295
|
+
reset role;
|
|
296
|
+
update agent_board.agents set status = 'suspended' where id in (select agent_id from _sid_readmit_limit_ids);
|
|
297
|
+
create temp table _sid_readmit_cycle(agent_id uuid);
|
|
298
|
+
insert into _sid_readmit_cycle select agent_id from _sid_readmit_children where label = 'second';
|
|
299
|
+
grant select, update on _sid_readmit_cycle to authenticated;
|
|
300
|
+
create function pg_temp.sid_readmit_cycles(p_count int)
|
|
301
|
+
returns void language plpgsql as $$
|
|
302
|
+
declare v_old uuid; v_new record; v_slug text; i int;
|
|
303
|
+
begin
|
|
304
|
+
for i in 1..p_count loop
|
|
305
|
+
select agent_id into v_old from _sid_readmit_cycle;
|
|
306
|
+
v_slug := 'sid-cycle-' || gen_random_uuid();
|
|
307
|
+
select * into v_new from agent_board.readmit_session_identity(v_old, v_slug, public.test_pubkey(v_slug));
|
|
308
|
+
perform agent_board.revoke_session_identity(v_new.child_agent_id);
|
|
309
|
+
update _sid_readmit_cycle set agent_id = v_new.child_agent_id;
|
|
310
|
+
end loop;
|
|
311
|
+
end;
|
|
312
|
+
$$;
|
|
313
|
+
grant execute on function pg_temp.sid_readmit_cycles(int) to authenticated;
|
|
314
|
+
set local role authenticated;
|
|
315
|
+
select lives_ok('select pg_temp.sid_readmit_cycles(56)', 'EC-SIDSCHEMA-5-readmit-cycles: revoke/readmit cycles enter the real owner mint path up to the hourly boundary');
|
|
316
|
+
reset role;
|
|
317
|
+
select is((select count(*) from agent_board.session_identities where root_agent_id = (select root_id from _sid_readmit_root) and readmitted_from is not null), 57::bigint,
|
|
318
|
+
'EC-SIDSCHEMA-5-readmit-cycles-state: every allowed cycle creates a distinct successor');
|
|
319
|
+
set local role authenticated;
|
|
320
|
+
select throws_ok(format('select pg_temp.sid_readmit_refuse(%L)', 'select pg_temp.sid_readmit_cycles(1)'),
|
|
321
|
+
'42501', 'identity_limit_hourly_exceeded', 'EC-SIDSCHEMA-5-readmit-hourly-limit: repeated real revoke/readmit cycles hit the named rolling-hour limit');
|
|
322
|
+
reset role;
|
|
323
|
+
select is((select count(*) from agent_board.delegation_grants where root_agent_id = (select root_id from _sid_readmit_root) and created_at > now() - interval '3600 seconds'), 60::bigint,
|
|
324
|
+
'EC-SIDSCHEMA-5-readmit-hourly-state: tombstoned readmission mints remain in the hourly census');
|
|
325
|
+
update agent_board.delegation_grants set created_at = now() - interval '2 hours' where root_agent_id = (select root_id from _sid_readmit_root);
|
|
326
|
+
update agent_board.delegation_policies set operator_child_ttl_ceiling = interval '30 minutes' where root_agent_id = (select root_id from _sid_readmit_root);
|
|
327
|
+
set local role authenticated;
|
|
328
|
+
select lives_ok('select pg_temp.sid_readmit_cycles(1)',
|
|
329
|
+
'EC-SIDSCHEMA-4-readmit-operator-control: real owner readmission enters the newly tightened operator policy');
|
|
330
|
+
reset role;
|
|
331
|
+
select is((select grant_expires_at from agent_board.delegation_grants where child_agent_id = (select agent_id from _sid_readmit_cycle)), now() + interval '30 minutes',
|
|
332
|
+
'EC-SIDSCHEMA-4-readmit-operator: readmission never bypasses the durable operator TTL');
|
|
333
|
+
select finish();
|
|
334
|
+
rollback;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
create extension if not exists pgtap with schema extensions;
|
|
2
|
+
set search_path = extensions, public, agent_board;
|
|
3
|
+
|
|
4
|
+
begin;
|
|
5
|
+
select plan(15);
|
|
6
|
+
|
|
7
|
+
select is(
|
|
8
|
+
(select identity_kind from agent_board.session_identities
|
|
9
|
+
where agent_id = '61100000-0000-0000-0000-000000000001'),
|
|
10
|
+
'legacy',
|
|
11
|
+
'EC-SIDSCHEMA-1: pre-migration root is represented as a legacy identity'
|
|
12
|
+
);
|
|
13
|
+
select is(
|
|
14
|
+
(select depth from agent_board.session_identities
|
|
15
|
+
where agent_id = '61100000-0000-0000-0000-000000000001'),
|
|
16
|
+
0::smallint,
|
|
17
|
+
'EC-SIDSCHEMA-1: pre-migration unparented root remains depth zero'
|
|
18
|
+
);
|
|
19
|
+
select is(
|
|
20
|
+
(select root_agent_id from agent_board.session_identities
|
|
21
|
+
where agent_id = '61100000-0000-0000-0000-000000000001'),
|
|
22
|
+
'61100000-0000-0000-0000-000000000001'::uuid,
|
|
23
|
+
'EC-SIDSCHEMA-1: null legacy root is normalized to itself'
|
|
24
|
+
);
|
|
25
|
+
select is(
|
|
26
|
+
(select parent_agent_id from agent_board.session_identities
|
|
27
|
+
where agent_id = '61100000-0000-0000-0000-000000000002'),
|
|
28
|
+
'61100000-0000-0000-0000-000000000001'::uuid,
|
|
29
|
+
'EC-SIDSCHEMA-1: legacy child parent is preserved exactly'
|
|
30
|
+
);
|
|
31
|
+
select is(
|
|
32
|
+
(select root_agent_id from agent_board.session_identities
|
|
33
|
+
where agent_id = '61100000-0000-0000-0000-000000000002'),
|
|
34
|
+
'61100000-0000-0000-0000-000000000001'::uuid,
|
|
35
|
+
'EC-SIDSCHEMA-1: legacy child root is preserved exactly'
|
|
36
|
+
);
|
|
37
|
+
select is(
|
|
38
|
+
(select depth from agent_board.session_identities
|
|
39
|
+
where agent_id = '61100000-0000-0000-0000-000000000002'),
|
|
40
|
+
1::smallint,
|
|
41
|
+
'EC-SIDSCHEMA-1: pre-migration parented child is depth one'
|
|
42
|
+
);
|
|
43
|
+
select is(
|
|
44
|
+
(select count(*)::int from agent_board.session_identities
|
|
45
|
+
where agent_id in (
|
|
46
|
+
'61100000-0000-0000-0000-000000000001'::uuid,
|
|
47
|
+
'61100000-0000-0000-0000-000000000002'::uuid
|
|
48
|
+
)),
|
|
49
|
+
2,
|
|
50
|
+
'EC-SIDSCHEMA-1: every seeded pre-migration principal is backfilled exactly once'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
select is(
|
|
54
|
+
(select depth from agent_board.session_identities where agent_id = '61100000-0000-0000-0000-000000000003'),
|
|
55
|
+
2::smallint, 'EC-SIDSCHEMA-1-backfill-grandchild: recursive backfill preserves depth two');
|
|
56
|
+
select is(
|
|
57
|
+
(select root_agent_id from agent_board.session_identities where agent_id = '61100000-0000-0000-0000-000000000003'),
|
|
58
|
+
'61100000-0000-0000-0000-000000000001'::uuid,
|
|
59
|
+
'EC-SIDSCHEMA-1-backfill-root: recursive backfill uses the true lineage root');
|
|
60
|
+
select is((select owner_user_id from agent_board.agents where id='61100000-0000-0000-0000-000000000001'),
|
|
61
|
+
'61100000-0000-0000-0000-000000000010'::uuid,
|
|
62
|
+
'EC-SIDSCHEMA-1-backfill-owner: migration preserves the existing human owner exactly');
|
|
63
|
+
select ok((select created_at='2026-01-01 00:00:00+00'::timestamptz and metadata='{"sid_backfill":"historical"}'::jsonb
|
|
64
|
+
from agent_board.agents where id='61100000-0000-0000-0000-000000000001'),
|
|
65
|
+
'EC-SIDSCHEMA-1-backfill-agent-history: original principal creation time and metadata remain unchanged');
|
|
66
|
+
select ok((select public_key=public.test_pubkey('sid-historical-backfill')
|
|
67
|
+
and fingerprint=agent_board._raw_key_fingerprint(public.test_pubkey('sid-historical-backfill'))
|
|
68
|
+
and status='pending_proof' and proof_state='unproven' and created_at='2026-01-01 00:00:00+00'::timestamptz
|
|
69
|
+
and metadata='{"sid_backfill":"historical-key"}'::jsonb from agent_board.agent_signing_keys
|
|
70
|
+
where id='61100000-0000-0000-0000-000000000011'),
|
|
71
|
+
'EC-SIDSCHEMA-1-backfill-key: original signing key, proof state and history are preserved byte-for-byte');
|
|
72
|
+
select ok((select result_state='joined' and provenance='operator-designated' and actor_agent_id is null
|
|
73
|
+
and created_at='2026-01-01 00:00:00+00'::timestamptz from agent_board.board_membership_events
|
|
74
|
+
where id='61100000-0000-0000-0000-000000000013'),
|
|
75
|
+
'EC-SIDSCHEMA-1-backfill-consent-history: the original append-only placement event and provenance remain unchanged');
|
|
76
|
+
select is((select count(*) from agent_board.board_membership_events where board_id='61100000-0000-0000-0000-000000000012'),
|
|
77
|
+
1::bigint,'EC-SIDSCHEMA-1-backfill-history-count: backfill does not synthesize consent or duplicate historical events');
|
|
78
|
+
select is((select count(*) from agent_board.session_identities where root_agent_id='61100000-0000-0000-0000-000000000001'
|
|
79
|
+
and identity_kind='legacy' and approved_risky_digest is null and holder_secret_hash is null and lease_alias_id is null),
|
|
80
|
+
3::bigint,'EC-SIDSCHEMA-1-backfill-no-implicit-approval: all legacy lineage rows remain unapproved and lease-free');
|
|
81
|
+
select * from finish();
|
|
82
|
+
rollback;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Re-run the SHIPPED backfill block against a mismatch on every CI pgTAP run.
|
|
2
|
+
-- psql reads local migration bytes; no server file access or copied SQL body.
|
|
3
|
+
\set sid_backfill_source `node -e 'process.stdout.write(require("fs").readFileSync("packages/agent-board-schema/migrations/20261011000100_agent_board_session_identities.sql").toString("base64"))'`
|
|
4
|
+
set search_path=extensions,public,agent_board;
|
|
5
|
+
begin;
|
|
6
|
+
select plan(1);
|
|
7
|
+
update agent_board.agents set root_agent_id='61100000-0000-0000-0000-000000000002'
|
|
8
|
+
where id='61100000-0000-0000-0000-000000000003';
|
|
9
|
+
select throws_ok(coalesce(nullif(current_setting('agent_board_test.pending_migration',true),''),
|
|
10
|
+
'do $$'||split_part(split_part(convert_from(decode(:'sid_backfill_source','base64'),'UTF8'),'do $$',2),'$$;',1)||'$$;'),
|
|
11
|
+
'23514','session identity backfill lineage mismatch: agent 61100000-0000-0000-0000-000000000003, stored root 61100000-0000-0000-0000-000000000002, computed root 61100000-0000-0000-0000-000000000001',
|
|
12
|
+
'R2-N2-5-backfill-ci: shipped backfill refuses the mismatch and names the agent, stored root and computed root');
|
|
13
|
+
select finish();
|
|
14
|
+
rollback;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
-- Literal-SQL class closure, including private writers and their callers.
|
|
2
|
+
-- Dynamic SQL, variable callees and Edge signers are outside this SQL scanner;
|
|
3
|
+
-- the Edge wiring is asserted separately against its actual source files.
|
|
4
|
+
create extension if not exists pgtap with schema extensions;
|
|
5
|
+
set search_path = extensions, public, agent_board;
|
|
6
|
+
begin;
|
|
7
|
+
select plan(23);
|
|
8
|
+
create temp view _sid_behavior_fns as
|
|
9
|
+
select p.oid,p.proname,agent_board_test.stripped_body(p.oid) as body,
|
|
10
|
+
agent_board_test.stripped_body(p.oid,true) as literals
|
|
11
|
+
from pg_proc p where p.pronamespace='agent_board'::regnamespace and p.prokind='f';
|
|
12
|
+
create temp view _sid_behavior_writers as
|
|
13
|
+
select * from _sid_behavior_fns f where f.body ~ agent_board_test.re(
|
|
14
|
+
agent_board_test.write_frag('session_identities',agent_board_test.bare_ok(f.oid)) || '|'
|
|
15
|
+
|| agent_board_test.write_frag('session_id_aliases',agent_board_test.bare_ok(f.oid)));
|
|
16
|
+
select is((select string_agg(proname,', ' order by proname) from _sid_behavior_writers),
|
|
17
|
+
'_revoke_agent_principal, _session_identity_agent_lineage, approve_identity_state, heartbeat_session_identity, link_session_alias, mint_delegated_child, mint_fleet_agent_session, readmit_session_identity, record_session_identity_state, renew_session_identity',
|
|
18
|
+
'EC-SIDSCHEMA-5-protected-population: every catalog-derived identity/alias writer has a gate or a positively proved lifecycle exemption');
|
|
19
|
+
create temp view _sid_behavior_unguarded as
|
|
20
|
+
select w.* from _sid_behavior_writers w
|
|
21
|
+
where w.proname not in ('_revoke_agent_principal','_session_identity_agent_lineage','approve_identity_state',
|
|
22
|
+
'readmit_session_identity','record_session_identity_state')
|
|
23
|
+
and not exists (select 1 from agent_board_test.fn_call_closure() c join _sid_behavior_fns g on g.oid=c.reached_oid
|
|
24
|
+
where c.root_oid=w.oid and g.literals ~ agent_board_test.re(
|
|
25
|
+
agent_board_test.ident_re('resolution_state') || '\s*=\s*''frozen''')
|
|
26
|
+
and g.literals ~ agent_board_test.re(agent_board_test.ci_re('raise exception') || '\s+''identity_fingerprint_unapproved'));
|
|
27
|
+
create temp view _sid_behavior_unsafe as
|
|
28
|
+
select distinct c.root_name from agent_board_test.fn_call_closure() c
|
|
29
|
+
join _sid_behavior_unguarded u on u.oid=c.reached_oid;
|
|
30
|
+
select is((select coalesce(string_agg(root_name,', ' order by root_name),'') from _sid_behavior_unsafe),'',
|
|
31
|
+
'EC-SIDSCHEMA-5-protected-class: every discovered protected writer/caller reaches an executable frozen-state refusal');
|
|
32
|
+
-- Each exempt lifecycle writer positively names the actual state it writes.
|
|
33
|
+
select ok(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)
|
|
34
|
+
~ agent_board_test.col_write_re('session_identities','tombstoned_at')
|
|
35
|
+
and agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)
|
|
36
|
+
~ agent_board_test.col_write_re('session_identities','credential_epoch'),
|
|
37
|
+
'EC-SIDSCHEMA-5-exempt-revoke: authorized revocation writes tombstones and epoch fences even while frozen; seven real entrypoints check its resulting closure');
|
|
38
|
+
select ok(agent_board_test.stripped_body('agent_board._session_identity_agent_lineage()'::regprocedure)
|
|
39
|
+
~ agent_board_test.col_write_re('session_identities','agent_id')
|
|
40
|
+
and agent_board_test.stripped_body('agent_board._session_identity_agent_lineage()'::regprocedure,true)
|
|
41
|
+
like '%tg_op = ''INSERT'' and v_node.id = new.id%',
|
|
42
|
+
'EC-SIDSCHEMA-1-exempt-constructor: the lineage trigger writes only a fresh legacy identity on an agents INSERT; real registration proves null lease and approval');
|
|
43
|
+
select ok(agent_board_test.stripped_body('agent_board.approve_identity_state(uuid,text,jsonb)'::regprocedure)
|
|
44
|
+
~ agent_board_test.col_write_re('session_identities','approved_risky_digest')
|
|
45
|
+
and agent_board_test.stripped_body('agent_board.approve_identity_state(uuid,text,jsonb)'::regprocedure)
|
|
46
|
+
~ agent_board_test.call_re('_session_identity_owner'),
|
|
47
|
+
'EC-SIDSCHEMA-5-exempt-approval: explicit approval writes the approved digest through the human-owner/strict-ancestor gate, not an implicit renewal');
|
|
48
|
+
select ok(agent_board_test.stripped_body('agent_board.record_session_identity_state(uuid,jsonb)'::regprocedure)
|
|
49
|
+
~ agent_board_test.col_write_re('session_identities','resolution_state')
|
|
50
|
+
and agent_board_test.stripped_body('agent_board.record_session_identity_state(uuid,jsonb)'::regprocedure,true)
|
|
51
|
+
like '%v_identity.resolution_state = ''frozen'' then ''frozen''%',
|
|
52
|
+
'EC-SIDSCHEMA-5-exempt-recording: measured recording writes persistent freeze and preserves an existing freeze; risky and recorded-only calls have separate state checks');
|
|
53
|
+
select ok(agent_board_test.stripped_body('agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)'::regprocedure)
|
|
54
|
+
~ agent_board_test.col_write_re('session_identities','readmitted_from')
|
|
55
|
+
and agent_board_test.stripped_body('agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)'::regprocedure,true)
|
|
56
|
+
like '%p_identity_agent_id, v_old.current_risky_digest, v_old.recorded_fingerprint, ''frozen''%',
|
|
57
|
+
'EC-SIDSCHEMA-5-exempt-readmission: owner readmission writes a fresh frozen successor with provenance; the old principal and ledger remain revoked');
|
|
58
|
+
|
|
59
|
+
create temp view _sid_behavior_issuers as
|
|
60
|
+
select * from _sid_behavior_fns f where f.body ~ agent_board_test.re(
|
|
61
|
+
agent_board_test.stmt_frag('insert into','nonfleet_sessions',agent_board_test.bare_ok(f.oid)) || '|'
|
|
62
|
+
|| agent_board_test.stmt_frag('insert into','agent_credentials',agent_board_test.bare_ok(f.oid)));
|
|
63
|
+
select is((select string_agg(proname,', ' order by proname) from _sid_behavior_issuers),
|
|
64
|
+
'_issue_agent_credential, _mint_fleet_agent_session_legacy, exchange_delegate_credential',
|
|
65
|
+
'EC-SIDSCHEMA-4-issuer-population: every catalog-discovered credential issuer has a bounded session path or a positively proved enrollment exemption');
|
|
66
|
+
select is((select coalesce(string_agg(c.root_name,', ' order by c.root_name),'')
|
|
67
|
+
from agent_board_test.fn_call_closure() c
|
|
68
|
+
where c.reached_oid='agent_board._mint_fleet_agent_session_legacy(uuid,text,uuid,uuid,integer)'::regprocedure
|
|
69
|
+
and c.root_oid<>c.reached_oid and c.root_name<>'mint_fleet_agent_session'),'',
|
|
70
|
+
'EC-SIDSCHEMA-4-issuer-wrapper-class: only the gated fleet session mint reaches the private legacy token issuer; a new bypass wrapper names itself');
|
|
71
|
+
select ok(not has_function_privilege('service_role',
|
|
72
|
+
'agent_board._mint_fleet_agent_session_legacy(uuid,text,uuid,uuid,integer)','EXECUTE')
|
|
73
|
+
and has_function_privilege('service_role','agent_board.mint_fleet_agent_session(uuid,text,uuid,uuid,integer,text,text)','EXECUTE'),
|
|
74
|
+
'EC-SIDSCHEMA-4-issuer-private-positive: service must enter the gated mint, never the renamed private implementation');
|
|
75
|
+
select ok(agent_board_test.stripped_body('agent_board._issue_agent_credential(uuid,text,uuid,jsonb)'::regprocedure,true)
|
|
76
|
+
like '%jsonb_build_object(''credential_role'', ''enrollment'')%',
|
|
77
|
+
'EC-SIDSCHEMA-4-enrollment-positive: the legacy issuer writes enrollment-only metadata, not a session-exchange credential');
|
|
78
|
+
select throws_ok($$select agent_board.rotate_agent_token('61100000-0000-0000-0000-000000000001')$$,
|
|
79
|
+
'0A000',null,'EC-SIDSCHEMA-4-enrollment-retired: the real legacy session-exchange path refuses enrollment refresh records');
|
|
80
|
+
select ok(agent_board_test.stripped_body('agent_board.exchange_delegate_credential(jsonb)'::regprocedure,true)
|
|
81
|
+
like '%v_expires := now() + interval ''900 seconds''%'
|
|
82
|
+
and pg_get_function_result('agent_board.exchange_delegate_credential(jsonb)'::regprocedure)=
|
|
83
|
+
'TABLE(agent_id uuid, auth_user_id uuid, credential_id uuid, expires_at timestamp with time zone)',
|
|
84
|
+
'EC-SIDSCHEMA-4-hosted-positive: hosted exchange writes a 900-second credential and returns its principal/id/expiry, never a holder secret or session identity');
|
|
85
|
+
|
|
86
|
+
-- A principal resolver returns a UUID and directly maps auth.uid through agents.
|
|
87
|
+
-- Named callers join through the same literal-SQL call graph; no resolver is
|
|
88
|
+
-- exempt from fencing. Row-returning RPCs and dynamically chosen callees are
|
|
89
|
+
-- not principal resolvers in this bounded scanner.
|
|
90
|
+
create temp view _sid_behavior_resolvers as
|
|
91
|
+
select * from _sid_behavior_fns f where pg_get_function_result(f.oid)='uuid'
|
|
92
|
+
and f.body ~ agent_board_test.call_re('auth.uid',false)
|
|
93
|
+
and f.body ~ agent_board_test.re(agent_board_test.qname_frag('agents',agent_board_test.bare_ok(f.oid)));
|
|
94
|
+
create temp view _sid_behavior_unsafe_resolvers as
|
|
95
|
+
select distinct c.root_name from agent_board_test.fn_call_closure() c
|
|
96
|
+
join _sid_behavior_resolvers r on r.oid=c.reached_oid
|
|
97
|
+
where not (r.body ~ agent_board_test.re(agent_board_test.qname_frag('session_identities',agent_board_test.bare_ok(r.oid)))
|
|
98
|
+
and r.literals like '%request.jwt.claim.identity_lease_epoch%'
|
|
99
|
+
and r.literals like '%request.jwt.claims%'
|
|
100
|
+
and r.literals like '%identity_kind is distinct from ''session'' or c.lease_epoch = si.credential_epoch::text%');
|
|
101
|
+
select is((select string_agg(proname,', ' order by proname) from _sid_behavior_resolvers),
|
|
102
|
+
'current_agent_id','EC-SIDSCHEMA-1-resolver-population: every catalog-discovered direct principal resolver is positively pinned');
|
|
103
|
+
select is((select coalesce(string_agg(root_name,', ' order by root_name),'') from _sid_behavior_unsafe_resolvers),'',
|
|
104
|
+
'EC-SIDSCHEMA-1-resolver-class: every direct principal resolver and named caller reaches the current session epoch fence from both JWT claim forms');
|
|
105
|
+
|
|
106
|
+
create temp view _sid_behavior_revokers as
|
|
107
|
+
select * from _sid_behavior_fns f where
|
|
108
|
+
(f.body ~ agent_board_test.re(agent_board_test.col_assign_frag('agents','status',agent_board_test.bare_ok(f.oid)))
|
|
109
|
+
and f.literals like '%''revoked''%')
|
|
110
|
+
or f.body ~ agent_board_test.stmt_re('insert into','fleet_revocation_events',agent_board_test.bare_ok(f.oid));
|
|
111
|
+
create temp view _sid_behavior_unsafe_revokers as
|
|
112
|
+
select * from _sid_behavior_revokers r where not exists (
|
|
113
|
+
select 1 from agent_board_test.fn_call_closure() c where c.root_oid=r.oid
|
|
114
|
+
and c.reached_oid='agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure);
|
|
115
|
+
select is((select string_agg(proname,', ' order by proname) from _sid_behavior_revokers),
|
|
116
|
+
'_revoke_agent_principal, revoke_agent, revoke_fleet_subject, tombstone_delegate_instance',
|
|
117
|
+
'EC-SIDSCHEMA-5-revoke-population: every catalog-derived principal status/ledger revoker is in the positively exercised revocation class');
|
|
118
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _sid_behavior_unsafe_revokers),'',
|
|
119
|
+
'EC-SIDSCHEMA-5-revoke-class: every principal revocation entrypoint reaches the shared identity fork/descendant tombstone and credential closure');
|
|
120
|
+
select is((select string_agg(distinct root_name,', ' order by root_name) from agent_board_test.fn_call_closure()
|
|
121
|
+
where reached_oid='agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure
|
|
122
|
+
and root_name in ('_revoke_agent_principal','revoke_agent_subtree','revoke_human_subtree',
|
|
123
|
+
'revoke_fleet_subject','revoke_agent','revoke_session_identity','tombstone_delegate_instance')),
|
|
124
|
+
'_revoke_agent_principal, revoke_agent, revoke_agent_subtree, revoke_fleet_subject, revoke_human_subtree, revoke_session_identity, tombstone_delegate_instance',
|
|
125
|
+
'EC-SIDSCHEMA-5-revoke-routes-positive: all seven independently exercised real routes reach the shared write-through; none is merely absent from a violation list');
|
|
126
|
+
|
|
127
|
+
-- New unsafe sites and delegating wrappers must join the classes by effect,
|
|
128
|
+
-- even with uppercase SQL, comments containing decoy gates and no public ACL.
|
|
129
|
+
create function agent_board.sid_behavior_unsafe_writer(p_identity uuid)
|
|
130
|
+
returns void language sql security definer set search_path='' as $probe$
|
|
131
|
+
/* resolution_state = 'frozen'; raise exception 'identity_fingerprint_unapproved'; */
|
|
132
|
+
UPDATE agent_board.session_identities SET last_heartbeat=now() WHERE agent_id=p_identity;
|
|
133
|
+
$probe$;
|
|
134
|
+
create function agent_board.sid_behavior_unsafe_wrapper(p_identity uuid)
|
|
135
|
+
returns void language sql security definer set search_path='' as $probe$
|
|
136
|
+
SELECT agent_board.sid_behavior_unsafe_writer(/* no direct write */ p_identity);
|
|
137
|
+
$probe$;
|
|
138
|
+
select is((select string_agg(root_name,', ' order by root_name) from _sid_behavior_unsafe
|
|
139
|
+
where root_name in ('sid_behavior_unsafe_wrapper','sid_behavior_unsafe_writer')),
|
|
140
|
+
'sid_behavior_unsafe_wrapper, sid_behavior_unsafe_writer',
|
|
141
|
+
'EC-SIDSCHEMA-5-protected-canary: an unsafe uppercase writer and commented wrapper both name themselves; comment gates do not count');
|
|
142
|
+
create function agent_board.sid_behavior_unsafe_issuer(p_agent uuid)
|
|
143
|
+
returns void language sql security definer set search_path='' as $probe$
|
|
144
|
+
INSERT INTO agent_board.nonfleet_sessions(id,session_id,identity_class,agent_id,max_ttl_seconds,issued_at,expires_at)
|
|
145
|
+
VALUES ('unsafe','unsafe','fleet_agent_session',p_agent,900,now(),now()+interval '100 days');
|
|
146
|
+
$probe$;
|
|
147
|
+
select is((select proname from _sid_behavior_issuers where proname='sid_behavior_unsafe_issuer'),
|
|
148
|
+
'sid_behavior_unsafe_issuer','EC-SIDSCHEMA-4-issuer-canary: a new unsafe credential INSERT names itself in the catalog-derived issuer population');
|
|
149
|
+
create function agent_board.sid_behavior_private_bypass(p_challenge uuid,p_key uuid,p_agent uuid)
|
|
150
|
+
returns void language sql security definer set search_path='' as $probe$
|
|
151
|
+
SELECT agent_board._mint_fleet_agent_session_legacy(p_challenge,'nonce',p_key,p_agent,900);
|
|
152
|
+
$probe$;
|
|
153
|
+
select is((select root_name from agent_board_test.fn_call_closure() where root_name='sid_behavior_private_bypass'
|
|
154
|
+
and reached_oid='agent_board._mint_fleet_agent_session_legacy(uuid,text,uuid,uuid,integer)'::regprocedure),
|
|
155
|
+
'sid_behavior_private_bypass','EC-SIDSCHEMA-4-issuer-bypass-canary: a new private-issuer wrapper is detected rather than exempted by its own absence of DML');
|
|
156
|
+
create function agent_board.sid_behavior_unsafe_revoker(p_agent uuid)
|
|
157
|
+
returns void language sql security definer set search_path='' as $probe$
|
|
158
|
+
UPDATE agent_board.agents SET status='revoked' WHERE id=p_agent;
|
|
159
|
+
$probe$;
|
|
160
|
+
select is((select proname from _sid_behavior_unsafe_revokers where proname='sid_behavior_unsafe_revoker'),
|
|
161
|
+
'sid_behavior_unsafe_revoker','EC-SIDSCHEMA-5-revoke-canary: a new principal revoker without identity write-through names itself in the catalog-derived class');
|
|
162
|
+
create function agent_board.sid_behavior_unsafe_resolver()
|
|
163
|
+
returns uuid language sql security definer set search_path='' as $probe$
|
|
164
|
+
/* session_identities; identity_lease_epoch; credential_epoch are not a gate. */
|
|
165
|
+
SELECT a.id FROM agent_board.agents a WHERE a.auth_user_id=auth.uid() LIMIT 1;
|
|
166
|
+
$probe$;
|
|
167
|
+
create function agent_board.sid_behavior_resolver_wrapper()
|
|
168
|
+
returns uuid language sql security definer set search_path='' as $probe$
|
|
169
|
+
SELECT agent_board.sid_behavior_unsafe_resolver();
|
|
170
|
+
$probe$;
|
|
171
|
+
select is((select string_agg(root_name,', ' order by root_name) from _sid_behavior_unsafe_resolvers
|
|
172
|
+
where root_name in ('sid_behavior_resolver_wrapper','sid_behavior_unsafe_resolver')),
|
|
173
|
+
'sid_behavior_resolver_wrapper, sid_behavior_unsafe_resolver',
|
|
174
|
+
'EC-SIDSCHEMA-1-resolver-canary: an uppercase auth resolver and its named wrapper both join the unsafe class; comment decoys do not fence credentials');
|
|
175
|
+
select finish();
|
|
176
|
+
rollback;
|