@creatornader/agent-bridge 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +150 -1
  2. package/README.md +239 -5
  3. package/ROADMAP.md +59 -7
  4. package/SECURITY.md +55 -5
  5. package/SKILL.md +103 -0
  6. package/dist/archive.d.ts +2 -2
  7. package/dist/archive.js +2 -2
  8. package/dist/{bridge-service-hX1ZQp4W.d.ts → bridge-service-YTjGr91A.d.ts} +1 -1
  9. package/dist/{bridge-store-DPVSCwfs.d.ts → bridge-store-Boaj-ZbO.d.ts} +2 -0
  10. package/dist/bridge.d.ts +17 -7
  11. package/dist/bridge.js +15 -7
  12. package/dist/{chunk-WAI2IYIQ.js → chunk-3YHVUPU2.js} +48 -5
  13. package/dist/chunk-3YHVUPU2.js.map +1 -0
  14. package/dist/{chunk-45BZGB77.js → chunk-6BTJE6D4.js} +168 -7
  15. package/dist/chunk-6BTJE6D4.js.map +1 -0
  16. package/dist/{chunk-5GMB3A77.js → chunk-DQOZJJZ2.js} +22 -10
  17. package/dist/chunk-DQOZJJZ2.js.map +1 -0
  18. package/dist/{chunk-NCO7RLBM.js → chunk-GTK4KOED.js} +108 -8
  19. package/dist/chunk-GTK4KOED.js.map +1 -0
  20. package/dist/{chunk-GADH244D.js → chunk-IHZFI7TH.js} +12 -4
  21. package/dist/chunk-IHZFI7TH.js.map +1 -0
  22. package/dist/{chunk-CEXBMOI5.js → chunk-LAHNQZDN.js} +2 -2
  23. package/dist/{chunk-UAHMP6GC.js → chunk-MCKKVNCE.js} +9 -6
  24. package/dist/chunk-MCKKVNCE.js.map +1 -0
  25. package/dist/{chunk-MBPZXTJA.js → chunk-PDIHDBP3.js} +173 -20
  26. package/dist/chunk-PDIHDBP3.js.map +1 -0
  27. package/dist/{chunk-Q6PPMXJC.js → chunk-SV3SVBYS.js} +377 -21
  28. package/dist/chunk-SV3SVBYS.js.map +1 -0
  29. package/dist/{chunk-SPSMZOCJ.js → chunk-SYGONYMM.js} +4 -4
  30. package/dist/{chunk-A6W4QALF.js → chunk-TC6A7GMF.js} +2 -2
  31. package/dist/cli.js +5865 -436
  32. package/dist/cli.js.map +1 -1
  33. package/dist/client-runtime.d.ts +4 -2
  34. package/dist/client-runtime.js +6 -6
  35. package/dist/contracts/generate.js +1 -1
  36. package/dist/dr.d.ts +9 -2
  37. package/dist/dr.js +6 -4
  38. package/dist/{gateway-CggQ8vTC.d.ts → gateway-BQHpV-MZ.d.ts} +27 -3
  39. package/dist/gateway-main.js +69 -5
  40. package/dist/gateway-main.js.map +1 -1
  41. package/dist/gateway.d.ts +3 -3
  42. package/dist/gateway.js +2 -2
  43. package/dist/index.js +7 -7
  44. package/dist/{postgres-bridge-store-Bfd3hRId.d.ts → postgres-bridge-store-WASumj26.d.ts} +1 -1
  45. package/dist/server.js +7 -7
  46. package/dist/sqlite-edge-store-Bc15DvpX.d.ts +148 -0
  47. package/dist/sqlite.d.ts +19 -80
  48. package/dist/sqlite.js +11 -3
  49. package/docs/architecture-v2.md +256 -10
  50. package/docs/decisions/0004-client-lifecycle-and-endpoint-migration.md +204 -0
  51. package/docs/deployment.md +205 -0
  52. package/docs/troubleshooting.md +23 -0
  53. package/llms.txt +67 -0
  54. package/openapi/agent-bridge-v2.openapi.json +1023 -0
  55. package/package.json +6 -2
  56. package/schemas/agent-bridge-v2.capabilities.json +24 -0
  57. package/schemas/agent-bridge-v2.mcp.json +6 -0
  58. package/schemas/agent-bridge-v2.schema.json +163 -0
  59. package/sql/migrations/017_gateway_authority_binding.sql +99 -0
  60. package/sql/migrations/018_endpoint_migration_challenges.sql +1028 -0
  61. package/sql/migrations/019_endpoint_migration_same_successor.sql +324 -0
  62. package/dist/chunk-45BZGB77.js.map +0 -1
  63. package/dist/chunk-5GMB3A77.js.map +0 -1
  64. package/dist/chunk-GADH244D.js.map +0 -1
  65. package/dist/chunk-MBPZXTJA.js.map +0 -1
  66. package/dist/chunk-NCO7RLBM.js.map +0 -1
  67. package/dist/chunk-Q6PPMXJC.js.map +0 -1
  68. package/dist/chunk-UAHMP6GC.js.map +0 -1
  69. package/dist/chunk-WAI2IYIQ.js.map +0 -1
  70. /package/dist/{chunk-CEXBMOI5.js.map → chunk-LAHNQZDN.js.map} +0 -0
  71. /package/dist/{chunk-SPSMZOCJ.js.map → chunk-SYGONYMM.js.map} +0 -0
  72. /package/dist/{chunk-A6W4QALF.js.map → chunk-TC6A7GMF.js.map} +0 -0
