@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,233 @@
|
|
|
1
|
+
-- Reservation and bounded mint transitions enter the real service-role RPCs.
|
|
2
|
+
create extension if not exists pgtap with schema extensions;
|
|
3
|
+
set search_path = extensions, public, agent_board;
|
|
4
|
+
begin;
|
|
5
|
+
grant usage on schema extensions to service_role, authenticated;
|
|
6
|
+
select plan(44);
|
|
7
|
+
|
|
8
|
+
create temp table _sid_mint_root(root_id uuid, suffix text);
|
|
9
|
+
create temp table _sid_children(label text primary key, agent_id uuid, key_id uuid, expires_at timestamptz);
|
|
10
|
+
do $$
|
|
11
|
+
declare v_root uuid := gen_random_uuid(); v_owner uuid; v_auth uuid;
|
|
12
|
+
begin
|
|
13
|
+
v_owner := public.ensure_github_user('sid-res-owner-' || v_root, 'sid-res-owner-' || v_root || '@example.test', 'sid-res-owner-' || v_root, 'SID owner');
|
|
14
|
+
v_auth := public.ensure_github_user('sid-res-root-' || v_root, 'sid-res-root-' || v_root || '@example.test', 'sid-res-root-' || v_root, 'SID root');
|
|
15
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
16
|
+
values (v_root, 'agent', 'sid-res-root-' || v_root, 'SID root', 'active', v_auth, v_owner);
|
|
17
|
+
insert into agent_board.session_identities(agent_id, root_agent_id, depth) values (v_root, v_root, 0);
|
|
18
|
+
insert into _sid_mint_root values (v_root, substr(v_root::text, 1, 8));
|
|
19
|
+
end;
|
|
20
|
+
$$;
|
|
21
|
+
grant select on _sid_mint_root, _sid_children to service_role;
|
|
22
|
+
grant insert on _sid_children to service_role;
|
|
23
|
+
|
|
24
|
+
create function pg_temp.sid_child(p_label text, p_alias text)
|
|
25
|
+
returns void language plpgsql as $$
|
|
26
|
+
declare v_root uuid; v_suffix text;
|
|
27
|
+
begin
|
|
28
|
+
select root_id, suffix into v_root, v_suffix from _sid_mint_root;
|
|
29
|
+
insert into _sid_children
|
|
30
|
+
select p_label, m.child_agent_id, m.signing_key_id, m.grant_expires_at
|
|
31
|
+
from agent_board.mint_delegated_child(v_root, 'sid-res-' || p_label || '-' || v_suffix,
|
|
32
|
+
'[]'::jsonb, public.test_pubkey(v_suffix || p_label), interval '8 hours', false, 'codex', p_alias, null) m;
|
|
33
|
+
end;
|
|
34
|
+
$$;
|
|
35
|
+
create function pg_temp.sid_child_refuse(p_call text)
|
|
36
|
+
returns void language plpgsql as $$
|
|
37
|
+
begin
|
|
38
|
+
execute p_call;
|
|
39
|
+
raise exception 'expected refusal but RPC succeeded' using errcode = 'P0001';
|
|
40
|
+
end;
|
|
41
|
+
$$;
|
|
42
|
+
grant execute on function pg_temp.sid_child(text, text), pg_temp.sid_child_refuse(text) to service_role;
|
|
43
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
44
|
+
set local role service_role;
|
|
45
|
+
select lives_ok($q$select agent_board.upsert_delegation_policy((select root_id from _sid_mint_root), 'root_minted', '[]', 200, interval '8 hours', true)$q$,
|
|
46
|
+
'EC-SIDSCHEMA-1-reservation-policy: trusted root policy is configured');
|
|
47
|
+
reset role;
|
|
48
|
+
select is((select count(*) from agent_board.delegation_policies where root_agent_id = (select root_id from _sid_mint_root) and revoked_at is null), 1::bigint,
|
|
49
|
+
'EC-SIDSCHEMA-1-reservation-policy-state: exactly one active policy was created');
|
|
50
|
+
set local role service_role;
|
|
51
|
+
select lives_ok($q$select pg_temp.sid_child(label, 'alias-' || label) from unnest(array['live','young','lapsed','revoked','stale']) label$q$,
|
|
52
|
+
'EC-SIDSCHEMA-1-reservation-mint: session child mint reserves an initial id for every child');
|
|
53
|
+
reset role;
|
|
54
|
+
select is((select count(*) from _sid_children), 5::bigint,
|
|
55
|
+
'EC-SIDSCHEMA-1-reservation-mint-state: each allowed mint returned its child, key and grant');
|
|
56
|
+
select is((select count(*) from agent_board.session_id_aliases where root_agent_id = (select root_id from _sid_mint_root)), 5::bigint,
|
|
57
|
+
'EC-SIDSCHEMA-1-reservation-aliases: every initial mint writes its reservation');
|
|
58
|
+
select is((select count(*) from agent_board.session_identities si join _sid_children c on c.agent_id = si.agent_id
|
|
59
|
+
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 = 'alias-' || c.label), 5::bigint,
|
|
60
|
+
'EC-SIDSCHEMA-1-mint-reservation-pointer: every real session mint stores its specific reserved alias');
|
|
61
|
+
select is((select count(*) from agent_board.session_identities where root_agent_id = (select root_id from _sid_mint_root) and identity_kind = 'session' and holder_secret_hash is null), 5::bigint,
|
|
62
|
+
'EC-SIDSCHEMA-1-reservation-unclaimed: child mint does not create a holder lease');
|
|
63
|
+
|
|
64
|
+
-- Claim the two lease-bearing cases through real key challenges and token mint.
|
|
65
|
+
update agent_board.session_identities set approved_risky_digest = 'approved', current_risky_digest = 'approved', resolution_state = 'active'
|
|
66
|
+
where agent_id in (select agent_id from _sid_children where label in ('live', 'lapsed'));
|
|
67
|
+
create temp table _sid_res_tokens(agent_id uuid, epoch bigint);
|
|
68
|
+
grant insert on _sid_res_tokens to service_role;
|
|
69
|
+
create function pg_temp.sid_claim_reservations()
|
|
70
|
+
returns void language plpgsql as $$
|
|
71
|
+
declare v_child record; v_challenge record;
|
|
72
|
+
begin
|
|
73
|
+
for v_child in select * from _sid_children where label in ('live', 'lapsed') order by label loop
|
|
74
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_child.agent_id, 'delegated-ephemeral');
|
|
75
|
+
insert into _sid_res_tokens
|
|
76
|
+
select v_child.agent_id, m.identity_lease_epoch from agent_board.mint_fleet_agent_session(
|
|
77
|
+
v_challenge.challenge_id, split_part(v_challenge.challenge, ':', 3), v_child.key_id, v_child.agent_id,
|
|
78
|
+
900, null, 'alias-' || v_child.label) m;
|
|
79
|
+
end loop;
|
|
80
|
+
end;
|
|
81
|
+
$$;
|
|
82
|
+
grant execute on function pg_temp.sid_claim_reservations() to service_role;
|
|
83
|
+
set local role service_role;
|
|
84
|
+
select lives_ok('select pg_temp.sid_claim_reservations()', 'EC-SIDSCHEMA-1-reservation-claims: proven child keys claim the live and lapsed fixtures');
|
|
85
|
+
reset role;
|
|
86
|
+
select is((select count(*) from _sid_res_tokens where epoch = 0), 2::bigint,
|
|
87
|
+
'EC-SIDSCHEMA-1-reservation-claims-state: two first claims start epoch-zero leases');
|
|
88
|
+
update agent_board.session_identities set last_heartbeat = now() - interval '180 seconds'
|
|
89
|
+
where agent_id = (select agent_id from _sid_children where label = 'lapsed');
|
|
90
|
+
update agent_board.session_identities set created_at = now() - interval '180 seconds'
|
|
91
|
+
where agent_id in (select agent_id from _sid_children where label in ('revoked', 'stale'));
|
|
92
|
+
select agent_board._revoke_agent_principal((select agent_id from _sid_children where label = 'revoked'), (select root_id from _sid_mint_root), 'reservation fixture revoked');
|
|
93
|
+
update agent_board.session_identities set tombstoned_at = now(), tombstone_reason = 'revoked'
|
|
94
|
+
where agent_id = (select agent_id from _sid_children where label = 'revoked');
|
|
95
|
+
set local role service_role;
|
|
96
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_child('duplicate-live', 'alias-live')$q$),
|
|
97
|
+
'42501', 'duplicate_live_session', 'EC-SIDSCHEMA-1-reservation-live: duplicate live reservation refuses by name');
|
|
98
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_child('duplicate-young', 'alias-young')$q$),
|
|
99
|
+
'42501', 'identity_lease_live', 'EC-SIDSCHEMA-1-reservation-young: younger-than-180-second reservation is not superseded');
|
|
100
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_child('duplicate-lapsed', 'alias-lapsed')$q$),
|
|
101
|
+
'42501', 'identity_lease_lapsed', 'EC-SIDSCHEMA-1-reservation-lapsed: claimed lapsed identity must be reclaimed, not re-minted');
|
|
102
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_child('duplicate-revoked', 'alias-revoked')$q$),
|
|
103
|
+
'42501', 'identity_tombstoned', 'EC-SIDSCHEMA-1-reservation-revoked: revoked unclaimed reservation cannot be superseded');
|
|
104
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_child('missing-id', null)$q$),
|
|
105
|
+
'22023', 'unattributed_session_refused', 'EC-SIDSCHEMA-1-reservation-id-required: session-form mint must supply its session id');
|
|
106
|
+
select lives_ok($q$select pg_temp.sid_child('replacement', 'alias-stale')$q$,
|
|
107
|
+
'EC-SIDSCHEMA-1-reservation-stale: exactly 180-second-old unclaimed reservation is superseded');
|
|
108
|
+
reset role;
|
|
109
|
+
select is((select count(*) from _sid_children where label = 'replacement'), 1::bigint,
|
|
110
|
+
'EC-SIDSCHEMA-1-reservation-replacement: stale replacement returns a new principal');
|
|
111
|
+
select is((select tombstone_reason from agent_board.session_identities where agent_id = (select agent_id from _sid_children where label = 'stale')), 'superseded_unclaimed',
|
|
112
|
+
'EC-SIDSCHEMA-1-reservation-tombstone: old unclaimed identity is permanently tombstoned');
|
|
113
|
+
select is((select status from agent_board.agents where id = (select agent_id from _sid_children where label = 'stale')), 'revoked',
|
|
114
|
+
'EC-SIDSCHEMA-1-reservation-principal: supersede writes through to agent status');
|
|
115
|
+
select is((select status || '/' || proof_state from agent_board.agent_signing_keys where id = (select key_id from _sid_children where label = 'stale')), 'revoked/revoked',
|
|
116
|
+
'EC-SIDSCHEMA-1-reservation-key: supersede revokes the old child key');
|
|
117
|
+
select ok((select revoked_at is not null from agent_board.delegation_grants where child_agent_id = (select agent_id from _sid_children where label = 'stale')),
|
|
118
|
+
'EC-SIDSCHEMA-1-reservation-grant: supersede revokes the old grant');
|
|
119
|
+
select is((select identity_agent_id from agent_board.session_id_aliases where root_agent_id = (select root_id from _sid_mint_root) and session_id = 'alias-stale'),
|
|
120
|
+
(select agent_id from _sid_children where label = 'replacement'),
|
|
121
|
+
'EC-SIDSCHEMA-1-reservation-repoint: reserved alias resolves to the newly minted identity');
|
|
122
|
+
-- A separate root keeps reservation fixtures out of the bounded-authority census.
|
|
123
|
+
create temp table _sid_bound_root(root_id uuid, board_id uuid, suffix text);
|
|
124
|
+
create temp table _sid_bound_children(label text primary key, agent_id uuid, key_id uuid, expires_at timestamptz);
|
|
125
|
+
do $$
|
|
126
|
+
declare v_root uuid := gen_random_uuid(); v_board uuid := gen_random_uuid(); v_auth uuid;
|
|
127
|
+
begin
|
|
128
|
+
v_auth := public.ensure_github_user('sid-bound-' || v_root, 'sid-bound-' || v_root || '@example.test', 'sid-bound-' || v_root, 'Bound root');
|
|
129
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
|
|
130
|
+
values (v_root, 'agent', 'sid-bound-' || v_root, 'Bound root', 'active', v_auth);
|
|
131
|
+
insert into agent_board.session_identities(agent_id, root_agent_id, depth, approved_risky_digest, current_risky_digest)
|
|
132
|
+
values (v_root, v_root, 0, 'approved', 'approved');
|
|
133
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id)
|
|
134
|
+
values (v_board, 'sid-bound-' || v_root, 'Bound room', v_root);
|
|
135
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
|
|
136
|
+
values (v_board, v_root, 'joined', 'owner');
|
|
137
|
+
insert into _sid_bound_root values (v_root, v_board, substr(v_root::text, 1, 8));
|
|
138
|
+
end;
|
|
139
|
+
$$;
|
|
140
|
+
grant select on _sid_bound_root, _sid_bound_children to service_role;
|
|
141
|
+
grant insert on _sid_bound_children to service_role;
|
|
142
|
+
create function pg_temp.sid_bound(p_label text, p_parent text default null,
|
|
143
|
+
p_scopes jsonb default '["read"]', p_role text default null, p_fork text default null)
|
|
144
|
+
returns void language plpgsql as $$
|
|
145
|
+
declare v_parent uuid; v_root uuid; v_board uuid; v_suffix text; v_rooms jsonb; v_fork uuid;
|
|
146
|
+
begin
|
|
147
|
+
select root_id, board_id, suffix into v_root, v_board, v_suffix from _sid_bound_root;
|
|
148
|
+
v_parent := coalesce((select agent_id from _sid_bound_children where label = p_parent), v_root);
|
|
149
|
+
v_fork := (select agent_id from _sid_bound_children where label = p_fork);
|
|
150
|
+
if p_role is not null then v_rooms := jsonb_build_array(jsonb_build_object('board_id', v_board, 'role', p_role)); end if;
|
|
151
|
+
insert into _sid_bound_children
|
|
152
|
+
select p_label, m.child_agent_id, m.signing_key_id, m.grant_expires_at
|
|
153
|
+
from agent_board.mint_delegated_child(v_parent, 'sid-bound-' || p_label || '-' || v_suffix,
|
|
154
|
+
p_scopes, public.test_pubkey(v_suffix || p_label), interval '14 days', false,
|
|
155
|
+
'claude', 'bound-' || p_label, '0', v_rooms,
|
|
156
|
+
'{"resolution":"resolved","risky_digest":"approved","recorded":{"version":"one"}}', v_fork) m;
|
|
157
|
+
end;
|
|
158
|
+
$$;
|
|
159
|
+
grant execute on function pg_temp.sid_bound(text, text, jsonb, text, text) to service_role;
|
|
160
|
+
set local role service_role;
|
|
161
|
+
select lives_ok($q$select agent_board.upsert_delegation_policy((select root_id from _sid_bound_root), 'root_minted', '["read","write"]', 200, interval '14 days', true)$q$,
|
|
162
|
+
'EC-SIDSCHEMA-2-policy: root authority is configured through its sole writer');
|
|
163
|
+
reset role;
|
|
164
|
+
select is((select allowed_scopes from agent_board.delegation_policies where root_agent_id = (select root_id from _sid_bound_root)), '["read","write"]'::jsonb,
|
|
165
|
+
'EC-SIDSCHEMA-2-policy-state: explicit root scopes are stored');
|
|
166
|
+
set local role service_role;
|
|
167
|
+
select lives_ok($q$select pg_temp.sid_bound('parent', null, '["read"]', 'admin')$q$,
|
|
168
|
+
'EC-SIDSCHEMA-2-depth-one: approved root mints its explicit-room session');
|
|
169
|
+
reset role;
|
|
170
|
+
select is((select bm.role from agent_board.board_memberships bm where bm.agent_id = (select agent_id from _sid_bound_children where label = 'parent')), 'admin',
|
|
171
|
+
'EC-SIDSCHEMA-2-room-role: exactly the parent-chosen bounded role is written');
|
|
172
|
+
select is((select count(*) from agent_board.board_memberships bm where bm.agent_id = (select agent_id from _sid_bound_children where label = 'parent')), 1::bigint,
|
|
173
|
+
'EC-SIDSCHEMA-2-room-count: exactly the explicitly requested room is admitted');
|
|
174
|
+
select is((select expires_at from _sid_bound_children where label = 'parent'), now() + interval '7 days',
|
|
175
|
+
'EC-SIDSCHEMA-2-ttl: approved session lifetime is capped at seven days');
|
|
176
|
+
set local role service_role;
|
|
177
|
+
select lives_ok($q$select pg_temp.sid_bound('no-room')$q$, 'EC-SIDSCHEMA-2-no-room: omitting rooms is an allowed empty request');
|
|
178
|
+
reset role;
|
|
179
|
+
select is((select count(*) from agent_board.board_memberships where agent_id = (select agent_id from _sid_bound_children where label = 'no-room')), 0::bigint,
|
|
180
|
+
'EC-SIDSCHEMA-2-no-default-room: omitted rooms never create a default membership');
|
|
181
|
+
set local role service_role;
|
|
182
|
+
select lives_ok($q$select pg_temp.sid_bound('grandchild', 'parent', '["read"]', 'observer')$q$,
|
|
183
|
+
'EC-SIDSCHEMA-2-depth-two: depth-one session may mint a bounded sub-agent');
|
|
184
|
+
reset role;
|
|
185
|
+
select is((select depth from agent_board.session_identities where agent_id = (select agent_id from _sid_bound_children where label = 'grandchild')), 2::smallint,
|
|
186
|
+
'EC-SIDSCHEMA-2-depth-two-state: successful sub-agent is depth two');
|
|
187
|
+
select is((select root_agent_id from agent_board.agents where id = (select agent_id from _sid_bound_children where label = 'grandchild')), (select root_id from _sid_bound_root),
|
|
188
|
+
'EC-SIDSCHEMA-2-true-root: depth-two principal records the depth-zero root, not its parent');
|
|
189
|
+
select is((select parent_agent_id from agent_board.agents where id = (select agent_id from _sid_bound_children where label = 'grandchild')), (select agent_id from _sid_bound_children where label = 'parent'),
|
|
190
|
+
'EC-SIDSCHEMA-2-parent: depth-two principal retains its immediate parent');
|
|
191
|
+
set local role service_role;
|
|
192
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('third', 'grandchild')$q$),
|
|
193
|
+
'42501', 'identity_depth_exceeded', 'EC-SIDSCHEMA-2-depth-three: depth three refuses by name');
|
|
194
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('inflated-role', 'parent', '["read"]', 'owner')$q$),
|
|
195
|
+
'42501', 'room_authority_exceeded', 'EC-SIDSCHEMA-2-role-bound: sub-agent cannot outrank its parent');
|
|
196
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('inflated-scope', 'parent', '["write"]')$q$),
|
|
197
|
+
'42501', 'scope_exceeded: requested scopes are not a subset of the root allowed_scopes', 'EC-SIDSCHEMA-2-scope-bound: sub-agent uses granted scopes, not root latitude');
|
|
198
|
+
select lives_ok($q$select pg_temp.sid_bound('fork', null, '["read"]', null, 'parent')$q$,
|
|
199
|
+
'EC-SIDSCHEMA-2-fork: a new related session may fork a live same-parent identity');
|
|
200
|
+
reset role;
|
|
201
|
+
select is((select forked_from from agent_board.session_identities where agent_id = (select agent_id from _sid_bound_children where label = 'fork')), (select agent_id from _sid_bound_children where label = 'parent'),
|
|
202
|
+
'EC-SIDSCHEMA-2-fork-state: new identity retains fork provenance');
|
|
203
|
+
-- A self-written depth-one policy must not expand the parent's granted scopes.
|
|
204
|
+
insert into agent_board.delegation_policies(root_agent_id, mode, allowed_scopes, child_ttl_ceiling)
|
|
205
|
+
select agent_id, 'root_minted', '["read","write"]', interval '14 days' from _sid_bound_children where label = 'parent';
|
|
206
|
+
set local role service_role;
|
|
207
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('self-scope', 'parent', '["write"]')$q$),
|
|
208
|
+
'42501', 'scope_exceeded: requested scopes are not a subset of the root allowed_scopes', 'EC-SIDSCHEMA-2-self-scope: self-written policy cannot widen a session grant');
|
|
209
|
+
reset role;
|
|
210
|
+
update agent_board.delegation_grants set grant_expires_at = now() where child_agent_id = (select agent_id from _sid_bound_children where label = 'parent');
|
|
211
|
+
set local role service_role;
|
|
212
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('expired-parent', 'parent')$q$),
|
|
213
|
+
'42501', 'identity_parent_inactive', 'EC-SIDSCHEMA-2-ancestor-expiry: expired parent grant refuses mint');
|
|
214
|
+
reset role;
|
|
215
|
+
update agent_board.delegation_grants set grant_expires_at = now() + interval '7 days', revoked_at = now()
|
|
216
|
+
where child_agent_id = (select agent_id from _sid_bound_children where label = 'parent');
|
|
217
|
+
set local role service_role;
|
|
218
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('revoked-parent', 'parent')$q$),
|
|
219
|
+
'42501', 'identity_parent_inactive', 'EC-SIDSCHEMA-2-ancestor-revoke: revoked parent grant refuses mint');
|
|
220
|
+
reset role;
|
|
221
|
+
update agent_board.delegation_grants set revoked_at = null where child_agent_id = (select agent_id from _sid_bound_children where label = 'parent');
|
|
222
|
+
update agent_board.session_identities set resolution_state = 'frozen' where agent_id = (select root_id from _sid_bound_root);
|
|
223
|
+
set local role service_role;
|
|
224
|
+
select throws_ok(format('select pg_temp.sid_child_refuse(%L)', $q$select pg_temp.sid_bound('frozen-root', 'parent')$q$),
|
|
225
|
+
'42501', 'identity_fingerprint_unapproved', 'EC-SIDSCHEMA-2-ancestor-freeze: frozen depth-zero ancestor refuses depth-two mint');
|
|
226
|
+
reset role;
|
|
227
|
+
update agent_board.session_identities set resolution_state = 'active' where agent_id = (select root_id from _sid_bound_root);
|
|
228
|
+
select is((select count(*) from _sid_bound_children), 4::bigint,
|
|
229
|
+
'EC-SIDSCHEMA-2-refusal-state: refused mints do not leave extra principals');
|
|
230
|
+
select is((select count(*) from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname = 'agent_board' and p.proname = 'mint_delegated_child'), 1::bigint,
|
|
231
|
+
'EC-SIDSCHEMA-2-single-mint: catalog contains exactly one child-mint signature');
|
|
232
|
+
select finish();
|
|
233
|
+
rollback;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
-- Production-path controls and independent review-regression refusals.
|
|
2
|
+
create extension if not exists pgtap with schema extensions;
|
|
3
|
+
set search_path=extensions,public,agent_board;
|
|
4
|
+
begin;
|
|
5
|
+
grant usage on schema extensions to authenticated,service_role;
|
|
6
|
+
select plan(39);
|
|
7
|
+
create temp table _sid_r1(label text primary key, agent_id uuid, auth_id uuid, key_id uuid);
|
|
8
|
+
create temp table _sid_r1_results(label text, agent_id uuid, session_id text);
|
|
9
|
+
do $$
|
|
10
|
+
declare v_label text; v_id uuid; v_auth uuid; v_owner uuid; v_key uuid;
|
|
11
|
+
begin
|
|
12
|
+
foreach v_label in array array['owner','root','bare'] loop
|
|
13
|
+
v_id:=gen_random_uuid(); v_key:=gen_random_uuid();
|
|
14
|
+
v_auth:=public.ensure_github_user('sid-r1-'||v_id,'sid-r1-'||v_id||'@example.test','sid-r1-'||v_id,'Review fixture');
|
|
15
|
+
if v_label='owner' then v_owner:=v_auth; end if;
|
|
16
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id,owner_user_id)
|
|
17
|
+
values(v_id,case when v_label='owner' then 'human' else 'agent' end,'sid-r1-'||v_id,'Review fixture','active',v_auth,v_owner);
|
|
18
|
+
insert into agent_board.session_identities(agent_id,root_agent_id,depth) values(v_id,v_id,0);
|
|
19
|
+
insert into agent_board.agent_signing_keys(id,agent_id,key_id,key_version,public_key,public_key_format,fingerprint,status,proof_state)
|
|
20
|
+
values(v_key,v_id,'primary',1,public.test_pubkey(v_key::text),'raw_ed25519_base64url',public.test_key_fingerprint(v_key::text),'active','proven');
|
|
21
|
+
insert into _sid_r1 values(v_label,v_id,v_auth,v_key);
|
|
22
|
+
end loop;
|
|
23
|
+
end;
|
|
24
|
+
$$;
|
|
25
|
+
-- A pre-commit principal can still have no identity row; this path stays additive.
|
|
26
|
+
delete from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='bare');
|
|
27
|
+
grant select,insert on _sid_r1,_sid_r1_results to service_role;
|
|
28
|
+
grant select on _sid_r1,_sid_r1_results to authenticated;
|
|
29
|
+
create function pg_temp.sid_r1_subject(p_label text) returns void language sql as $$
|
|
30
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r1 where label=p_label),true);
|
|
31
|
+
$$;
|
|
32
|
+
create function pg_temp.sid_r1_legacy(p_parent text,p_label text) returns void language plpgsql as $$
|
|
33
|
+
declare v_mint record;
|
|
34
|
+
begin
|
|
35
|
+
select * into v_mint from agent_board.mint_delegated_child((select agent_id from _sid_r1 where label=p_parent),
|
|
36
|
+
'sid-r1-'||p_label||'-'||gen_random_uuid(),'["read"]',public.test_pubkey(p_label),interval '1 hour');
|
|
37
|
+
insert into _sid_r1 select p_label,v_mint.child_agent_id,a.auth_user_id,v_mint.signing_key_id
|
|
38
|
+
from agent_board.agents a where a.id=v_mint.child_agent_id;
|
|
39
|
+
end;
|
|
40
|
+
$$;
|
|
41
|
+
create function pg_temp.sid_r1_token(p_label text) returns void language plpgsql as $$
|
|
42
|
+
declare v_fx record; v_challenge record;
|
|
43
|
+
begin
|
|
44
|
+
select * into v_fx from _sid_r1 where label=p_label;
|
|
45
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_fx.agent_id,
|
|
46
|
+
case when p_label in ('root','bare') then 'primary' else 'delegated-ephemeral' end);
|
|
47
|
+
insert into _sid_r1_results select p_label,v_fx.agent_id,m.session_id from agent_board.mint_fleet_agent_session(
|
|
48
|
+
v_challenge.challenge_id,split_part(v_challenge.challenge,':',3),v_fx.key_id,v_fx.agent_id) m;
|
|
49
|
+
end;
|
|
50
|
+
$$;
|
|
51
|
+
create function pg_temp.sid_r1_session(p_label text,p_alias text,p_fork text default null) returns void language plpgsql as $$
|
|
52
|
+
declare v_mint record;
|
|
53
|
+
begin
|
|
54
|
+
select * into v_mint from agent_board.mint_delegated_child((select agent_id from _sid_r1 where label='root'),
|
|
55
|
+
'sid-r1-'||p_label||'-'||gen_random_uuid(),'["read"]',public.test_pubkey(p_label),interval '1 hour',false,
|
|
56
|
+
'claude',p_alias,null,'[]','{"resolution":"resolved","risky_digest":"approved"}',
|
|
57
|
+
(select agent_id from _sid_r1 where label=p_fork));
|
|
58
|
+
insert into _sid_r1 select p_label,v_mint.child_agent_id,a.auth_user_id,v_mint.signing_key_id
|
|
59
|
+
from agent_board.agents a where a.id=v_mint.child_agent_id;
|
|
60
|
+
end;
|
|
61
|
+
$$;
|
|
62
|
+
create function pg_temp.sid_r1_refuse(p_sql text) returns void language plpgsql as $$
|
|
63
|
+
begin execute p_sql; raise exception 'expected refusal but RPC succeeded' using errcode='P0001'; end;
|
|
64
|
+
$$;
|
|
65
|
+
grant execute on function pg_temp.sid_r1_subject(text),pg_temp.sid_r1_refuse(text) to authenticated,service_role;
|
|
66
|
+
grant execute on function pg_temp.sid_r1_legacy(text,text),pg_temp.sid_r1_token(text),pg_temp.sid_r1_session(text,text,text) to service_role;
|
|
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 agent_id from _sid_r1 where label='root'),'root_minted','["read"]',200,interval '7 days',true);
|
|
71
|
+
select agent_board.upsert_delegation_policy((select agent_id from _sid_r1 where label='bare'),'root_minted','["read"]',200,interval '7 days',true)$q$,'R1-policy-control: both real legacy policies are configured');
|
|
72
|
+
reset role;
|
|
73
|
+
select is((select count(*) from agent_board.delegation_policies where root_agent_id in(select agent_id from _sid_r1 where label in('root','bare'))),2::bigint,'R1-policy-state: both policies exist');
|
|
74
|
+
set local role service_role;
|
|
75
|
+
select lives_ok($q$select pg_temp.sid_r1_legacy('root','legacy-child'); select pg_temp.sid_r1_legacy('bare','bare-child')$q$,'R1-legacy-child-control: never-frozen and absent identity rows preserve legacy child mint');
|
|
76
|
+
reset role;
|
|
77
|
+
select is((select count(*) from _sid_r1 where label in('legacy-child','bare-child')),2::bigint,'R1-legacy-child-state: both legacy children were returned');
|
|
78
|
+
set local role service_role;
|
|
79
|
+
select lives_ok($q$select pg_temp.sid_r1_token('root'); select pg_temp.sid_r1_token('bare')$q$,'R1-legacy-token-control: never-frozen and absent identity rows preserve token mint');
|
|
80
|
+
reset role;
|
|
81
|
+
select is((select count(*) from _sid_r1_results),2::bigint,'R1-legacy-token-state: both real PoP issuances returned tokens');
|
|
82
|
+
set local role service_role;
|
|
83
|
+
select pg_temp.sid_r1_subject('root');
|
|
84
|
+
select lives_ok($q$select agent_board.record_session_identity_state((select agent_id from _sid_r1 where label='root'),'{"resolution":"resolved","risky_digest":"changed"}')$q$,'R1-freeze-control: real recorded risky reading freezes a legacy parent');
|
|
85
|
+
reset role;
|
|
86
|
+
select is((select resolution_state from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='root')),'frozen','R1-freeze-state: root is explicitly frozen');
|
|
87
|
+
set local role service_role;
|
|
88
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select pg_temp.sid_r1_legacy('root','forbidden')$q$),'42501','identity_fingerprint_unapproved','R1-F001-legacy-child: frozen root cannot bypass approval through legacy child mint');
|
|
89
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select pg_temp.sid_r1_token('root')$q$),'42501','identity_fingerprint_unapproved','R1-F001-legacy-token: frozen root cannot bypass approval through legacy token mint');
|
|
90
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select pg_temp.sid_r1_token('legacy-child')$q$),'42501','identity_fingerprint_unapproved','R1-F001-ancestor-token: legacy child token checks its frozen ancestor');
|
|
91
|
+
reset role;
|
|
92
|
+
select is((select count(*) from _sid_r1_results),2::bigint,'R1-freeze-refusal-state: frozen issuances create no token result');
|
|
93
|
+
set local role service_role;
|
|
94
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select agent_board.record_session_identity_state((select agent_id from _sid_r1 where label='root'),'{"resolution":"Unresolved","risky_digest":"changed"}')$q$),'22023','invalid identity fingerprint resolution','R1-N1-record-vocabulary: out-of-vocabulary reading is refused');
|
|
95
|
+
reset role;
|
|
96
|
+
select is((select recorded_fingerprint->>'resolution' from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='root')),'resolved','R1-N1-record-state: bad spelling does not replace the reading');
|
|
97
|
+
-- Historical/malformed stored metadata must not be laundered by a fresh approval argument.
|
|
98
|
+
update agent_board.session_identities set recorded_fingerprint='{"resolution":"Unresolved"}' where agent_id=(select agent_id from _sid_r1 where label='root');
|
|
99
|
+
set local role service_role;
|
|
100
|
+
select pg_temp.sid_r1_subject('owner');
|
|
101
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select agent_board.approve_identity_state((select agent_id from _sid_r1 where label='root'),'changed','{"resolution":"resolved"}')$q$),'42501','identity_fingerprint_unapproved','R1-N1-approval-exact: any stored state other than exactly resolved refuses approval');
|
|
102
|
+
reset role;
|
|
103
|
+
update agent_board.session_identities set recorded_fingerprint='{"resolution":"resolved"}' where agent_id=(select agent_id from _sid_r1 where label='root');
|
|
104
|
+
set local role service_role;
|
|
105
|
+
select lives_ok($q$select agent_board.approve_identity_state((select agent_id from _sid_r1 where label='root'),'changed','{"resolution":"resolved"}')$q$,'R1-owner-control: unrevoked human approves resolved reading');
|
|
106
|
+
reset role;
|
|
107
|
+
select is((select resolution_state from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='root')),'active','R1-owner-control-state: approval thaws the identity');
|
|
108
|
+
-- Matching digest for session fixtures; still no implicit lease on the legacy root.
|
|
109
|
+
update agent_board.session_identities set approved_risky_digest='approved',current_risky_digest='approved' where agent_id=(select agent_id from _sid_r1 where label='root');
|
|
110
|
+
set local role service_role;
|
|
111
|
+
select lives_ok($q$select pg_temp.sid_r1_session('unclaimed','r1-unclaimed'); select pg_temp.sid_r1_session('fork','r1-fork','unclaimed')$q$,'R1-N2-fork-control: real mint creates an unclaimed reservation and related fork');
|
|
112
|
+
reset role;
|
|
113
|
+
update agent_board.session_identities set holder_secret_hash=digest('fork-holder','sha256'),last_heartbeat=now() where agent_id=(select agent_id from _sid_r1 where label='fork');
|
|
114
|
+
update agent_board.session_identities set created_at=now()-interval '181 seconds' where agent_id=(select agent_id from _sid_r1 where label='unclaimed');
|
|
115
|
+
select ok((select agent_board._session_identity_lease_live(agent_id) from _sid_r1 where label='fork'),'R1-N2-live-fork-state: fork has a live lease before supersede');
|
|
116
|
+
set local role service_role;
|
|
117
|
+
select lives_ok($q$select pg_temp.sid_r1_session('replacement','r1-unclaimed')$q$,'R1-N2-supersede-control: conflicting mint supersedes the stale unclaimed reservation');
|
|
118
|
+
reset role;
|
|
119
|
+
select is((select tombstone_reason from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='unclaimed')),'superseded_unclaimed','R1-N2-superseded-state: only the stale reservation is superseded');
|
|
120
|
+
select ok((select si.tombstoned_at is null and a.status='active' and si.credential_epoch=0 and g.revoked_at is null
|
|
121
|
+
from _sid_r1 f join agent_board.session_identities si on si.agent_id=f.agent_id join agent_board.agents a on a.id=f.agent_id
|
|
122
|
+
join agent_board.delegation_grants g on g.child_agent_id=f.agent_id where f.label='fork'),'R1-N2-fork-survives: supersede leaves a live fork and its credentials intact');
|
|
123
|
+
select set_config('request.jwt.claim.role','authenticated',true);
|
|
124
|
+
set local role authenticated;
|
|
125
|
+
select lives_ok($q$select agent_board.revoke_session_identity((select agent_id from _sid_r1 where label='replacement'))$q$,'R1-revoke-control: real owner revoke remains authorised');
|
|
126
|
+
reset role;
|
|
127
|
+
-- Real revocation of the old source must still traverse its surviving fork.
|
|
128
|
+
set local role authenticated;
|
|
129
|
+
select lives_ok($q$select agent_board.revoke_session_identity((select agent_id from _sid_r1 where label='unclaimed'))$q$,'R1-N2-real-revoke-control: ordinary revoke still closes related forks');
|
|
130
|
+
reset role;
|
|
131
|
+
select ok((select tombstoned_at is not null from agent_board.session_identities where agent_id=(select agent_id from _sid_r1 where label='fork')),'R1-N2-real-revoke-state: fork is tombstoned by real revoke');
|
|
132
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
133
|
+
set local role service_role;
|
|
134
|
+
select pg_temp.sid_r1_subject('root');
|
|
135
|
+
select lives_ok($q$select * from agent_board.revoke_fleet_subject('agent',(select agent_id from _sid_r1 where label='owner'),(select agent_id from _sid_r1 where label='root'),'review owner revocation')$q$,'R1-ledger-control: real fleet revocation permanently revokes the human owner');
|
|
136
|
+
reset role;
|
|
137
|
+
select ok((select a.status='active' and agent_board._actor_fleet_revoked(a.id) from agent_board.agents a join _sid_r1 f on f.agent_id=a.id where f.label='owner'),'R1-ledger-state: legacy human stays active while permanently ledger-revoked');
|
|
138
|
+
set local role service_role;
|
|
139
|
+
select pg_temp.sid_r1_subject('owner');
|
|
140
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select agent_board.approve_identity_state((select agent_id from _sid_r1 where label='root'),'approved','{"resolution":"resolved"}')$q$),'42501','fleet_revoked: approve_identity_state is not permitted for a fleet-revoked agent','R1-F002-approve-ledger: ledger-revoked human owner cannot approve');
|
|
141
|
+
reset role;
|
|
142
|
+
select set_config('request.jwt.claim.role','authenticated',true);
|
|
143
|
+
set local role authenticated;
|
|
144
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select agent_board.rename_agent((select agent_id from _sid_r1 where label='root'),'forbidden')$q$),'42501','fleet_revoked: rename_agent is not permitted for a fleet-revoked agent','R1-F002-rename-ledger: ledger-revoked human owner cannot rename');
|
|
145
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select agent_board.revoke_session_identity((select agent_id from _sid_r1 where label='root'))$q$),'42501','fleet_revoked: revoke_session_identity is not permitted for a fleet-revoked agent','R1-F002-revoke-ledger: ledger-revoked human owner cannot revoke');
|
|
146
|
+
reset role;
|
|
147
|
+
select is((select display_name from agent_board.agents where id=(select agent_id from _sid_r1 where label='root')),'Review fixture','R1-ledger-refusal-state: refused rename leaves the target untouched');
|
|
148
|
+
|
|
149
|
+
-- The class is derived from bodies/call closure, not an authored RPC name list.
|
|
150
|
+
-- Scanner floor: recognizes _session_identity_owner calls or the exact inline
|
|
151
|
+
-- human.agent_type = 'human' spelling plus auth.uid. Differently named aliases,
|
|
152
|
+
-- joins or ownership helpers must extend this scanner; it does not prove
|
|
153
|
+
-- arbitrary control flow. Panel-r2 adds a positive authorization-condition pin.
|
|
154
|
+
create temp view _sid_r1_owner_paths as
|
|
155
|
+
select p.oid,p.proname,agent_board_test.stripped_body(p.oid) body
|
|
156
|
+
from pg_proc p where p.pronamespace='agent_board'::regnamespace and p.prokind='f'
|
|
157
|
+
and (agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_session_identity_owner')
|
|
158
|
+
or (agent_board_test.stripped_body(p.oid,true) like '%human.agent_type = ''human''%'
|
|
159
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('auth.uid',false)))
|
|
160
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.re(
|
|
161
|
+
agent_board_test.write_frag('agents',agent_board_test.bare_ok(p.oid)) || '|'
|
|
162
|
+
|| agent_board_test.write_frag('session_identities',agent_board_test.bare_ok(p.oid)) || '|'
|
|
163
|
+
|| agent_board_test.call_frag('_revoke_agent_principal'));
|
|
164
|
+
select ok((select count(*)>0 from _sid_r1_owner_paths),'R1-owner-class-populated: owner-path writers are discovered from the catalog');
|
|
165
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _sid_r1_owner_paths
|
|
166
|
+
where not exists(select 1 from agent_board_test.fn_call_closure() c
|
|
167
|
+
where c.root_oid=oid and c.reached_oid='agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure)),
|
|
168
|
+
'','R1-owner-class: every discovered human-owner writer reaches locked permanent-ledger refusal');
|
|
169
|
+
select ok(agent_board_test.stripped_body('agent_board._session_identity_owner(uuid)'::regprocedure,true) like '%human.agent_type = ''human''%'
|
|
170
|
+
and agent_board_test.stripped_body('agent_board._session_identity_owner(uuid)'::regprocedure,true) like '%human.status = ''active''%'
|
|
171
|
+
and agent_board_test.stripped_body('agent_board._session_identity_owner(uuid)'::regprocedure) like '%root.owner_user_id = auth.uid()%'
|
|
172
|
+
and pg_get_function_result('agent_board._session_identity_owner(uuid)'::regprocedure)='boolean',
|
|
173
|
+
'R1-owner-predicate-positive: exempt predicate returns only a human ownership boolean, not an authority mutation');
|
|
174
|
+
select ok(position('current_agent_id()' in agent_board_test.stripped_body('agent_board.heartbeat_session_identity(uuid)'::regprocedure))
|
|
175
|
+
< position('for update' in agent_board_test.stripped_body('agent_board.heartbeat_session_identity(uuid)'::regprocedure)),
|
|
176
|
+
'R1-heartbeat-authorise-first: resolver authorisation precedes the first identity lock');
|
|
177
|
+
select ok(position('_session_identity_owner(' in agent_board_test.stripped_body('agent_board.revoke_session_identity(uuid)'::regprocedure))
|
|
178
|
+
< position('for update' in agent_board_test.stripped_body('agent_board.revoke_session_identity(uuid)'::regprocedure)),
|
|
179
|
+
'R1-revoke-authorise-first: owner/ancestor authorisation precedes the first target lock');
|
|
180
|
+
select ok(agent_board_test.stripped_body('agent_board.revoke_agent(uuid,text)'::regprocedure,true) like '%revocation requires a resolved actor%'
|
|
181
|
+
and agent_board_test.stripped_body('agent_board.revoke_agent(uuid,text)'::regprocedure) not like '%coalesce(agent_board.current_agent_id(), p_agent_id)%',
|
|
182
|
+
'R1-admin-revoker: unresolved platform-admin principal refuses rather than blaming the target');
|
|
183
|
+
create temp table _sid_r1_admin(auth_id uuid);
|
|
184
|
+
insert into _sid_r1_admin select public.ensure_github_user('sid-r1-admin-'||gen_random_uuid(),'sid-r1-admin@example.test','sid-r1-admin-'||gen_random_uuid(),'Unresolved admin');
|
|
185
|
+
insert into public.user_org_roles(user_id,role) select auth_id,'platform_admin' from _sid_r1_admin;
|
|
186
|
+
grant select on _sid_r1_admin to authenticated;
|
|
187
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r1_admin),true);
|
|
188
|
+
set local role authenticated;
|
|
189
|
+
select throws_ok(format('select pg_temp.sid_r1_refuse(%L)',$q$select * from agent_board.revoke_agent((select agent_id from _sid_r1 where label='root'),'review unresolved admin')$q$),
|
|
190
|
+
'42501','revocation requires a resolved actor','R1-admin-unresolved: actual admin without a board principal cannot blame the target');
|
|
191
|
+
reset role;
|
|
192
|
+
select is((select status from agent_board.agents where id=(select agent_id from _sid_r1 where label='root')),'active','R1-admin-unresolved-state: unresolved actor refusal leaves principal active');
|
|
193
|
+
select finish();
|
|
194
|
+
rollback;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
-- Real API compatibility controls; no existing suite inputs are accommodated.
|
|
2
|
+
create extension if not exists pgtap with schema extensions;
|
|
3
|
+
set search_path=extensions,public,agent_board;
|
|
4
|
+
begin;
|
|
5
|
+
grant usage on schema extensions to authenticated,service_role;
|
|
6
|
+
select plan(28);
|
|
7
|
+
create temp table _sid_r2(label text primary key,agent_id uuid,auth_id uuid,key_id uuid);
|
|
8
|
+
create temp table _sid_r2_results(label text,status text,revoked_count integer);
|
|
9
|
+
create temp table _sid_r2_challenge(challenge_id uuid,nonce text);
|
|
10
|
+
do $$
|
|
11
|
+
declare v_label text; v_id uuid; v_auth uuid; v_owner uuid;
|
|
12
|
+
begin
|
|
13
|
+
foreach v_label in array array['owner','parent','service-legacy','admin-legacy'] loop
|
|
14
|
+
v_id:=gen_random_uuid();
|
|
15
|
+
v_auth:=public.ensure_github_user('sid-r2-'||v_id,'sid-r2-'||v_id||'@example.test','sid-r2-'||v_id,'Review fixture');
|
|
16
|
+
if v_label='owner' then v_owner:=v_auth; end if;
|
|
17
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id,owner_user_id)
|
|
18
|
+
values(v_id,case when v_label='owner' then 'human' else 'agent' end,'sid-r2-'||v_id,'Review fixture','active',v_auth,v_owner);
|
|
19
|
+
insert into agent_board.session_identities(agent_id,root_agent_id,depth,approved_risky_digest,current_risky_digest,recorded_fingerprint)
|
|
20
|
+
values(v_id,v_id,0,'approved','approved','{"resolution":"resolved"}');
|
|
21
|
+
insert into _sid_r2 values(v_label,v_id,v_auth,null);
|
|
22
|
+
end loop;
|
|
23
|
+
insert into agent_board.agent_credentials(agent_id,refresh_token_hash,hash_version,credential_label,expires_at)
|
|
24
|
+
select agent_id,digest(label,'sha256'),1,'review control',now()+interval '1 hour'
|
|
25
|
+
from _sid_r2 where label in('service-legacy','admin-legacy');
|
|
26
|
+
end;
|
|
27
|
+
$$;
|
|
28
|
+
grant select,insert on _sid_r2,_sid_r2_results,_sid_r2_challenge to service_role;
|
|
29
|
+
grant select,insert on _sid_r2_results to authenticated;
|
|
30
|
+
grant select on _sid_r2 to authenticated;
|
|
31
|
+
create function pg_temp.sid_r2_refuse(p_sql text) returns void language plpgsql as $$
|
|
32
|
+
begin execute p_sql; raise exception 'expected refusal but RPC succeeded' using errcode='P0001'; end;
|
|
33
|
+
$$;
|
|
34
|
+
grant execute on function pg_temp.sid_r2_refuse(text) to authenticated,service_role;
|
|
35
|
+
create function pg_temp.sid_r2_mint(p_label text,p_session boolean) returns void language plpgsql as $$
|
|
36
|
+
declare v_mint record;
|
|
37
|
+
begin
|
|
38
|
+
select * into v_mint from agent_board.mint_delegated_child((select agent_id from _sid_r2 where label='parent'),
|
|
39
|
+
'sid-r2-'||p_label||'-'||gen_random_uuid(),'["read"]',public.test_pubkey(p_label),interval '1 hour',false,
|
|
40
|
+
case when p_session then 'claude' end,case when p_session then 'r2-'||p_label end,null,'[]',
|
|
41
|
+
case when p_session then '{"resolution":"resolved","risky_digest":"approved"}'::jsonb end);
|
|
42
|
+
insert into _sid_r2 select p_label,v_mint.child_agent_id,a.auth_user_id,v_mint.signing_key_id
|
|
43
|
+
from agent_board.agents a where a.id=v_mint.child_agent_id;
|
|
44
|
+
end;
|
|
45
|
+
$$;
|
|
46
|
+
grant execute on function pg_temp.sid_r2_mint(text,boolean) to service_role;
|
|
47
|
+
select set_config('request.jwt.claims','{}',true);
|
|
48
|
+
select set_config('request.jwt.claim.sub','',true);
|
|
49
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
50
|
+
set local role service_role;
|
|
51
|
+
select lives_ok($q$insert into _sid_r2_results select 'service',status,revoked_credential_count from agent_board.revoke_agent((select agent_id from _sid_r2 where label='service-legacy'),'operator script')$q$,
|
|
52
|
+
'R2-F001-service-legacy: service_role without sub still revokes a legacy target');
|
|
53
|
+
reset role;
|
|
54
|
+
select ok((select a.status='suspended' and r.status='suspended' and r.revoked_count=1
|
|
55
|
+
and c.revoked_at is not null and a.metadata->>'revocationReason'='operator script'
|
|
56
|
+
from _sid_r2 f join agent_board.agents a on a.id=f.agent_id join agent_board.agent_credentials c on c.agent_id=a.id
|
|
57
|
+
join _sid_r2_results r on r.label='service' where f.label='service-legacy'),
|
|
58
|
+
'R2-F001-service-state: suspension, returned credential count and credential revocation remain legacy-compatible');
|
|
59
|
+
create temp table _sid_r2_admin(auth_id uuid);
|
|
60
|
+
insert into _sid_r2_admin select public.ensure_github_user('sid-r2-admin-'||gen_random_uuid(),'sid-r2-admin@example.test','sid-r2-admin-'||gen_random_uuid(),'Unresolved admin');
|
|
61
|
+
insert into public.user_org_roles(user_id,role) select auth_id,'platform_admin' from _sid_r2_admin;
|
|
62
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2_admin),true);
|
|
63
|
+
select set_config('request.jwt.claim.role','authenticated',true);
|
|
64
|
+
set local role authenticated;
|
|
65
|
+
select lives_ok($q$insert into _sid_r2_results select 'admin',status,revoked_credential_count from agent_board.revoke_agent((select agent_id from _sid_r2 where label='admin-legacy'),'Portal admin')$q$,
|
|
66
|
+
'R2-F001-admin-legacy: Portal platform admin without a board principal still revokes a legacy target');
|
|
67
|
+
reset role;
|
|
68
|
+
select ok((select a.status='suspended' and r.status='suspended' and r.revoked_count=1 and c.revoked_at is not null
|
|
69
|
+
from _sid_r2 f join agent_board.agents a on a.id=f.agent_id join agent_board.agent_credentials c on c.agent_id=a.id
|
|
70
|
+
join _sid_r2_results r on r.label='admin' where f.label='admin-legacy'),
|
|
71
|
+
'R2-F001-admin-state: the real Portal admin control suspends and revokes credentials');
|
|
72
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
73
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='parent'),true);
|
|
74
|
+
set local role service_role;
|
|
75
|
+
select lives_ok($q$select agent_board.upsert_delegation_policy((select agent_id from _sid_r2 where label='parent'),'root_minted','["read"]',200,interval '7 days',true)$q$,
|
|
76
|
+
'R2-policy-control: real delegation policy admits the review fixtures');
|
|
77
|
+
reset role;
|
|
78
|
+
select ok(exists(select 1 from agent_board.delegation_policies where root_agent_id=(select agent_id from _sid_r2 where label='parent')),
|
|
79
|
+
'R2-policy-state: configured policy exists');
|
|
80
|
+
set local role service_role;
|
|
81
|
+
select lives_ok($q$select pg_temp.sid_r2_mint('legacy-child',false)$q$,'R2-ancestor-mint-control: real legacy child mint succeeds');
|
|
82
|
+
reset role;
|
|
83
|
+
select ok(exists(select 1 from _sid_r2 where label='legacy-child'),'R2-ancestor-mint-state: real legacy child result exists');
|
|
84
|
+
update agent_board.session_identities set current_risky_digest='approved',resolution_state='frozen',recorded_fingerprint='{"resolution":"resolved"}'
|
|
85
|
+
where agent_id=(select agent_id from _sid_r2 where label='legacy-child');
|
|
86
|
+
set local role service_role;
|
|
87
|
+
select lives_ok($q$select agent_board.approve_identity_state((select agent_id from _sid_r2 where label='legacy-child'),'approved','{"resolution":"resolved"}')$q$,
|
|
88
|
+
'R2-ancestor-approval-control: unrevoked matching ancestor approves a legacy descendant');
|
|
89
|
+
reset role;
|
|
90
|
+
select is((select resolution_state from agent_board.session_identities where agent_id=(select agent_id from _sid_r2 where label='legacy-child')),'active',
|
|
91
|
+
'R2-ancestor-approval-state: the allow control really thaws the descendant');
|
|
92
|
+
set local role service_role;
|
|
93
|
+
select lives_ok($q$select pg_temp.sid_r2_mint('self',true); select pg_temp.sid_r2_mint('frozen',true)$q$,'R2-session-control: both real session identities are minted');
|
|
94
|
+
reset role;
|
|
95
|
+
select is((select count(*) from _sid_r2 where label in('self','frozen')),2::bigint,'R2-session-state: both session results exist');
|
|
96
|
+
set local role service_role;
|
|
97
|
+
insert into _sid_r2_challenge select challenge_id,split_part(challenge,':',3)
|
|
98
|
+
from agent_board.issue_signing_key_challenge((select agent_id from _sid_r2 where label='frozen'),'delegated-ephemeral');
|
|
99
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='self'),true);
|
|
100
|
+
select set_config('request.jwt.claim.identity_lease_epoch','0',true);
|
|
101
|
+
reset role;
|
|
102
|
+
select set_config('request.jwt.claim.role','authenticated',true);
|
|
103
|
+
set local role authenticated;
|
|
104
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select * from agent_board.revoke_fleet_subject('agent',(select agent_id from _sid_r2 where label='self'),(select agent_id from _sid_r2 where label='self'),'superseded_unclaimed')$q$),
|
|
105
|
+
'22023','superseded_unclaimed is reserved for the supersede path','R2-N2-2-self-reason: self revocation cannot forge the reserved supersede reason');
|
|
106
|
+
reset role;
|
|
107
|
+
select ok((select tombstoned_at is null from agent_board.session_identities where agent_id=(select agent_id from _sid_r2 where label='self')),
|
|
108
|
+
'R2-N2-2-reason-state: rejected reason leaves the identity live');
|
|
109
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
110
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='owner'),true);
|
|
111
|
+
set local role service_role;
|
|
112
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select * from agent_board.revoke_agent((select agent_id from _sid_r2 where label='self'),'superseded_unclaimed')$q$),
|
|
113
|
+
'22023','superseded_unclaimed is reserved for the supersede path','R2-N2-2-admin-reason: admin revocation cannot forge the reserved reason');
|
|
114
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select * from agent_board.revoke_agent_subtree((select agent_id from _sid_r2 where label='self'),(select agent_id from _sid_r2 where label='owner'),'superseded_unclaimed')$q$),
|
|
115
|
+
'22023','superseded_unclaimed is reserved for the supersede path','R2-N2-2-subtree-reason: service subtree revocation cannot forge the reserved reason');
|
|
116
|
+
reset role;
|
|
117
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='self'),true);
|
|
118
|
+
select set_config('request.jwt.claim.role','authenticated',true);
|
|
119
|
+
set local role authenticated;
|
|
120
|
+
select lives_ok($q$select * from agent_board.revoke_fleet_subject('agent',(select agent_id from _sid_r2 where label='self'),(select agent_id from _sid_r2 where label='self'),'ordinary self revocation')$q$,
|
|
121
|
+
'R2-self-revoke-control: ordinary self-revocation remains supported');
|
|
122
|
+
reset role;
|
|
123
|
+
select is((select tombstone_reason from agent_board.session_identities where agent_id=(select agent_id from _sid_r2 where label='self')),'revoked',
|
|
124
|
+
'R2-self-revoke-state: ordinary self-revocation is not mislabelled as supersede');
|
|
125
|
+
update agent_board.session_identities set resolution_state='frozen' where agent_id=(select agent_id from _sid_r2 where label='frozen');
|
|
126
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='owner'),true);
|
|
127
|
+
set local role authenticated;
|
|
128
|
+
select lives_ok($q$select agent_board.revoke_session_identity((select agent_id from _sid_r2 where label='frozen'))$q$,
|
|
129
|
+
'R2-frozen-revoke-control: human owner revokes an explicitly frozen session');
|
|
130
|
+
reset role;
|
|
131
|
+
select ok((select resolution_state='frozen' and tombstoned_at is not null from agent_board.session_identities where agent_id=(select agent_id from _sid_r2 where label='frozen')),
|
|
132
|
+
'R2-frozen-revoke-state: the target is both frozen and tombstoned');
|
|
133
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
134
|
+
set local role service_role;
|
|
135
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select * from agent_board.mint_fleet_agent_session((select challenge_id from _sid_r2_challenge),(select nonce from _sid_r2_challenge),(select key_id from _sid_r2 where label='frozen'),(select agent_id from _sid_r2 where label='frozen'),900,null,'r2-frozen')$q$),
|
|
136
|
+
'42501','identity_tombstoned','R2-N2-1-token-precedence: tombstone takes precedence over fingerprint freeze at token mint');
|
|
137
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select * from agent_board.mint_delegated_child((select agent_id from _sid_r2 where label='frozen'),'r2-forbidden','["read"]',public.test_pubkey('r2-forbidden'),interval '1 hour',false,'claude','r2-forbidden','0','[]','{"resolution":"resolved","risky_digest":"approved"}')$q$),
|
|
138
|
+
'42501','identity_tombstoned','R2-N2-1-child-precedence: tombstone takes precedence over fingerprint freeze at child mint');
|
|
139
|
+
select set_config('request.jwt.claim.sub',(select auth_id::text from _sid_r2 where label='parent'),true);
|
|
140
|
+
select lives_ok($q$select * from agent_board.revoke_fleet_subject('agent',(select agent_id from _sid_r2 where label='parent'),(select agent_id from _sid_r2 where label='parent'),'ancestor ledger revocation')$q$,
|
|
141
|
+
'R2-ancestor-ledger-control: real fleet self-revocation records the ancestor permanently');
|
|
142
|
+
reset role;
|
|
143
|
+
update agent_board.session_identities set resolution_state='frozen' where agent_id=(select agent_id from _sid_r2 where label='legacy-child');
|
|
144
|
+
select ok((select a.status='active' and si.tombstoned_at is null and agent_board._actor_fleet_revoked(a.id)
|
|
145
|
+
from _sid_r2 f join agent_board.agents a on a.id=f.agent_id join agent_board.session_identities si on si.agent_id=a.id where f.label='parent'),
|
|
146
|
+
'R2-ancestor-ledger-state: active legacy ancestor is permanently revoked without physically closing its legacy descendant');
|
|
147
|
+
set local role service_role;
|
|
148
|
+
select throws_ok(format('select pg_temp.sid_r2_refuse(%L)',$q$select agent_board.approve_identity_state((select agent_id from _sid_r2 where label='legacy-child'),'approved','{"resolution":"resolved"}')$q$),
|
|
149
|
+
'42501','fleet_revoked: approve_identity_state is not permitted for a fleet-revoked agent','R2-N2-3-ancestor-ledger: ledger-revoked ancestor cannot approve');
|
|
150
|
+
reset role;
|
|
151
|
+
select is((select resolution_state from agent_board.session_identities where agent_id=(select agent_id from _sid_r2 where label='legacy-child')),'frozen',
|
|
152
|
+
'R2-ancestor-refusal-state: refused approval leaves the descendant frozen');
|
|
153
|
+
-- Scanner floor: helper calls or the exact inline human.agent_type ownership
|
|
154
|
+
-- spelling with auth.uid. Other aliases/joins/helpers must extend this scanner.
|
|
155
|
+
-- This source-structural check does not prove arbitrary control flow.
|
|
156
|
+
create temp view _sid_r2_owner_paths as
|
|
157
|
+
select p.oid,p.proname,agent_board_test.stripped_body(p.oid) body
|
|
158
|
+
from pg_proc p where p.pronamespace='agent_board'::regnamespace and p.prokind='f'
|
|
159
|
+
and (agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_session_identity_owner')
|
|
160
|
+
or (agent_board_test.stripped_body(p.oid,true) like '%human.agent_type = ''human''%'
|
|
161
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('auth.uid',false)))
|
|
162
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.re(
|
|
163
|
+
agent_board_test.write_frag('agents',agent_board_test.bare_ok(p.oid)) || '|'
|
|
164
|
+
|| agent_board_test.write_frag('session_identities',agent_board_test.bare_ok(p.oid)) || '|'
|
|
165
|
+
|| agent_board_test.call_frag('_revoke_agent_principal'));
|
|
166
|
+
select ok((select count(*)>0 from _sid_r2_owner_paths),'R2-owner-class-populated: ownership writer set is catalog-derived');
|
|
167
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _sid_r2_owner_paths
|
|
168
|
+
where not (agent_board_test.stripped_body(oid) ~ agent_board_test.re('if\s+(not\s+)?'||agent_board_test.call_frag('_session_identity_owner'))
|
|
169
|
+
or (agent_board_test.stripped_body(oid) like '%if not exists (%'
|
|
170
|
+
and agent_board_test.stripped_body(oid) like '%human.auth_user_id = auth.uid()%'
|
|
171
|
+
and agent_board_test.stripped_body(oid) like '%target.owner_user_id = auth.uid()%'))),'',
|
|
172
|
+
'R2-owner-check-positive: every discovered writer actually uses its ownership check as an authorization condition');
|
|
173
|
+
select finish();
|
|
174
|
+
rollback;
|