@creatornader/agent-bridge 0.3.1 → 0.5.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/CHANGELOG.md +222 -1
- package/README.md +312 -12
- package/ROADMAP.md +88 -7
- package/SECURITY.md +78 -5
- package/SKILL.md +108 -1
- package/dist/archive.d.ts +2 -2
- package/dist/archive.js +2 -2
- package/dist/{bridge-service-hX1ZQp4W.d.ts → bridge-service-CNBKdYXh.d.ts} +2 -1
- package/dist/{bridge-store-DPVSCwfs.d.ts → bridge-store-ByJ6CaJk.d.ts} +3 -0
- package/dist/bridge.d.ts +25 -8
- package/dist/bridge.js +19 -9
- package/dist/{chunk-MBPZXTJA.js → chunk-7VJI2MXI.js} +285 -42
- package/dist/chunk-7VJI2MXI.js.map +1 -0
- package/dist/{chunk-4VB44KJU.js → chunk-AWE3DFZV.js} +3 -2
- package/dist/chunk-AWE3DFZV.js.map +1 -0
- package/dist/{chunk-GADH244D.js → chunk-E4OWHGUE.js} +16 -5
- package/dist/chunk-E4OWHGUE.js.map +1 -0
- package/dist/{chunk-NCO7RLBM.js → chunk-GTK4KOED.js} +108 -8
- package/dist/chunk-GTK4KOED.js.map +1 -0
- package/dist/{chunk-SPSMZOCJ.js → chunk-HUHG7XTB.js} +6 -5
- package/dist/chunk-HUHG7XTB.js.map +1 -0
- package/dist/{chunk-5GMB3A77.js → chunk-IVG4XWWS.js} +26 -14
- package/dist/chunk-IVG4XWWS.js.map +1 -0
- package/dist/{chunk-WAI2IYIQ.js → chunk-P635FAN4.js} +50 -7
- package/dist/chunk-P635FAN4.js.map +1 -0
- package/dist/{chunk-45BZGB77.js → chunk-QNXHDOJ5.js} +218 -24
- package/dist/chunk-QNXHDOJ5.js.map +1 -0
- package/dist/{chunk-UAHMP6GC.js → chunk-QPC6JHA3.js} +10 -7
- package/dist/chunk-QPC6JHA3.js.map +1 -0
- package/dist/{chunk-Q6PPMXJC.js → chunk-SN7PIVDF.js} +377 -21
- package/dist/chunk-SN7PIVDF.js.map +1 -0
- package/dist/{chunk-A6W4QALF.js → chunk-TC6A7GMF.js} +2 -2
- package/dist/{chunk-2DFNOLMO.js → chunk-XDNT6ZNZ.js} +9 -6
- package/dist/chunk-XDNT6ZNZ.js.map +1 -0
- package/dist/{chunk-CEXBMOI5.js → chunk-Z6JP6MYD.js} +2 -2
- package/dist/cli.js +5867 -438
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +4 -2
- package/dist/client-runtime.js +7 -7
- package/dist/contracts/generate.js +1 -1
- package/dist/dr.d.ts +9 -2
- package/dist/dr.js +6 -4
- package/dist/{gateway-CggQ8vTC.d.ts → gateway-CW-61lvs.d.ts} +27 -3
- package/dist/gateway-main.js +71 -7
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/index.js +8 -8
- package/dist/{postgres-bridge-store-Bfd3hRId.d.ts → postgres-bridge-store-C9KG9Z69.d.ts} +1 -1
- package/dist/postgres-production-preflight.d.ts +32 -0
- package/dist/postgres-production-preflight.js +249 -0
- package/dist/postgres-production-preflight.js.map +1 -0
- package/dist/server.js +8 -8
- package/dist/sqlite-edge-store-CdMIOrho.d.ts +148 -0
- package/dist/sqlite.d.ts +19 -80
- package/dist/sqlite.js +11 -3
- package/docs/architecture-v2.md +292 -15
- package/docs/decisions/0004-client-lifecycle-and-endpoint-migration.md +204 -0
- package/docs/deployment.md +349 -0
- package/docs/troubleshooting.md +23 -0
- package/llms.txt +69 -1
- package/openapi/agent-bridge-v2.openapi.json +1151 -124
- package/package.json +10 -2
- package/schemas/agent-bridge-v2.capabilities.json +26 -1
- package/schemas/agent-bridge-v2.mcp.json +11 -1
- package/schemas/agent-bridge-v2.schema.json +167 -0
- package/sql/migrations/007_runtime_role.sql +8 -4
- package/sql/migrations/013_row_isolation.sql +71 -8
- package/sql/migrations/014_owner_control_plane.sql +124 -31
- package/sql/migrations/015_portable_archives.sql +143 -41
- package/sql/migrations/016_native_dr_fence.sql +102 -35
- package/sql/migrations/017_gateway_authority_binding.sql +99 -0
- package/sql/migrations/018_endpoint_migration_challenges.sql +1054 -0
- package/sql/migrations/019_endpoint_migration_same_successor.sql +348 -0
- package/sql/migrations/020_managed_authority_compat.sql +698 -0
- package/sql/migrations/021_native_backup_reader.sql +421 -0
- package/sql/setup.sql +1 -3
- package/dist/chunk-2DFNOLMO.js.map +0 -1
- package/dist/chunk-45BZGB77.js.map +0 -1
- package/dist/chunk-4VB44KJU.js.map +0 -1
- package/dist/chunk-5GMB3A77.js.map +0 -1
- package/dist/chunk-GADH244D.js.map +0 -1
- package/dist/chunk-MBPZXTJA.js.map +0 -1
- package/dist/chunk-NCO7RLBM.js.map +0 -1
- package/dist/chunk-Q6PPMXJC.js.map +0 -1
- package/dist/chunk-SPSMZOCJ.js.map +0 -1
- package/dist/chunk-UAHMP6GC.js.map +0 -1
- package/dist/chunk-WAI2IYIQ.js.map +0 -1
- /package/dist/{chunk-A6W4QALF.js.map → chunk-TC6A7GMF.js.map} +0 -0
- /package/dist/{chunk-CEXBMOI5.js.map → chunk-Z6JP6MYD.js.map} +0 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
|
|
3
|
+
select pg_advisory_xact_lock(1646705660);
|
|
4
|
+
|
|
5
|
+
do $preflight$
|
|
6
|
+
begin
|
|
7
|
+
if not agent_bridge.security_schema_ready() then
|
|
8
|
+
raise exception 'endpoint migration challenge preflight rejected security drift';
|
|
9
|
+
elseif not agent_bridge.owner_control_plane_ready() then
|
|
10
|
+
raise exception 'endpoint migration challenge preflight rejected owner control drift';
|
|
11
|
+
elseif not agent_bridge.gateway_authority_ready() then
|
|
12
|
+
raise exception 'endpoint migration challenge preflight rejected gateway authority drift';
|
|
13
|
+
end if;
|
|
14
|
+
end
|
|
15
|
+
$preflight$;
|
|
16
|
+
|
|
17
|
+
alter table agent_bridge.request_authorities
|
|
18
|
+
add column authorized_endpoint_migration_operation text;
|
|
19
|
+
alter table agent_bridge.request_authorities
|
|
20
|
+
add constraint request_authorities_endpoint_migration_operation check (
|
|
21
|
+
authorized_endpoint_migration_operation is null
|
|
22
|
+
or authorized_endpoint_migration_operation in (
|
|
23
|
+
'issue_endpoint_migration_challenge','consume_endpoint_migration_challenge'
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
alter table agent_bridge.rate_limit_policies
|
|
28
|
+
drop constraint rate_limit_policy_operation;
|
|
29
|
+
alter table agent_bridge.rate_limit_policies
|
|
30
|
+
add constraint rate_limit_policy_operation check (
|
|
31
|
+
operation_id is null or operation_id in (
|
|
32
|
+
'capabilities','status','gateway_metrics','publish_message','history',
|
|
33
|
+
'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
|
|
34
|
+
'cancel_delivery','requeue_delivery','extend_delivery',
|
|
35
|
+
'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence',
|
|
36
|
+
'issue_endpoint_migration_challenge','consume_endpoint_migration_challenge'
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
insert into agent_bridge.rate_limit_policies(policy_id,operation_id,capacity,refill_per_second,enabled)
|
|
41
|
+
values
|
|
42
|
+
('operation:issue_endpoint_migration_challenge','issue_endpoint_migration_challenge',30,1,true),
|
|
43
|
+
('operation:consume_endpoint_migration_challenge','consume_endpoint_migration_challenge',30,1,true);
|
|
44
|
+
|
|
45
|
+
create or replace function agent_bridge.record_scope_denial_unbound_011(
|
|
46
|
+
requested_credential_id uuid,
|
|
47
|
+
requested_operation_id text,
|
|
48
|
+
requested_request_id uuid
|
|
49
|
+
) returns void
|
|
50
|
+
language plpgsql security definer set search_path = '' as $$
|
|
51
|
+
declare
|
|
52
|
+
credential_workspace text;
|
|
53
|
+
credential_principal text;
|
|
54
|
+
begin
|
|
55
|
+
if requested_operation_id is null or requested_operation_id not in (
|
|
56
|
+
'capabilities','status','gateway_metrics','publish_message','history',
|
|
57
|
+
'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
|
|
58
|
+
'cancel_delivery','requeue_delivery','extend_delivery',
|
|
59
|
+
'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence',
|
|
60
|
+
'issue_endpoint_migration_challenge','consume_endpoint_migration_challenge'
|
|
61
|
+
) then
|
|
62
|
+
raise exception using errcode='55000', message='operation security policy is unavailable';
|
|
63
|
+
end if;
|
|
64
|
+
if not exists (
|
|
65
|
+
select 1 from agent_bridge.rate_limit_policies
|
|
66
|
+
where operation_id=requested_operation_id and enabled
|
|
67
|
+
) then
|
|
68
|
+
raise exception using errcode='55000', message='operation security policy is unavailable';
|
|
69
|
+
end if;
|
|
70
|
+
select credential.workspace_id,agent.principal into credential_workspace,credential_principal
|
|
71
|
+
from agent_bridge.credentials credential
|
|
72
|
+
join agent_bridge.agents agent
|
|
73
|
+
on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
74
|
+
where credential.id=requested_credential_id;
|
|
75
|
+
if not found then raise exception 'credential is unavailable for security audit'; end if;
|
|
76
|
+
insert into agent_bridge.security_events(
|
|
77
|
+
event_type,outcome,reason_code,workspace_id,principal,actor_principal,
|
|
78
|
+
credential_id,operation_id,request_id
|
|
79
|
+
) values (
|
|
80
|
+
'scope_denied','denied','missing_scope',credential_workspace,
|
|
81
|
+
credential_principal,credential_principal,requested_credential_id,
|
|
82
|
+
requested_operation_id,requested_request_id
|
|
83
|
+
);
|
|
84
|
+
end
|
|
85
|
+
$$;
|
|
86
|
+
|
|
87
|
+
create or replace function agent_bridge.consume_rate_limit_unbound_011(
|
|
88
|
+
requested_credential_id uuid,
|
|
89
|
+
requested_operation_id text,
|
|
90
|
+
requested_request_id uuid
|
|
91
|
+
) returns table(
|
|
92
|
+
allowed boolean,
|
|
93
|
+
limit_value integer,
|
|
94
|
+
remaining_value integer,
|
|
95
|
+
retry_after_seconds numeric,
|
|
96
|
+
denied_policy_id text
|
|
97
|
+
)
|
|
98
|
+
language plpgsql security definer set search_path = '' as $$
|
|
99
|
+
declare
|
|
100
|
+
global_policy agent_bridge.rate_limit_policies%rowtype;
|
|
101
|
+
operation_policy agent_bridge.rate_limit_policies%rowtype;
|
|
102
|
+
request_time timestamptz := clock_timestamp();
|
|
103
|
+
global_available numeric(30,6);
|
|
104
|
+
operation_available numeric(30,6);
|
|
105
|
+
global_after numeric(30,6);
|
|
106
|
+
operation_after numeric(30,6);
|
|
107
|
+
global_retry numeric(30,6) := 0;
|
|
108
|
+
operation_retry numeric(30,6) := 0;
|
|
109
|
+
credential_workspace text;
|
|
110
|
+
credential_principal text;
|
|
111
|
+
begin
|
|
112
|
+
if requested_operation_id is null or requested_operation_id not in (
|
|
113
|
+
'capabilities','status','gateway_metrics','publish_message','history',
|
|
114
|
+
'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
|
|
115
|
+
'cancel_delivery','requeue_delivery','extend_delivery',
|
|
116
|
+
'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence',
|
|
117
|
+
'issue_endpoint_migration_challenge','consume_endpoint_migration_challenge'
|
|
118
|
+
) then
|
|
119
|
+
raise exception using errcode='55000', message='operation rate limit policy is unavailable';
|
|
120
|
+
end if;
|
|
121
|
+
select * into global_policy from agent_bridge.rate_limit_policies
|
|
122
|
+
where policy_id='global' and enabled for share;
|
|
123
|
+
if not found then raise exception using errcode='55000', message='global rate limit policy is unavailable'; end if;
|
|
124
|
+
select * into operation_policy from agent_bridge.rate_limit_policies
|
|
125
|
+
where operation_id=requested_operation_id and enabled for share;
|
|
126
|
+
if not found then raise exception using errcode='55000', message='operation rate limit policy is unavailable'; end if;
|
|
127
|
+
|
|
128
|
+
insert into agent_bridge.rate_limit_buckets(credential_id,policy_id,tokens,updated_at)
|
|
129
|
+
values
|
|
130
|
+
(requested_credential_id,global_policy.policy_id,global_policy.capacity,request_time),
|
|
131
|
+
(requested_credential_id,operation_policy.policy_id,operation_policy.capacity,request_time)
|
|
132
|
+
on conflict do nothing;
|
|
133
|
+
perform 1 from agent_bridge.rate_limit_buckets
|
|
134
|
+
where credential_id=requested_credential_id
|
|
135
|
+
and policy_id in (global_policy.policy_id,operation_policy.policy_id)
|
|
136
|
+
order by policy_id for update;
|
|
137
|
+
|
|
138
|
+
select least(global_policy.capacity::numeric,
|
|
139
|
+
greatest(0::numeric,bucket.tokens)+greatest(0::numeric,
|
|
140
|
+
extract(epoch from request_time-bucket.updated_at)::numeric)*global_policy.refill_per_second)
|
|
141
|
+
into global_available
|
|
142
|
+
from agent_bridge.rate_limit_buckets bucket
|
|
143
|
+
where bucket.credential_id=requested_credential_id and bucket.policy_id=global_policy.policy_id;
|
|
144
|
+
select least(operation_policy.capacity::numeric,
|
|
145
|
+
greatest(0::numeric,bucket.tokens)+greatest(0::numeric,
|
|
146
|
+
extract(epoch from request_time-bucket.updated_at)::numeric)*operation_policy.refill_per_second)
|
|
147
|
+
into operation_available
|
|
148
|
+
from agent_bridge.rate_limit_buckets bucket
|
|
149
|
+
where bucket.credential_id=requested_credential_id and bucket.policy_id=operation_policy.policy_id;
|
|
150
|
+
|
|
151
|
+
allowed := global_available>=1 and operation_available>=1;
|
|
152
|
+
global_after := case when allowed then global_available-1 else global_available end;
|
|
153
|
+
operation_after := case when allowed then operation_available-1 else operation_available end;
|
|
154
|
+
update agent_bridge.rate_limit_buckets bucket set
|
|
155
|
+
tokens=case when bucket.policy_id=global_policy.policy_id then global_after else operation_after end,
|
|
156
|
+
updated_at=request_time
|
|
157
|
+
where bucket.credential_id=requested_credential_id
|
|
158
|
+
and bucket.policy_id in (global_policy.policy_id,operation_policy.policy_id);
|
|
159
|
+
|
|
160
|
+
limit_value := least(global_policy.capacity,operation_policy.capacity);
|
|
161
|
+
remaining_value := floor(least(global_after,operation_after))::integer;
|
|
162
|
+
retry_after_seconds := 0;
|
|
163
|
+
denied_policy_id := null;
|
|
164
|
+
if not allowed then
|
|
165
|
+
if global_available<1 then global_retry := (1-global_available)/global_policy.refill_per_second; end if;
|
|
166
|
+
if operation_available<1 then operation_retry := (1-operation_available)/operation_policy.refill_per_second; end if;
|
|
167
|
+
retry_after_seconds := greatest(global_retry,operation_retry);
|
|
168
|
+
denied_policy_id := case when global_retry>=operation_retry then global_policy.policy_id else operation_policy.policy_id end;
|
|
169
|
+
select credential.workspace_id,agent.principal into credential_workspace,credential_principal
|
|
170
|
+
from agent_bridge.credentials credential
|
|
171
|
+
join agent_bridge.agents agent on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
172
|
+
where credential.id=requested_credential_id;
|
|
173
|
+
if not found then raise exception 'credential is unavailable for rate audit'; end if;
|
|
174
|
+
insert into agent_bridge.security_events(
|
|
175
|
+
event_type,outcome,reason_code,workspace_id,principal,actor_principal,
|
|
176
|
+
credential_id,operation_id,request_id,policy_id,retry_after_seconds
|
|
177
|
+
) values (
|
|
178
|
+
'rate_denied','denied','rate_limit_exceeded',credential_workspace,
|
|
179
|
+
credential_principal,credential_principal,requested_credential_id,
|
|
180
|
+
requested_operation_id,requested_request_id,denied_policy_id,
|
|
181
|
+
greatest(1,ceil(retry_after_seconds)::integer)
|
|
182
|
+
);
|
|
183
|
+
end if;
|
|
184
|
+
return next;
|
|
185
|
+
end
|
|
186
|
+
$$;
|
|
187
|
+
|
|
188
|
+
create or replace function agent_bridge.consume_rate_limit(
|
|
189
|
+
requested_credential_id uuid,
|
|
190
|
+
requested_operation_id text,
|
|
191
|
+
requested_request_id uuid
|
|
192
|
+
) returns table(
|
|
193
|
+
allowed boolean,
|
|
194
|
+
limit_value integer,
|
|
195
|
+
remaining_value integer,
|
|
196
|
+
retry_after_seconds numeric,
|
|
197
|
+
denied_policy_id text
|
|
198
|
+
)
|
|
199
|
+
language plpgsql security definer set search_path = '' as $$
|
|
200
|
+
declare
|
|
201
|
+
authority agent_bridge.request_authorities%rowtype;
|
|
202
|
+
decision record;
|
|
203
|
+
stamped_operation text;
|
|
204
|
+
begin
|
|
205
|
+
select * into authority from agent_bridge.assert_active_request_credential(requested_credential_id);
|
|
206
|
+
select * into decision from agent_bridge.consume_rate_limit_unbound_011(
|
|
207
|
+
requested_credential_id,requested_operation_id,requested_request_id
|
|
208
|
+
);
|
|
209
|
+
if decision.allowed and requested_operation_id in (
|
|
210
|
+
'issue_endpoint_migration_challenge','consume_endpoint_migration_challenge'
|
|
211
|
+
) then
|
|
212
|
+
update agent_bridge.request_authorities
|
|
213
|
+
set authorized_endpoint_migration_operation=requested_operation_id
|
|
214
|
+
where backend_pid=authority.backend_pid and transaction_id=authority.transaction_id
|
|
215
|
+
and authorized_endpoint_migration_operation is null
|
|
216
|
+
returning authorized_endpoint_migration_operation into stamped_operation;
|
|
217
|
+
if not found then
|
|
218
|
+
select authorized_endpoint_migration_operation into stamped_operation
|
|
219
|
+
from agent_bridge.request_authorities
|
|
220
|
+
where backend_pid=authority.backend_pid and transaction_id=authority.transaction_id;
|
|
221
|
+
if stamped_operation is distinct from requested_operation_id then
|
|
222
|
+
raise exception using errcode='42501',message='endpoint migration authorization is already bound to another operation';
|
|
223
|
+
end if;
|
|
224
|
+
end if;
|
|
225
|
+
end if;
|
|
226
|
+
allowed := decision.allowed;
|
|
227
|
+
limit_value := decision.limit_value;
|
|
228
|
+
remaining_value := decision.remaining_value;
|
|
229
|
+
retry_after_seconds := decision.retry_after_seconds;
|
|
230
|
+
denied_policy_id := decision.denied_policy_id;
|
|
231
|
+
return next;
|
|
232
|
+
end
|
|
233
|
+
$$;
|
|
234
|
+
|
|
235
|
+
create table agent_bridge.endpoint_migration_challenges (
|
|
236
|
+
challenge_hash char(64) primary key,
|
|
237
|
+
authority_id uuid not null references agent_bridge.gateway_authority(authority_id) on delete restrict,
|
|
238
|
+
workspace_id text not null references agent_bridge.workspaces(id) on delete restrict,
|
|
239
|
+
principal text not null,
|
|
240
|
+
issuer_credential_id uuid not null references agent_bridge.credentials(id) on delete restrict,
|
|
241
|
+
verifier_credential_id uuid not null references agent_bridge.credentials(id) on delete restrict,
|
|
242
|
+
issued_at timestamptz not null default clock_timestamp(),
|
|
243
|
+
expires_at timestamptz not null,
|
|
244
|
+
consumed_at timestamptz,
|
|
245
|
+
consumed_credential_id uuid references agent_bridge.credentials(id) on delete restrict,
|
|
246
|
+
constraint endpoint_migration_challenges_expiry check(
|
|
247
|
+
expires_at>issued_at and expires_at<=issued_at+interval '60 seconds'
|
|
248
|
+
),
|
|
249
|
+
constraint endpoint_migration_challenges_consumption check(
|
|
250
|
+
(consumed_at is null and consumed_credential_id is null)
|
|
251
|
+
or (consumed_at is not null and consumed_credential_id=verifier_credential_id)
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
create index endpoint_migration_challenges_cleanup
|
|
255
|
+
on agent_bridge.endpoint_migration_challenges(expires_at);
|
|
256
|
+
|
|
257
|
+
create table agent_bridge.endpoint_migration_challenge_events (
|
|
258
|
+
sequence bigint generated always as identity primary key,
|
|
259
|
+
event_id uuid not null default gen_random_uuid() unique,
|
|
260
|
+
request_id uuid not null unique,
|
|
261
|
+
event_type text not null check(event_type in ('issued','consumed','expired')),
|
|
262
|
+
outcome text not null check(outcome='succeeded'),
|
|
263
|
+
authority_id uuid not null references agent_bridge.gateway_authority(authority_id) on delete restrict,
|
|
264
|
+
workspace_id text not null references agent_bridge.workspaces(id) on delete restrict,
|
|
265
|
+
principal text not null,
|
|
266
|
+
issuer_credential_id uuid not null references agent_bridge.credentials(id) on delete restrict,
|
|
267
|
+
verifier_credential_id uuid not null references agent_bridge.credentials(id) on delete restrict,
|
|
268
|
+
created_at timestamptz not null default clock_timestamp()
|
|
269
|
+
);
|
|
270
|
+
create index endpoint_migration_challenge_events_workspace_sequence
|
|
271
|
+
on agent_bridge.endpoint_migration_challenge_events(workspace_id,sequence desc);
|
|
272
|
+
|
|
273
|
+
create function agent_bridge.reject_endpoint_migration_challenge_event_mutation()
|
|
274
|
+
returns trigger language plpgsql set search_path = '' as $$
|
|
275
|
+
begin
|
|
276
|
+
raise exception 'endpoint migration challenge events are append-only';
|
|
277
|
+
end
|
|
278
|
+
$$;
|
|
279
|
+
create trigger endpoint_migration_challenge_events_append_only
|
|
280
|
+
before update or delete or truncate on agent_bridge.endpoint_migration_challenge_events
|
|
281
|
+
for each statement execute function agent_bridge.reject_endpoint_migration_challenge_event_mutation();
|
|
282
|
+
|
|
283
|
+
create function agent_bridge.guard_endpoint_migration_challenge()
|
|
284
|
+
returns trigger language plpgsql set search_path = '' as $$
|
|
285
|
+
begin
|
|
286
|
+
if tg_op='UPDATE' then
|
|
287
|
+
if old.consumed_at is not null or new.consumed_at is null
|
|
288
|
+
or new.consumed_credential_id is distinct from old.verifier_credential_id
|
|
289
|
+
or new.challenge_hash is distinct from old.challenge_hash
|
|
290
|
+
or new.authority_id is distinct from old.authority_id
|
|
291
|
+
or new.workspace_id is distinct from old.workspace_id
|
|
292
|
+
or new.principal is distinct from old.principal
|
|
293
|
+
or new.issuer_credential_id is distinct from old.issuer_credential_id
|
|
294
|
+
or new.verifier_credential_id is distinct from old.verifier_credential_id
|
|
295
|
+
or new.issued_at is distinct from old.issued_at
|
|
296
|
+
or new.expires_at is distinct from old.expires_at then
|
|
297
|
+
raise exception 'endpoint migration challenge may only be consumed once';
|
|
298
|
+
end if;
|
|
299
|
+
return new;
|
|
300
|
+
end if;
|
|
301
|
+
if current_setting('agent_bridge.endpoint_migration_cleanup',true) is distinct from 'internal'
|
|
302
|
+
or (old.expires_at>clock_timestamp() and old.consumed_at is null) then
|
|
303
|
+
raise exception 'endpoint migration challenge deletion is restricted';
|
|
304
|
+
end if;
|
|
305
|
+
return old;
|
|
306
|
+
end
|
|
307
|
+
$$;
|
|
308
|
+
create trigger endpoint_migration_challenges_guard
|
|
309
|
+
before update or delete on agent_bridge.endpoint_migration_challenges
|
|
310
|
+
for each row execute function agent_bridge.guard_endpoint_migration_challenge();
|
|
311
|
+
create function agent_bridge.reject_endpoint_migration_challenge_truncate()
|
|
312
|
+
returns trigger language plpgsql set search_path = '' as $$
|
|
313
|
+
begin
|
|
314
|
+
raise exception 'endpoint migration challenge truncation is forbidden';
|
|
315
|
+
end
|
|
316
|
+
$$;
|
|
317
|
+
create trigger endpoint_migration_challenges_no_truncate
|
|
318
|
+
before truncate on agent_bridge.endpoint_migration_challenges
|
|
319
|
+
for each statement execute function agent_bridge.reject_endpoint_migration_challenge_truncate();
|
|
320
|
+
|
|
321
|
+
create function agent_bridge.cleanup_endpoint_migration_challenges()
|
|
322
|
+
returns integer language plpgsql security definer set search_path = '' as $$
|
|
323
|
+
declare
|
|
324
|
+
removed integer := 0;
|
|
325
|
+
challenge record;
|
|
326
|
+
begin
|
|
327
|
+
perform set_config('agent_bridge.endpoint_migration_cleanup','internal',true);
|
|
328
|
+
for challenge in
|
|
329
|
+
select challenge_hash,authority_id,workspace_id,principal,issuer_credential_id,verifier_credential_id
|
|
330
|
+
from agent_bridge.endpoint_migration_challenges
|
|
331
|
+
where expires_at<=clock_timestamp()
|
|
332
|
+
order by expires_at,challenge_hash
|
|
333
|
+
limit 100
|
|
334
|
+
for update skip locked
|
|
335
|
+
loop
|
|
336
|
+
delete from agent_bridge.endpoint_migration_challenges where challenge_hash=challenge.challenge_hash;
|
|
337
|
+
if challenge.authority_id is not null then
|
|
338
|
+
insert into agent_bridge.endpoint_migration_challenge_events(
|
|
339
|
+
request_id,event_type,outcome,authority_id,workspace_id,principal,
|
|
340
|
+
issuer_credential_id,verifier_credential_id
|
|
341
|
+
) values (
|
|
342
|
+
gen_random_uuid(),'expired','succeeded',challenge.authority_id,
|
|
343
|
+
challenge.workspace_id,challenge.principal,challenge.issuer_credential_id,
|
|
344
|
+
challenge.verifier_credential_id
|
|
345
|
+
);
|
|
346
|
+
end if;
|
|
347
|
+
removed := removed+1;
|
|
348
|
+
end loop;
|
|
349
|
+
return removed;
|
|
350
|
+
end
|
|
351
|
+
$$;
|
|
352
|
+
|
|
353
|
+
create function agent_bridge.endpoint_migration_challenge_active_credential(
|
|
354
|
+
requested_credential_id uuid,
|
|
355
|
+
requested_workspace_id text,
|
|
356
|
+
requested_principal text
|
|
357
|
+
) returns boolean language sql stable security definer set search_path = '' as $$
|
|
358
|
+
select exists(
|
|
359
|
+
select 1 from agent_bridge.credentials credential
|
|
360
|
+
join agent_bridge.agents agent
|
|
361
|
+
on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
362
|
+
where credential.id=requested_credential_id
|
|
363
|
+
and credential.workspace_id=requested_workspace_id
|
|
364
|
+
and agent.principal=requested_principal
|
|
365
|
+
and credential.revoked_at is null
|
|
366
|
+
and (credential.expires_at is null or credential.expires_at>clock_timestamp())
|
|
367
|
+
and (not exists(select 1 from agent_bridge.credentials successor
|
|
368
|
+
where successor.replaces_credential_id=credential.id)
|
|
369
|
+
or credential.expiry_grace_until>clock_timestamp())
|
|
370
|
+
and agent.disabled_at is null
|
|
371
|
+
)
|
|
372
|
+
$$;
|
|
373
|
+
|
|
374
|
+
create function agent_bridge.endpoint_migration_challenge_direct_lineage(
|
|
375
|
+
issuer_credential_id uuid,
|
|
376
|
+
verifier_credential_id uuid
|
|
377
|
+
) returns boolean language sql stable security definer set search_path = '' as $$
|
|
378
|
+
select exists(
|
|
379
|
+
select 1 from agent_bridge.credentials credential
|
|
380
|
+
where credential.id=verifier_credential_id
|
|
381
|
+
and credential.replaces_credential_id=issuer_credential_id
|
|
382
|
+
)
|
|
383
|
+
$$;
|
|
384
|
+
|
|
385
|
+
create function agent_bridge.issue_endpoint_migration_challenge(
|
|
386
|
+
requested_expected_gateway_authority_id uuid,
|
|
387
|
+
requested_verifier_credential_id uuid,
|
|
388
|
+
requested_challenge text
|
|
389
|
+
) returns table(
|
|
390
|
+
gateway_authority_id uuid,
|
|
391
|
+
issuer_credential_id uuid,
|
|
392
|
+
verifier_credential_id uuid,
|
|
393
|
+
expires_at timestamptz
|
|
394
|
+
)
|
|
395
|
+
language plpgsql security definer set search_path = '' as $$
|
|
396
|
+
declare
|
|
397
|
+
authority agent_bridge.request_authorities%rowtype;
|
|
398
|
+
gateway_id uuid;
|
|
399
|
+
now_time timestamptz := clock_timestamp();
|
|
400
|
+
challenge_commitment char(64);
|
|
401
|
+
existing agent_bridge.endpoint_migration_challenges%rowtype;
|
|
402
|
+
begin
|
|
403
|
+
if requested_challenge is null or requested_challenge !~ '^[0-9a-f]{64}$' then
|
|
404
|
+
raise exception using errcode='22023',message='endpoint migration challenge is invalid';
|
|
405
|
+
end if;
|
|
406
|
+
select * into authority from agent_bridge.active_request_authority();
|
|
407
|
+
if not found or authority.authorized_endpoint_migration_operation is distinct from 'issue_endpoint_migration_challenge' then
|
|
408
|
+
raise exception using errcode='42501',message='endpoint migration issue authorization is required';
|
|
409
|
+
end if;
|
|
410
|
+
select authority_id into strict gateway_id from agent_bridge.gateway_authority where singleton;
|
|
411
|
+
if requested_expected_gateway_authority_id is distinct from gateway_id then
|
|
412
|
+
raise exception using errcode='28000',message='expected gateway authority does not match';
|
|
413
|
+
end if;
|
|
414
|
+
if not agent_bridge.endpoint_migration_challenge_active_credential(
|
|
415
|
+
authority.credential_id,authority.workspace_id,authority.principal
|
|
416
|
+
) or not agent_bridge.endpoint_migration_challenge_active_credential(
|
|
417
|
+
requested_verifier_credential_id,authority.workspace_id,authority.principal
|
|
418
|
+
) or not agent_bridge.endpoint_migration_challenge_direct_lineage(
|
|
419
|
+
authority.credential_id,requested_verifier_credential_id
|
|
420
|
+
) then
|
|
421
|
+
raise exception using errcode='28000',message='endpoint migration verifier is not an active direct replacement credential';
|
|
422
|
+
end if;
|
|
423
|
+
challenge_commitment := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
424
|
+
'agent-bridge.endpoint-migration-challenge.v1:'||requested_challenge,'UTF8'
|
|
425
|
+
)),'hex')::character(64);
|
|
426
|
+
perform agent_bridge.cleanup_endpoint_migration_challenges();
|
|
427
|
+
select * into existing from agent_bridge.endpoint_migration_challenges
|
|
428
|
+
where challenge_hash=challenge_commitment for update;
|
|
429
|
+
if found then
|
|
430
|
+
if existing.authority_id=gateway_id
|
|
431
|
+
and existing.workspace_id=authority.workspace_id
|
|
432
|
+
and existing.principal=authority.principal
|
|
433
|
+
and existing.issuer_credential_id=authority.credential_id
|
|
434
|
+
and existing.verifier_credential_id=requested_verifier_credential_id
|
|
435
|
+
and existing.expires_at>now_time
|
|
436
|
+
and existing.consumed_at is null then
|
|
437
|
+
gateway_authority_id := existing.authority_id;
|
|
438
|
+
issuer_credential_id := existing.issuer_credential_id;
|
|
439
|
+
verifier_credential_id := existing.verifier_credential_id;
|
|
440
|
+
expires_at := existing.expires_at;
|
|
441
|
+
return next;
|
|
442
|
+
return;
|
|
443
|
+
end if;
|
|
444
|
+
raise exception using errcode='23505',message='endpoint migration challenge commitment conflicts with an existing challenge';
|
|
445
|
+
end if;
|
|
446
|
+
insert into agent_bridge.endpoint_migration_challenges(
|
|
447
|
+
challenge_hash,authority_id,workspace_id,principal,issuer_credential_id,verifier_credential_id,expires_at
|
|
448
|
+
) values (
|
|
449
|
+
challenge_commitment,gateway_id,authority.workspace_id,authority.principal,
|
|
450
|
+
authority.credential_id,requested_verifier_credential_id,now_time+interval '60 seconds'
|
|
451
|
+
);
|
|
452
|
+
insert into agent_bridge.endpoint_migration_challenge_events(
|
|
453
|
+
request_id,event_type,outcome,authority_id,workspace_id,principal,issuer_credential_id,verifier_credential_id
|
|
454
|
+
) values (
|
|
455
|
+
authority.request_id,'issued','succeeded',gateway_id,authority.workspace_id,authority.principal,
|
|
456
|
+
authority.credential_id,requested_verifier_credential_id
|
|
457
|
+
);
|
|
458
|
+
gateway_authority_id := gateway_id;
|
|
459
|
+
issuer_credential_id := authority.credential_id;
|
|
460
|
+
verifier_credential_id := requested_verifier_credential_id;
|
|
461
|
+
expires_at := now_time+interval '60 seconds';
|
|
462
|
+
return next;
|
|
463
|
+
end
|
|
464
|
+
$$;
|
|
465
|
+
|
|
466
|
+
create function agent_bridge.consume_endpoint_migration_challenge(
|
|
467
|
+
requested_expected_gateway_authority_id uuid,
|
|
468
|
+
requested_issuer_credential_id uuid,
|
|
469
|
+
requested_challenge text
|
|
470
|
+
) returns table(
|
|
471
|
+
gateway_authority_id uuid,
|
|
472
|
+
issuer_credential_id uuid,
|
|
473
|
+
verifier_credential_id uuid,
|
|
474
|
+
expires_at timestamptz,
|
|
475
|
+
consumed boolean
|
|
476
|
+
)
|
|
477
|
+
language plpgsql security definer set search_path = '' as $$
|
|
478
|
+
declare
|
|
479
|
+
authority agent_bridge.request_authorities%rowtype;
|
|
480
|
+
gateway_id uuid;
|
|
481
|
+
challenge agent_bridge.endpoint_migration_challenges%rowtype;
|
|
482
|
+
challenge_commitment char(64);
|
|
483
|
+
begin
|
|
484
|
+
if requested_challenge is null or requested_challenge !~ '^[0-9a-f]{64}$' then
|
|
485
|
+
raise exception using errcode='22023',message='endpoint migration challenge is invalid';
|
|
486
|
+
end if;
|
|
487
|
+
select * into authority from agent_bridge.active_request_authority();
|
|
488
|
+
if not found or authority.authorized_endpoint_migration_operation is distinct from 'consume_endpoint_migration_challenge' then
|
|
489
|
+
raise exception using errcode='42501',message='endpoint migration consume authorization is required';
|
|
490
|
+
end if;
|
|
491
|
+
select authority_id into strict gateway_id from agent_bridge.gateway_authority where singleton;
|
|
492
|
+
if requested_expected_gateway_authority_id is distinct from gateway_id then
|
|
493
|
+
raise exception using errcode='28000',message='expected gateway authority does not match';
|
|
494
|
+
end if;
|
|
495
|
+
challenge_commitment := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
496
|
+
'agent-bridge.endpoint-migration-challenge.v1:'||requested_challenge,'UTF8'
|
|
497
|
+
)),'hex')::character(64);
|
|
498
|
+
select * into challenge from agent_bridge.endpoint_migration_challenges
|
|
499
|
+
where challenge_hash=challenge_commitment for update;
|
|
500
|
+
if not found then
|
|
501
|
+
gateway_authority_id := gateway_id;
|
|
502
|
+
issuer_credential_id := requested_issuer_credential_id;
|
|
503
|
+
verifier_credential_id := authority.credential_id;
|
|
504
|
+
expires_at := null;
|
|
505
|
+
consumed := false;
|
|
506
|
+
return next;
|
|
507
|
+
return;
|
|
508
|
+
end if;
|
|
509
|
+
gateway_authority_id := challenge.authority_id;
|
|
510
|
+
issuer_credential_id := challenge.issuer_credential_id;
|
|
511
|
+
verifier_credential_id := challenge.verifier_credential_id;
|
|
512
|
+
expires_at := challenge.expires_at;
|
|
513
|
+
consumed := false;
|
|
514
|
+
if challenge.authority_id is distinct from gateway_id
|
|
515
|
+
or challenge.authority_id is distinct from requested_expected_gateway_authority_id
|
|
516
|
+
or challenge.workspace_id is distinct from authority.workspace_id
|
|
517
|
+
or challenge.principal is distinct from authority.principal
|
|
518
|
+
or challenge.issuer_credential_id is distinct from requested_issuer_credential_id
|
|
519
|
+
or challenge.verifier_credential_id is distinct from authority.credential_id
|
|
520
|
+
or challenge.expires_at<=clock_timestamp()
|
|
521
|
+
or challenge.consumed_at is not null
|
|
522
|
+
or not agent_bridge.endpoint_migration_challenge_active_credential(
|
|
523
|
+
challenge.issuer_credential_id,challenge.workspace_id,challenge.principal
|
|
524
|
+
)
|
|
525
|
+
or not agent_bridge.endpoint_migration_challenge_active_credential(
|
|
526
|
+
authority.credential_id,authority.workspace_id,authority.principal
|
|
527
|
+
)
|
|
528
|
+
or not agent_bridge.endpoint_migration_challenge_direct_lineage(
|
|
529
|
+
challenge.issuer_credential_id,authority.credential_id
|
|
530
|
+
) then
|
|
531
|
+
return next;
|
|
532
|
+
return;
|
|
533
|
+
end if;
|
|
534
|
+
update agent_bridge.endpoint_migration_challenges stored
|
|
535
|
+
set consumed_at=clock_timestamp(),consumed_credential_id=authority.credential_id
|
|
536
|
+
where stored.challenge_hash=challenge.challenge_hash and stored.consumed_at is null;
|
|
537
|
+
consumed := found;
|
|
538
|
+
if consumed then
|
|
539
|
+
insert into agent_bridge.endpoint_migration_challenge_events(
|
|
540
|
+
request_id,event_type,outcome,authority_id,workspace_id,principal,issuer_credential_id,verifier_credential_id
|
|
541
|
+
) values (
|
|
542
|
+
authority.request_id,'consumed','succeeded',challenge.authority_id,
|
|
543
|
+
challenge.workspace_id,challenge.principal,challenge.issuer_credential_id,
|
|
544
|
+
challenge.verifier_credential_id
|
|
545
|
+
);
|
|
546
|
+
end if;
|
|
547
|
+
return next;
|
|
548
|
+
end
|
|
549
|
+
$$;
|
|
550
|
+
|
|
551
|
+
create table agent_bridge.endpoint_migration_challenge_attestations (
|
|
552
|
+
name text primary key,
|
|
553
|
+
catalog_definition text not null,
|
|
554
|
+
attested_at timestamptz not null default clock_timestamp(),
|
|
555
|
+
constraint endpoint_migration_challenge_attestation_name check(name='endpoint-migration-v1')
|
|
556
|
+
);
|
|
557
|
+
create function agent_bridge.reject_endpoint_migration_challenge_attestation_mutation()
|
|
558
|
+
returns trigger language plpgsql set search_path = '' as $$
|
|
559
|
+
begin
|
|
560
|
+
raise exception 'endpoint migration challenge attestations are immutable';
|
|
561
|
+
end
|
|
562
|
+
$$;
|
|
563
|
+
create trigger endpoint_migration_challenge_attestations_append_only
|
|
564
|
+
before update or delete or truncate on agent_bridge.endpoint_migration_challenge_attestations
|
|
565
|
+
for each statement execute function agent_bridge.reject_endpoint_migration_challenge_attestation_mutation();
|
|
566
|
+
|
|
567
|
+
create function agent_bridge.endpoint_migration_challenge_catalog_definition()
|
|
568
|
+
returns text language sql stable security definer set search_path = '' as $$
|
|
569
|
+
with protected_relations(relation_name) as (values
|
|
570
|
+
('endpoint_migration_challenges'),('endpoint_migration_challenge_events'),
|
|
571
|
+
('endpoint_migration_challenge_attestations'),('request_authorities')
|
|
572
|
+
), protected_functions(function_name) as (values
|
|
573
|
+
('guard_endpoint_migration_challenge'),('reject_endpoint_migration_challenge_truncate'),
|
|
574
|
+
('reject_endpoint_migration_challenge_event_mutation'),
|
|
575
|
+
('reject_endpoint_migration_challenge_attestation_mutation'),
|
|
576
|
+
('cleanup_endpoint_migration_challenges'),('endpoint_migration_challenge_active_credential'),
|
|
577
|
+
('endpoint_migration_challenge_direct_lineage'),('issue_endpoint_migration_challenge'),
|
|
578
|
+
('consume_endpoint_migration_challenge'),('endpoint_migration_challenge_catalog_definition'),
|
|
579
|
+
('endpoint_migration_challenge_ready')
|
|
580
|
+
), catalog(kind,identity,definition) as (
|
|
581
|
+
select 'relation',relation.relname,pg_catalog.pg_get_userbyid(relation.relowner)||':'||
|
|
582
|
+
relation.relkind::text||':'||relation.relpersistence::text||':'||relation.relrowsecurity::text||':'||relation.relforcerowsecurity::text
|
|
583
|
+
from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
584
|
+
where namespace.nspname='agent_bridge' and relation.relname in(select relation_name from protected_relations)
|
|
585
|
+
union all
|
|
586
|
+
select 'column',relation.relname||'.'||attribute.attname,pg_catalog.format_type(attribute.atttypid,attribute.atttypmod)||':'||
|
|
587
|
+
attribute.attnotnull::text||':'||attribute.attidentity::text||':'||attribute.attgenerated::text||':'||
|
|
588
|
+
coalesce(pg_catalog.pg_get_expr(default_record.adbin,default_record.adrelid),'')
|
|
589
|
+
from pg_catalog.pg_attribute attribute join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
590
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
591
|
+
left join pg_catalog.pg_attrdef default_record on default_record.adrelid=attribute.attrelid and default_record.adnum=attribute.attnum
|
|
592
|
+
where namespace.nspname='agent_bridge' and relation.relname in(select relation_name from protected_relations)
|
|
593
|
+
and attribute.attnum>0 and not attribute.attisdropped
|
|
594
|
+
union all
|
|
595
|
+
select 'constraint',relation.relname||'.'||constraint_record.conname,
|
|
596
|
+
constraint_record.convalidated::text||':'||pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
597
|
+
from pg_catalog.pg_constraint constraint_record join pg_catalog.pg_class relation on relation.oid=constraint_record.conrelid
|
|
598
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
599
|
+
where namespace.nspname='agent_bridge' and relation.relname in(select relation_name from protected_relations)
|
|
600
|
+
union all
|
|
601
|
+
select 'index',index_relation.relname,pg_catalog.pg_get_indexdef(index_record.indexrelid)||':'||
|
|
602
|
+
coalesce(pg_catalog.pg_get_expr(index_record.indpred,index_record.indrelid),'')
|
|
603
|
+
from pg_catalog.pg_index index_record join pg_catalog.pg_class source_relation on source_relation.oid=index_record.indrelid
|
|
604
|
+
join pg_catalog.pg_class index_relation on index_relation.oid=index_record.indexrelid
|
|
605
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=source_relation.relnamespace
|
|
606
|
+
where namespace.nspname='agent_bridge' and source_relation.relname in(select relation_name from protected_relations)
|
|
607
|
+
union all
|
|
608
|
+
select 'trigger',relation.relname||'.'||trigger.tgname,trigger.tgenabled::text||':'||pg_catalog.pg_get_triggerdef(trigger.oid,true)
|
|
609
|
+
from pg_catalog.pg_trigger trigger join pg_catalog.pg_class relation on relation.oid=trigger.tgrelid
|
|
610
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
611
|
+
where namespace.nspname='agent_bridge' and not trigger.tgisinternal
|
|
612
|
+
and relation.relname in(select relation_name from protected_relations)
|
|
613
|
+
union all
|
|
614
|
+
select 'function',procedure.oid::regprocedure::text,pg_catalog.pg_get_userbyid(procedure.proowner)||':'||pg_catalog.pg_get_functiondef(procedure.oid)
|
|
615
|
+
from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
616
|
+
where namespace.nspname='agent_bridge' and procedure.proname in(select function_name from protected_functions)
|
|
617
|
+
union all
|
|
618
|
+
select 'function_acl',procedure.oid::regprocedure::text,
|
|
619
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
620
|
+
case when privilege.grantee=0 then 'PUBLIC'
|
|
621
|
+
else pg_catalog.pg_get_userbyid(privilege.grantee) end||':'||
|
|
622
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
623
|
+
from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
624
|
+
cross join lateral pg_catalog.aclexplode(coalesce(
|
|
625
|
+
procedure.proacl,pg_catalog.acldefault('f',procedure.proowner)
|
|
626
|
+
)) privilege
|
|
627
|
+
where namespace.nspname='agent_bridge' and procedure.proname in(select function_name from protected_functions)
|
|
628
|
+
union all
|
|
629
|
+
select 'relation_acl',relation.relname,
|
|
630
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
631
|
+
case when privilege.grantee=0 then 'PUBLIC'
|
|
632
|
+
else pg_catalog.pg_get_userbyid(privilege.grantee) end||':'||
|
|
633
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
634
|
+
from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
635
|
+
cross join lateral pg_catalog.aclexplode(coalesce(
|
|
636
|
+
relation.relacl,pg_catalog.acldefault(
|
|
637
|
+
case when relation.relkind='S' then 'S'::"char" else 'r'::"char" end,
|
|
638
|
+
relation.relowner
|
|
639
|
+
)
|
|
640
|
+
)) privilege
|
|
641
|
+
where namespace.nspname='agent_bridge' and relation.relname in(select relation_name from protected_relations)
|
|
642
|
+
union all
|
|
643
|
+
select 'rate_policy',policy.policy_id,
|
|
644
|
+
coalesce(policy.operation_id,'')||':'||policy.capacity::text||':'||
|
|
645
|
+
policy.refill_per_second::text||':'||policy.enabled::text
|
|
646
|
+
from agent_bridge.rate_limit_policies policy
|
|
647
|
+
where policy.policy_id in (
|
|
648
|
+
'operation:issue_endpoint_migration_challenge',
|
|
649
|
+
'operation:consume_endpoint_migration_challenge'
|
|
650
|
+
)
|
|
651
|
+
) select string_agg(kind||E'\x1f'||identity||E'\x1f'||definition,E'\x1e' order by kind,identity,definition) from catalog
|
|
652
|
+
$$;
|
|
653
|
+
|
|
654
|
+
create function agent_bridge.endpoint_migration_challenge_ready()
|
|
655
|
+
returns boolean language sql stable security definer set search_path = '' as $$
|
|
656
|
+
with names as (
|
|
657
|
+
select ('agent_bridge_runtime_'||substr(md5(current_database()),1,16))::name runtime_role
|
|
658
|
+
) select
|
|
659
|
+
(select count(*)=1 and bool_and(catalog_definition=agent_bridge.endpoint_migration_challenge_catalog_definition())
|
|
660
|
+
from agent_bridge.endpoint_migration_challenge_attestations where name='endpoint-migration-v1')
|
|
661
|
+
and (select count(*)=1 from agent_bridge.gateway_authority where singleton)
|
|
662
|
+
and not exists(select 1 from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
663
|
+
where namespace.nspname='agent_bridge' and relation.relname in (
|
|
664
|
+
'endpoint_migration_challenges','endpoint_migration_challenge_events','endpoint_migration_challenge_attestations'
|
|
665
|
+
) and (relation.relrowsecurity or relation.relforcerowsecurity))
|
|
666
|
+
and not exists(select 1 from unnest(array[
|
|
667
|
+
'agent_bridge.endpoint_migration_challenges','agent_bridge.endpoint_migration_challenge_events',
|
|
668
|
+
'agent_bridge.endpoint_migration_challenge_attestations'
|
|
669
|
+
]) relation_name cross join unnest(array['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']) privilege
|
|
670
|
+
where has_table_privilege('public',relation_name,privilege)
|
|
671
|
+
or has_table_privilege((select runtime_role from names),relation_name,privilege))
|
|
672
|
+
and not exists(select 1 from unnest(array[
|
|
673
|
+
'agent_bridge.endpoint_migration_challenges','agent_bridge.endpoint_migration_challenge_events',
|
|
674
|
+
'agent_bridge.endpoint_migration_challenge_attestations'
|
|
675
|
+
]) relation_name cross join unnest(array['SELECT','INSERT','UPDATE','REFERENCES']) privilege
|
|
676
|
+
where has_any_column_privilege('public',relation_name,privilege)
|
|
677
|
+
or has_any_column_privilege((select runtime_role from names),relation_name,privilege))
|
|
678
|
+
and has_function_privilege((select runtime_role from names),
|
|
679
|
+
'agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text)','EXECUTE')
|
|
680
|
+
and has_function_privilege((select runtime_role from names),
|
|
681
|
+
'agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text)','EXECUTE')
|
|
682
|
+
and not has_function_privilege('public',
|
|
683
|
+
'agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text)','EXECUTE')
|
|
684
|
+
and not has_function_privilege('public',
|
|
685
|
+
'agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text)','EXECUTE')
|
|
686
|
+
and not exists(select 1 from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
687
|
+
where namespace.nspname='agent_bridge' and procedure.proname in (
|
|
688
|
+
'cleanup_endpoint_migration_challenges','endpoint_migration_challenge_catalog_definition',
|
|
689
|
+
'endpoint_migration_challenge_ready','guard_endpoint_migration_challenge',
|
|
690
|
+
'reject_endpoint_migration_challenge_truncate','reject_endpoint_migration_challenge_event_mutation',
|
|
691
|
+
'reject_endpoint_migration_challenge_attestation_mutation',
|
|
692
|
+
'endpoint_migration_challenge_active_credential','endpoint_migration_challenge_direct_lineage'
|
|
693
|
+
) and has_function_privilege((select runtime_role from names),procedure.oid,'EXECUTE'))
|
|
694
|
+
$$;
|
|
695
|
+
|
|
696
|
+
create or replace function agent_bridge.security_schema_ready()
|
|
697
|
+
returns boolean language plpgsql stable security definer set search_path = '' as $$
|
|
698
|
+
declare
|
|
699
|
+
runtime_role text := 'agent_bridge_runtime_'||substr(md5(current_database()),1,16);
|
|
700
|
+
begin
|
|
701
|
+
return
|
|
702
|
+
(select scopes=array[
|
|
703
|
+
'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
|
|
704
|
+
'gateway:metrics','messages:read','messages:write','presence:read',
|
|
705
|
+
'presence:write','receipts:write','status:read'
|
|
706
|
+
]::text[] from agent_bridge.credential_scope_sets where name='release-a-full')
|
|
707
|
+
and not exists(with expected(policy_id,operation_id,capacity,refill_per_second) as (values
|
|
708
|
+
('global',null::text,300,50::numeric),
|
|
709
|
+
('operation:capabilities','capabilities',30,1),('operation:status','status',30,1),
|
|
710
|
+
('operation:gateway_metrics','gateway_metrics',30,1),('operation:publish_message','publish_message',120,20),
|
|
711
|
+
('operation:history','history',120,20),('operation:record_receipt','record_receipt',120,20),
|
|
712
|
+
('operation:claim_delivery','claim_delivery',120,20),('operation:list_deliveries','list_deliveries',120,20),
|
|
713
|
+
('operation:list_delivery_events','list_delivery_events',120,20),('operation:cancel_delivery','cancel_delivery',60,10),
|
|
714
|
+
('operation:requeue_delivery','requeue_delivery',60,10),('operation:extend_delivery','extend_delivery',120,20),
|
|
715
|
+
('operation:acknowledge_delivery','acknowledge_delivery',120,20),('operation:negative_acknowledge_delivery','negative_acknowledge_delivery',120,20),
|
|
716
|
+
('operation:heartbeat','heartbeat',120,20),('operation:presence','presence',120,20),
|
|
717
|
+
('operation:issue_endpoint_migration_challenge','issue_endpoint_migration_challenge',30,1),
|
|
718
|
+
('operation:consume_endpoint_migration_challenge','consume_endpoint_migration_challenge',30,1)
|
|
719
|
+
) select 1 from expected left join agent_bridge.rate_limit_policies policy using(policy_id)
|
|
720
|
+
where policy.policy_id is null or policy.operation_id is distinct from expected.operation_id
|
|
721
|
+
or policy.capacity is distinct from expected.capacity or policy.refill_per_second is distinct from expected.refill_per_second
|
|
722
|
+
or not policy.enabled)
|
|
723
|
+
and (select count(*)=19 from agent_bridge.rate_limit_policies)
|
|
724
|
+
and not exists (
|
|
725
|
+
select 1 from (values
|
|
726
|
+
('security_events_append_only','agent_bridge.security_events'::regclass),
|
|
727
|
+
('credentials_validate_security','agent_bridge.credentials'::regclass),
|
|
728
|
+
('credentials_append_only','agent_bridge.credentials'::regclass),
|
|
729
|
+
('credential_scope_sets_immutable','agent_bridge.credential_scope_sets'::regclass)
|
|
730
|
+
) required(trigger_name,relation_id)
|
|
731
|
+
left join pg_catalog.pg_trigger trigger on trigger.tgname=required.trigger_name
|
|
732
|
+
and trigger.tgrelid=required.relation_id and not trigger.tgisinternal
|
|
733
|
+
where trigger.oid is null or trigger.tgenabled<>'O'
|
|
734
|
+
)
|
|
735
|
+
and not exists (
|
|
736
|
+
select 1 from (values
|
|
737
|
+
('agent_bridge.credentials'::regclass,'credentials_scopes_canonical'),
|
|
738
|
+
('agent_bridge.credentials'::regclass,'credentials_replacement_not_self'),
|
|
739
|
+
('agent_bridge.credentials'::regclass,'credentials_grace_shortens_expiry'),
|
|
740
|
+
('agent_bridge.credential_scope_sets'::regclass,'credential_scope_sets_scopes'),
|
|
741
|
+
('agent_bridge.security_events'::regclass,'security_events_type'),
|
|
742
|
+
('agent_bridge.security_events'::regclass,'security_events_outcome'),
|
|
743
|
+
('agent_bridge.security_events'::regclass,'security_events_reason'),
|
|
744
|
+
('agent_bridge.security_events'::regclass,'security_events_retry'),
|
|
745
|
+
('agent_bridge.security_events'::regclass,'security_events_actor'),
|
|
746
|
+
('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_shape'),
|
|
747
|
+
('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_operation'),
|
|
748
|
+
('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_capacity'),
|
|
749
|
+
('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_refill'),
|
|
750
|
+
('agent_bridge.rate_limit_buckets'::regclass,'rate_limit_buckets_tokens')
|
|
751
|
+
) required(relation_id,constraint_name)
|
|
752
|
+
left join pg_catalog.pg_constraint constraint_record
|
|
753
|
+
on constraint_record.conrelid=required.relation_id and constraint_record.conname=required.constraint_name
|
|
754
|
+
where constraint_record.oid is null or not constraint_record.convalidated
|
|
755
|
+
)
|
|
756
|
+
and exists(select 1 from pg_catalog.pg_attribute attribute
|
|
757
|
+
where attribute.attrelid='agent_bridge.credentials'::regclass and attribute.attname='scopes' and attribute.attnotnull)
|
|
758
|
+
and pg_catalog.to_regclass('agent_bridge.credentials_replacement_lineage') is not null
|
|
759
|
+
and pg_catalog.to_regclass('agent_bridge.rate_limit_buckets_cleanup') is not null
|
|
760
|
+
and pg_catalog.to_regprocedure('agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)') is not null
|
|
761
|
+
and pg_catalog.to_regprocedure('agent_bridge.revoke_credential(uuid,text,text,uuid)') is not null
|
|
762
|
+
and pg_catalog.to_regprocedure('agent_bridge.record_scope_denial_unbound_011(uuid,text,uuid)') is not null
|
|
763
|
+
and pg_catalog.to_regprocedure('agent_bridge.consume_rate_limit_unbound_011(uuid,text,uuid)') is not null
|
|
764
|
+
and pg_catalog.to_regprocedure('agent_bridge.consume_rate_limit(uuid,text,uuid)') is not null
|
|
765
|
+
and pg_catalog.to_regprocedure('agent_bridge.endpoint_migration_challenge_ready()') is not null
|
|
766
|
+
and (select count(*)=5 and bool_and(procedure.prosecdef
|
|
767
|
+
and procedure.proconfig @> array['search_path=""']::text[])
|
|
768
|
+
from pg_catalog.pg_proc procedure where procedure.oid in (
|
|
769
|
+
'agent_bridge.record_scope_denial(uuid,text,uuid)'::regprocedure,
|
|
770
|
+
'agent_bridge.consume_rate_limit(uuid,text,uuid)'::regprocedure,
|
|
771
|
+
'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)'::regprocedure,
|
|
772
|
+
'agent_bridge.revoke_credential(uuid,text,text,uuid)'::regprocedure,
|
|
773
|
+
'agent_bridge.security_schema_ready()'::regprocedure
|
|
774
|
+
))
|
|
775
|
+
and not exists(select 1 from pg_catalog.pg_proc procedure
|
|
776
|
+
cross join lateral pg_catalog.aclexplode(coalesce(procedure.proacl,pg_catalog.acldefault('f',procedure.proowner))) access
|
|
777
|
+
where procedure.oid in (
|
|
778
|
+
'agent_bridge.record_scope_denial(uuid,text,uuid)'::regprocedure,
|
|
779
|
+
'agent_bridge.consume_rate_limit(uuid,text,uuid)'::regprocedure,
|
|
780
|
+
'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)'::regprocedure,
|
|
781
|
+
'agent_bridge.revoke_credential(uuid,text,text,uuid)'::regprocedure,
|
|
782
|
+
'agent_bridge.security_schema_ready()'::regprocedure
|
|
783
|
+
) and access.grantee=0 and access.privilege_type='EXECUTE')
|
|
784
|
+
and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.record_scope_denial(uuid,text,uuid)','EXECUTE')
|
|
785
|
+
and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.consume_rate_limit(uuid,text,uuid)','EXECUTE')
|
|
786
|
+
and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.security_schema_ready()','EXECUTE')
|
|
787
|
+
and not pg_catalog.has_function_privilege(runtime_role,'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)','EXECUTE')
|
|
788
|
+
and not pg_catalog.has_function_privilege(runtime_role,'agent_bridge.revoke_credential(uuid,text,text,uuid)','EXECUTE')
|
|
789
|
+
and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.credential_scope_sets','SELECT,INSERT,UPDATE,DELETE')
|
|
790
|
+
and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.security_events','SELECT,INSERT,UPDATE,DELETE')
|
|
791
|
+
and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.rate_limit_policies','SELECT,INSERT,UPDATE,DELETE')
|
|
792
|
+
and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.rate_limit_buckets','SELECT,INSERT,UPDATE,DELETE')
|
|
793
|
+
and agent_bridge.endpoint_migration_challenge_ready();
|
|
794
|
+
end
|
|
795
|
+
$$;
|
|
796
|
+
|
|
797
|
+
create or replace function agent_bridge.credential_security_prerequisite_definition()
|
|
798
|
+
returns text language sql stable set search_path = '' set timezone = 'UTC' as $$
|
|
799
|
+
with dependency_relations(relation_name) as (values
|
|
800
|
+
('credentials'),('credential_scope_sets'),('security_events'),
|
|
801
|
+
('rate_limit_policies'),('rate_limit_buckets'),('endpoint_migration_challenges'),
|
|
802
|
+
('endpoint_migration_challenge_events'),('endpoint_migration_challenge_attestations')
|
|
803
|
+
), protected_functions(function_name) as (values
|
|
804
|
+
('canonicalize_scopes'),('validate_credential_security'),
|
|
805
|
+
('reject_credential_delete'),('reject_scope_set_mutation'),
|
|
806
|
+
('reject_security_event_mutation'),('record_scope_denial'),
|
|
807
|
+
('record_scope_denial_unbound_011'),('consume_rate_limit'),
|
|
808
|
+
('consume_rate_limit_unbound_011'),('replace_credential'),
|
|
809
|
+
('revoke_credential'),('security_schema_ready'),
|
|
810
|
+
('guard_endpoint_migration_challenge'),
|
|
811
|
+
('reject_endpoint_migration_challenge_truncate'),
|
|
812
|
+
('reject_endpoint_migration_challenge_event_mutation'),
|
|
813
|
+
('reject_endpoint_migration_challenge_attestation_mutation'),
|
|
814
|
+
('cleanup_endpoint_migration_challenges'),
|
|
815
|
+
('endpoint_migration_challenge_active_credential'),
|
|
816
|
+
('endpoint_migration_challenge_direct_lineage'),
|
|
817
|
+
('issue_endpoint_migration_challenge'),
|
|
818
|
+
('consume_endpoint_migration_challenge'),
|
|
819
|
+
('endpoint_migration_challenge_catalog_definition'),
|
|
820
|
+
('endpoint_migration_challenge_ready')
|
|
821
|
+
), catalog_objects(kind,identity,definition) as (
|
|
822
|
+
select 'relation',relation.relname,
|
|
823
|
+
case when relation.relowner=namespace.nspowner then 'schema-owner' else 'unexpected-owner' end
|
|
824
|
+
||':'||relation.relkind::text||':'||relation.relpersistence::text||':'||
|
|
825
|
+
relation.relrowsecurity::text||':'||relation.relforcerowsecurity::text
|
|
826
|
+
from pg_catalog.pg_class relation
|
|
827
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
828
|
+
where namespace.nspname='agent_bridge'
|
|
829
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
830
|
+
union all
|
|
831
|
+
select 'column',relation.relname||'.'||attribute.attname,
|
|
832
|
+
pg_catalog.format_type(attribute.atttypid,attribute.atttypmod)||':'||
|
|
833
|
+
attribute.attnotnull::text||':'||attribute.attidentity::text||':'||
|
|
834
|
+
attribute.attgenerated::text||':'||coalesce(pg_catalog.pg_get_expr(
|
|
835
|
+
default_record.adbin,default_record.adrelid),'')
|
|
836
|
+
from pg_catalog.pg_attribute attribute
|
|
837
|
+
join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
838
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
839
|
+
left join pg_catalog.pg_attrdef default_record on default_record.adrelid=attribute.attrelid
|
|
840
|
+
and default_record.adnum=attribute.attnum
|
|
841
|
+
where namespace.nspname='agent_bridge'
|
|
842
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
843
|
+
and attribute.attnum>0 and not attribute.attisdropped
|
|
844
|
+
union all
|
|
845
|
+
select 'constraint',relation.relname||'.'||constraint_record.conname,
|
|
846
|
+
constraint_record.convalidated::text||':'||
|
|
847
|
+
pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
848
|
+
from pg_catalog.pg_constraint constraint_record
|
|
849
|
+
join pg_catalog.pg_class relation on relation.oid=constraint_record.conrelid
|
|
850
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
851
|
+
where namespace.nspname='agent_bridge'
|
|
852
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
853
|
+
union all
|
|
854
|
+
select 'index',index_relation.relname,
|
|
855
|
+
index_record.indisunique::text||':'||index_record.indisvalid::text||':'||
|
|
856
|
+
index_record.indisready::text||':'||pg_catalog.pg_get_indexdef(index_record.indexrelid)||':'||
|
|
857
|
+
coalesce(pg_catalog.pg_get_expr(index_record.indpred,index_record.indrelid),'')
|
|
858
|
+
from pg_catalog.pg_index index_record
|
|
859
|
+
join pg_catalog.pg_class source_relation on source_relation.oid=index_record.indrelid
|
|
860
|
+
join pg_catalog.pg_class index_relation on index_relation.oid=index_record.indexrelid
|
|
861
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=source_relation.relnamespace
|
|
862
|
+
where namespace.nspname='agent_bridge'
|
|
863
|
+
and source_relation.relname in (select relation_name from dependency_relations)
|
|
864
|
+
union all
|
|
865
|
+
select 'trigger',relation.relname||'.'||trigger.tgname,
|
|
866
|
+
trigger.tgenabled::text||':'||pg_catalog.pg_get_triggerdef(trigger.oid,true)
|
|
867
|
+
from pg_catalog.pg_trigger trigger
|
|
868
|
+
join pg_catalog.pg_class relation on relation.oid=trigger.tgrelid
|
|
869
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
870
|
+
where not trigger.tgisinternal and namespace.nspname='agent_bridge'
|
|
871
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
872
|
+
union all
|
|
873
|
+
select 'function',procedure.oid::regprocedure::text,
|
|
874
|
+
case when procedure.proowner=namespace.nspowner then 'schema-owner' else 'unexpected-owner' end
|
|
875
|
+
||':'||pg_catalog.pg_get_functiondef(procedure.oid)
|
|
876
|
+
from pg_catalog.pg_proc procedure
|
|
877
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
878
|
+
where namespace.nspname='agent_bridge'
|
|
879
|
+
and procedure.proname in (select function_name from protected_functions)
|
|
880
|
+
union all
|
|
881
|
+
select 'endpoint_catalog','v1',agent_bridge.endpoint_migration_challenge_catalog_definition()
|
|
882
|
+
)
|
|
883
|
+
select jsonb_agg(jsonb_build_object(
|
|
884
|
+
'kind',kind,'identity',identity,'definition',definition
|
|
885
|
+
) order by kind,identity,definition)::text from catalog_objects
|
|
886
|
+
$$;
|
|
887
|
+
|
|
888
|
+
create or replace function agent_bridge.owner_control_plane_ready()
|
|
889
|
+
returns boolean language sql stable security definer set search_path = '' as $$
|
|
890
|
+
with names as (select
|
|
891
|
+
('agent_bridge_control_owner_'||substr(md5(current_database()),1,16))::name owner_role,
|
|
892
|
+
('agent_bridge_control_operator_'||substr(md5(current_database()),1,16))::name operator_role,
|
|
893
|
+
('agent_bridge_control_auditor_'||substr(md5(current_database()),1,16))::name auditor_role,
|
|
894
|
+
(select pg_catalog.pg_get_userbyid(nspowner)::name from pg_catalog.pg_namespace where nspname='agent_bridge') schema_owner
|
|
895
|
+
), latest_registry as (
|
|
896
|
+
select distinct on (event.member_role,event.control_role) event.member_role,event.control_role,event.action
|
|
897
|
+
from agent_bridge.control_membership_events event
|
|
898
|
+
order by event.member_role,event.control_role,event.sequence desc
|
|
899
|
+
), active_registry as (
|
|
900
|
+
select member_role,control_role from latest_registry where action='register'
|
|
901
|
+
), expected_memberships as (
|
|
902
|
+
select names.owner_role granted_role,names.schema_owner member_role,
|
|
903
|
+
true admin_option,true inherit_option,true set_option from names
|
|
904
|
+
union all select names.operator_role,names.schema_owner,true,true,true from names
|
|
905
|
+
union all select names.auditor_role,names.schema_owner,true,true,true from names
|
|
906
|
+
union all select case registry.control_role when 'operator' then names.operator_role else names.auditor_role end,
|
|
907
|
+
registry.member_role,false,true,true from active_registry registry cross join names
|
|
908
|
+
), raw_memberships as (
|
|
909
|
+
select granted.rolname::name granted_role,member.rolname::name member_role,
|
|
910
|
+
membership.admin_option,
|
|
911
|
+
coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
|
|
912
|
+
coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
|
|
913
|
+
membership.grantor,member.oid member_oid
|
|
914
|
+
from pg_catalog.pg_auth_members membership
|
|
915
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
916
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
917
|
+
where granted.rolname in ((select owner_role from names),(select operator_role from names),(select auditor_role from names))
|
|
918
|
+
), actual_memberships as (
|
|
919
|
+
select membership.granted_role,membership.member_role,
|
|
920
|
+
bool_or(membership.admin_option) admin_option,
|
|
921
|
+
bool_or(membership.inherit_option) inherit_option,
|
|
922
|
+
bool_or(membership.set_option) set_option,
|
|
923
|
+
bool_and(case when membership.member_role=names.schema_owner then
|
|
924
|
+
(membership.grantor=10 and membership.admin_option
|
|
925
|
+
and membership.inherit_option=membership.set_option)
|
|
926
|
+
or (membership.grantor=membership.member_oid
|
|
927
|
+
and membership.inherit_option and membership.set_option)
|
|
928
|
+
else membership.grantor=(select oid from pg_catalog.pg_roles where rolname=names.schema_owner)
|
|
929
|
+
and not membership.admin_option
|
|
930
|
+
and membership.inherit_option and membership.set_option end) grants_valid
|
|
931
|
+
from raw_memberships membership cross join names
|
|
932
|
+
group by membership.granted_role,membership.member_role
|
|
933
|
+
), authority_closure as (
|
|
934
|
+
select control_role.granted_role,candidate.rolname::name member_role
|
|
935
|
+
from names cross join lateral(values(names.owner_role),(names.operator_role),(names.auditor_role)) control_role(granted_role)
|
|
936
|
+
cross join pg_catalog.pg_roles candidate
|
|
937
|
+
where candidate.rolname not in(names.owner_role,names.operator_role,names.auditor_role)
|
|
938
|
+
and(not candidate.rolsuper or candidate.rolname=names.schema_owner)
|
|
939
|
+
and pg_catalog.pg_has_role(candidate.rolname,control_role.granted_role,'MEMBER')
|
|
940
|
+
) select
|
|
941
|
+
current_setting('server_version_num')::integer/10000=any(array[15,16,17,18])
|
|
942
|
+
and(select count(*)=1 and bool_and(catalog_definition=agent_bridge.owner_control_attestation_definition())
|
|
943
|
+
from agent_bridge.owner_control_attestations where name='owner-control-v5')
|
|
944
|
+
and not exists(
|
|
945
|
+
(select granted_role,member_role,admin_option,inherit_option,set_option from actual_memberships
|
|
946
|
+
except select granted_role,member_role,admin_option,inherit_option,set_option from expected_memberships)
|
|
947
|
+
union all(select granted_role,member_role,admin_option,inherit_option,set_option from expected_memberships
|
|
948
|
+
except select granted_role,member_role,admin_option,inherit_option,set_option from actual_memberships)
|
|
949
|
+
)
|
|
950
|
+
and not exists(select 1 from actual_memberships where not grants_valid)
|
|
951
|
+
and not exists(select 1 from active_registry registry left join pg_catalog.pg_roles role_record on role_record.rolname=registry.member_role
|
|
952
|
+
where role_record.oid is null or not role_record.rolcanlogin or role_record.rolsuper or role_record.rolcreaterole
|
|
953
|
+
or role_record.rolcreatedb or role_record.rolreplication or role_record.rolbypassrls)
|
|
954
|
+
and not exists(
|
|
955
|
+
(select granted_role,member_role from authority_closure except select granted_role,member_role from expected_memberships)
|
|
956
|
+
union all(select granted_role,member_role from expected_memberships except select granted_role,member_role from authority_closure)
|
|
957
|
+
)
|
|
958
|
+
and not exists(select 1 from active_registry registry join pg_catalog.pg_roles inherited on inherited.rolname<>registry.member_role
|
|
959
|
+
where pg_catalog.pg_has_role(registry.member_role,inherited.rolname,'MEMBER') and not exists(
|
|
960
|
+
select 1 from active_registry permitted cross join names where permitted.member_role=registry.member_role
|
|
961
|
+
and inherited.rolname=case permitted.control_role when 'operator' then names.operator_role else names.auditor_role end
|
|
962
|
+
))
|
|
963
|
+
and not exists(select 1 from active_registry registry cross join pg_catalog.pg_roles candidate,names
|
|
964
|
+
where candidate.rolname not in(registry.member_role,names.schema_owner) and not candidate.rolsuper
|
|
965
|
+
and pg_catalog.pg_has_role(candidate.rolname,registry.member_role,'MEMBER'))
|
|
966
|
+
$$;
|
|
967
|
+
|
|
968
|
+
revoke all on agent_bridge.endpoint_migration_challenges,
|
|
969
|
+
agent_bridge.endpoint_migration_challenge_events,
|
|
970
|
+
agent_bridge.endpoint_migration_challenge_attestations from public;
|
|
971
|
+
revoke all on all sequences in schema agent_bridge from public;
|
|
972
|
+
revoke all on function agent_bridge.guard_endpoint_migration_challenge() from public;
|
|
973
|
+
revoke all on function agent_bridge.reject_endpoint_migration_challenge_truncate() from public;
|
|
974
|
+
revoke all on function agent_bridge.reject_endpoint_migration_challenge_event_mutation() from public;
|
|
975
|
+
revoke all on function agent_bridge.reject_endpoint_migration_challenge_attestation_mutation() from public;
|
|
976
|
+
revoke all on function agent_bridge.cleanup_endpoint_migration_challenges() from public;
|
|
977
|
+
revoke all on function agent_bridge.endpoint_migration_challenge_active_credential(uuid,text,text) from public;
|
|
978
|
+
revoke all on function agent_bridge.endpoint_migration_challenge_direct_lineage(uuid,uuid) from public;
|
|
979
|
+
revoke all on function agent_bridge.endpoint_migration_challenge_catalog_definition() from public;
|
|
980
|
+
revoke all on function agent_bridge.endpoint_migration_challenge_ready() from public;
|
|
981
|
+
revoke all on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) from public;
|
|
982
|
+
revoke all on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) from public;
|
|
983
|
+
|
|
984
|
+
do $ownership_and_grants$
|
|
985
|
+
declare
|
|
986
|
+
runtime_role text := 'agent_bridge_runtime_'||substr(md5(current_database()),1,16);
|
|
987
|
+
control_owner text := 'agent_bridge_control_owner_'||substr(md5(current_database()),1,16);
|
|
988
|
+
role_name text;
|
|
989
|
+
begin
|
|
990
|
+
execute format('grant create on schema agent_bridge to %I',control_owner);
|
|
991
|
+
execute format('alter function agent_bridge.owner_control_plane_ready() owner to %I',control_owner);
|
|
992
|
+
execute format('revoke create on schema agent_bridge from %I',control_owner);
|
|
993
|
+
foreach role_name in array array[
|
|
994
|
+
'guard_endpoint_migration_challenge()',
|
|
995
|
+
'reject_endpoint_migration_challenge_truncate()',
|
|
996
|
+
'reject_endpoint_migration_challenge_event_mutation()',
|
|
997
|
+
'reject_endpoint_migration_challenge_attestation_mutation()',
|
|
998
|
+
'cleanup_endpoint_migration_challenges()',
|
|
999
|
+
'endpoint_migration_challenge_active_credential(uuid,text,text)',
|
|
1000
|
+
'endpoint_migration_challenge_direct_lineage(uuid,uuid)',
|
|
1001
|
+
'endpoint_migration_challenge_catalog_definition()',
|
|
1002
|
+
'endpoint_migration_challenge_ready()',
|
|
1003
|
+
'issue_endpoint_migration_challenge(uuid,uuid,text)',
|
|
1004
|
+
'consume_endpoint_migration_challenge(uuid,uuid,text)'
|
|
1005
|
+
] loop
|
|
1006
|
+
execute format('alter function agent_bridge.%s owner to %I',role_name,current_user);
|
|
1007
|
+
end loop;
|
|
1008
|
+
foreach role_name in array array[runtime_role,'anon','authenticated'] loop
|
|
1009
|
+
if exists(select 1 from pg_catalog.pg_roles where rolname=role_name) then
|
|
1010
|
+
execute format('revoke all on agent_bridge.endpoint_migration_challenges,agent_bridge.endpoint_migration_challenge_events,agent_bridge.endpoint_migration_challenge_attestations from %I',role_name);
|
|
1011
|
+
execute format('revoke all on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) from %I',role_name);
|
|
1012
|
+
execute format('revoke all on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) from %I',role_name);
|
|
1013
|
+
execute format('revoke all on function agent_bridge.cleanup_endpoint_migration_challenges() from %I',role_name);
|
|
1014
|
+
execute format('revoke all on function agent_bridge.endpoint_migration_challenge_catalog_definition() from %I',role_name);
|
|
1015
|
+
execute format('revoke all on function agent_bridge.endpoint_migration_challenge_ready() from %I',role_name);
|
|
1016
|
+
end if;
|
|
1017
|
+
end loop;
|
|
1018
|
+
execute format('revoke all on agent_bridge.request_authorities from %I',runtime_role);
|
|
1019
|
+
execute format('grant execute on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) to %I',runtime_role);
|
|
1020
|
+
execute format('grant execute on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) to %I',runtime_role);
|
|
1021
|
+
execute format('grant execute on function agent_bridge.endpoint_migration_challenge_catalog_definition() to %I',control_owner);
|
|
1022
|
+
execute format('grant execute on function agent_bridge.owner_control_plane_ready() to %I',runtime_role);
|
|
1023
|
+
end
|
|
1024
|
+
$ownership_and_grants$;
|
|
1025
|
+
|
|
1026
|
+
insert into agent_bridge.endpoint_migration_challenge_attestations(name,catalog_definition)
|
|
1027
|
+
values('endpoint-migration-v1',agent_bridge.endpoint_migration_challenge_catalog_definition());
|
|
1028
|
+
select set_config(
|
|
1029
|
+
'role','agent_bridge_control_owner_'||substr(md5(current_database()),1,16),false
|
|
1030
|
+
);
|
|
1031
|
+
insert into agent_bridge.owner_control_attestations(name,catalog_definition)
|
|
1032
|
+
values('owner-control-v5',agent_bridge.owner_control_attestation_definition());
|
|
1033
|
+
reset role;
|
|
1034
|
+
|
|
1035
|
+
do $final_readiness$
|
|
1036
|
+
begin
|
|
1037
|
+
if not agent_bridge.security_schema_ready() then
|
|
1038
|
+
raise exception 'endpoint migration challenge final security readiness validation failed';
|
|
1039
|
+
elseif not agent_bridge.owner_control_plane_ready() then
|
|
1040
|
+
raise exception 'endpoint migration challenge final owner readiness validation failed';
|
|
1041
|
+
elseif not agent_bridge.gateway_authority_ready() then
|
|
1042
|
+
raise exception 'endpoint migration challenge final gateway authority readiness validation failed';
|
|
1043
|
+
elseif not agent_bridge.endpoint_migration_challenge_ready() then
|
|
1044
|
+
raise exception 'endpoint migration challenge final endpoint readiness validation failed';
|
|
1045
|
+
elseif not agent_bridge.portable_archive_ready() then
|
|
1046
|
+
raise exception 'endpoint migration challenge final portable archive readiness validation failed';
|
|
1047
|
+
end if;
|
|
1048
|
+
end
|
|
1049
|
+
$final_readiness$;
|
|
1050
|
+
|
|
1051
|
+
insert into agent_bridge.schema_migrations(version,name,checksum)
|
|
1052
|
+
values(18,'endpoint_migration_challenges','__AGENT_BRIDGE_MIGRATION_CHECKSUM__');
|
|
1053
|
+
|
|
1054
|
+
commit;
|