@@ -0,0 +1,1028 @@
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,true admin_option from names
903
+ union all select names.operator_role,names.schema_owner,true from names
904
+ union all select names.auditor_role,names.schema_owner,true from names
905
+ union all select case registry.control_role when 'operator' then names.operator_role else names.auditor_role end,
906
+ registry.member_role,false from active_registry registry cross join names
907
+ ), actual_memberships as (
908
+ select granted.rolname::name granted_role,member.rolname::name member_role,membership.admin_option,
909
+ coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
910
+ coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option
911
+ from pg_catalog.pg_auth_members membership
912
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
913
+ join pg_catalog.pg_roles member on member.oid=membership.member
914
+ where granted.rolname in ((select owner_role from names),(select operator_role from names),(select auditor_role from names))
915
+ ), authority_closure as (
916
+ select control_role.granted_role,candidate.rolname::name member_role
917
+ from names cross join lateral(values(names.owner_role),(names.operator_role),(names.auditor_role)) control_role(granted_role)
918
+ cross join pg_catalog.pg_roles candidate
919
+ where candidate.rolname not in(names.owner_role,names.operator_role,names.auditor_role)
920
+ and(not candidate.rolsuper or candidate.rolname=names.schema_owner)
921
+ and pg_catalog.pg_has_role(candidate.rolname,control_role.granted_role,'MEMBER')
922
+ ) select
923
+ current_setting('server_version_num')::integer/10000=any(array[15,16,17,18])
924
+ and(select count(*)=1 and bool_and(catalog_definition=agent_bridge.owner_control_attestation_definition())
925
+ from agent_bridge.owner_control_attestations where name='owner-control-v5')
926
+ and not exists(
927
+ (select granted_role,member_role,admin_option from actual_memberships except select granted_role,member_role,admin_option from expected_memberships)
928
+ union all(select granted_role,member_role,admin_option from expected_memberships except select granted_role,member_role,admin_option from actual_memberships)
929
+ )
930
+ and not exists(select 1 from actual_memberships where not inherit_option or not set_option)
931
+ and not exists(select 1 from active_registry registry left join pg_catalog.pg_roles role_record on role_record.rolname=registry.member_role
932
+ where role_record.oid is null or not role_record.rolcanlogin or role_record.rolsuper or role_record.rolcreaterole
933
+ or role_record.rolcreatedb or role_record.rolreplication or role_record.rolbypassrls)
934
+ and not exists(
935
+ (select granted_role,member_role from authority_closure except select granted_role,member_role from expected_memberships)
936
+ union all(select granted_role,member_role from expected_memberships except select granted_role,member_role from authority_closure)
937
+ )
938
+ and not exists(select 1 from active_registry registry join pg_catalog.pg_roles inherited on inherited.rolname<>registry.member_role
939
+ where pg_catalog.pg_has_role(registry.member_role,inherited.rolname,'MEMBER') and not exists(
940
+ select 1 from active_registry permitted cross join names where permitted.member_role=registry.member_role
941
+ and inherited.rolname=case permitted.control_role when 'operator' then names.operator_role else names.auditor_role end
942
+ ))
943
+ and not exists(select 1 from active_registry registry cross join pg_catalog.pg_roles candidate,names
944
+ where candidate.rolname not in(registry.member_role,names.schema_owner) and not candidate.rolsuper
945
+ and pg_catalog.pg_has_role(candidate.rolname,registry.member_role,'MEMBER'))
946
+ $$;
947
+
948
+ revoke all on agent_bridge.endpoint_migration_challenges,
949
+ agent_bridge.endpoint_migration_challenge_events,
950
+ agent_bridge.endpoint_migration_challenge_attestations from public;
951
+ revoke all on all sequences in schema agent_bridge from public;
952
+ revoke all on function agent_bridge.guard_endpoint_migration_challenge() from public;
953
+ revoke all on function agent_bridge.reject_endpoint_migration_challenge_truncate() from public;
954
+ revoke all on function agent_bridge.reject_endpoint_migration_challenge_event_mutation() from public;
955
+ revoke all on function agent_bridge.reject_endpoint_migration_challenge_attestation_mutation() from public;
956
+ revoke all on function agent_bridge.cleanup_endpoint_migration_challenges() from public;
957
+ revoke all on function agent_bridge.endpoint_migration_challenge_active_credential(uuid,text,text) from public;
958
+ revoke all on function agent_bridge.endpoint_migration_challenge_direct_lineage(uuid,uuid) from public;
959
+ revoke all on function agent_bridge.endpoint_migration_challenge_catalog_definition() from public;
960
+ revoke all on function agent_bridge.endpoint_migration_challenge_ready() from public;
961
+ revoke all on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) from public;
962
+ revoke all on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) from public;
963
+
964
+ do $ownership_and_grants$
965
+ declare
966
+ runtime_role text := 'agent_bridge_runtime_'||substr(md5(current_database()),1,16);
967
+ control_owner text := 'agent_bridge_control_owner_'||substr(md5(current_database()),1,16);
968
+ role_name text;
969
+ begin
970
+ execute format('alter function agent_bridge.owner_control_plane_ready() owner to %I',control_owner);
971
+ foreach role_name in array array[
972
+ 'guard_endpoint_migration_challenge()',
973
+ 'reject_endpoint_migration_challenge_truncate()',
974
+ 'reject_endpoint_migration_challenge_event_mutation()',
975
+ 'reject_endpoint_migration_challenge_attestation_mutation()',
976
+ 'cleanup_endpoint_migration_challenges()',
977
+ 'endpoint_migration_challenge_active_credential(uuid,text,text)',
978
+ 'endpoint_migration_challenge_direct_lineage(uuid,uuid)',
979
+ 'endpoint_migration_challenge_catalog_definition()',
980
+ 'endpoint_migration_challenge_ready()',
981
+ 'issue_endpoint_migration_challenge(uuid,uuid,text)',
982
+ 'consume_endpoint_migration_challenge(uuid,uuid,text)'
983
+ ] loop
984
+ execute format('alter function agent_bridge.%s owner to %I',role_name,current_user);
985
+ end loop;
986
+ foreach role_name in array array[runtime_role,'anon','authenticated'] loop
987
+ if exists(select 1 from pg_catalog.pg_roles where rolname=role_name) then
988
+ 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);
989
+ execute format('revoke all on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) from %I',role_name);
990
+ execute format('revoke all on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) from %I',role_name);
991
+ execute format('revoke all on function agent_bridge.cleanup_endpoint_migration_challenges() from %I',role_name);
992
+ execute format('revoke all on function agent_bridge.endpoint_migration_challenge_catalog_definition() from %I',role_name);
993
+ execute format('revoke all on function agent_bridge.endpoint_migration_challenge_ready() from %I',role_name);
994
+ end if;
995
+ end loop;
996
+ execute format('revoke all on agent_bridge.request_authorities from %I',runtime_role);
997
+ execute format('grant execute on function agent_bridge.issue_endpoint_migration_challenge(uuid,uuid,text) to %I',runtime_role);
998
+ execute format('grant execute on function agent_bridge.consume_endpoint_migration_challenge(uuid,uuid,text) to %I',runtime_role);
999
+ execute format('grant execute on function agent_bridge.endpoint_migration_challenge_catalog_definition() to %I',control_owner);
1000
+ execute format('grant execute on function agent_bridge.owner_control_plane_ready() to %I',runtime_role);
1001
+ end
1002
+ $ownership_and_grants$;
1003
+
1004
+ insert into agent_bridge.endpoint_migration_challenge_attestations(name,catalog_definition)
1005
+ values('endpoint-migration-v1',agent_bridge.endpoint_migration_challenge_catalog_definition());
1006
+ insert into agent_bridge.owner_control_attestations(name,catalog_definition)
1007
+ values('owner-control-v5',agent_bridge.owner_control_attestation_definition());
1008
+
1009
+ do $final_readiness$
1010
+ begin
1011
+ if not agent_bridge.security_schema_ready() then
1012
+ raise exception 'endpoint migration challenge final security readiness validation failed';
1013
+ elseif not agent_bridge.owner_control_plane_ready() then
1014
+ raise exception 'endpoint migration challenge final owner readiness validation failed';
1015
+ elseif not agent_bridge.gateway_authority_ready() then
1016
+ raise exception 'endpoint migration challenge final gateway authority readiness validation failed';
1017
+ elseif not agent_bridge.endpoint_migration_challenge_ready() then
1018
+ raise exception 'endpoint migration challenge final endpoint readiness validation failed';
1019
+ elseif not agent_bridge.portable_archive_ready() then
1020
+ raise exception 'endpoint migration challenge final portable archive readiness validation failed';
1021
+ end if;
1022
+ end
1023
+ $final_readiness$;
1024
+
1025
+ insert into agent_bridge.schema_migrations(version,name,checksum)
1026
+ values(18,'endpoint_migration_challenges','__AGENT_BRIDGE_MIGRATION_CHECKSUM__');
1027
+
1028
+ commit;