@creatornader/agent-bridge 0.2.0 → 0.3.1

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 (115) hide show
  1. package/CHANGELOG.md +86 -4
  2. package/README.md +567 -55
  3. package/ROADMAP.md +114 -0
  4. package/SECURITY.md +90 -0
  5. package/SKILL.md +68 -2
  6. package/bin/agent-bridge +2 -2
  7. package/clients/claude-desktop.json +3 -1
  8. package/dist/archive.d.ts +163 -0
  9. package/dist/archive.js +50 -0
  10. package/dist/archive.js.map +1 -0
  11. package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
  12. package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
  13. package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
  14. package/dist/bridge.d.ts +76 -17
  15. package/dist/bridge.js +52 -24
  16. package/dist/chunk-2DFNOLMO.js +618 -0
  17. package/dist/chunk-2DFNOLMO.js.map +1 -0
  18. package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
  19. package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
  20. package/dist/chunk-45BZGB77.js +709 -0
  21. package/dist/chunk-45BZGB77.js.map +1 -0
  22. package/dist/chunk-4VB44KJU.js +230 -0
  23. package/dist/chunk-4VB44KJU.js.map +1 -0
  24. package/dist/chunk-5GMB3A77.js +1013 -0
  25. package/dist/chunk-5GMB3A77.js.map +1 -0
  26. package/dist/chunk-A6W4QALF.js +1258 -0
  27. package/dist/chunk-A6W4QALF.js.map +1 -0
  28. package/dist/chunk-BZLMF5O5.js +328 -0
  29. package/dist/chunk-BZLMF5O5.js.map +1 -0
  30. package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
  31. package/dist/chunk-CEXBMOI5.js.map +1 -0
  32. package/dist/chunk-GADH244D.js +527 -0
  33. package/dist/chunk-GADH244D.js.map +1 -0
  34. package/dist/chunk-MBPZXTJA.js +2428 -0
  35. package/dist/chunk-MBPZXTJA.js.map +1 -0
  36. package/dist/chunk-NCO7RLBM.js +146 -0
  37. package/dist/chunk-NCO7RLBM.js.map +1 -0
  38. package/dist/chunk-Q6PPMXJC.js +1050 -0
  39. package/dist/chunk-Q6PPMXJC.js.map +1 -0
  40. package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
  41. package/dist/chunk-SPSMZOCJ.js.map +1 -0
  42. package/dist/chunk-UAHMP6GC.js +54 -0
  43. package/dist/chunk-UAHMP6GC.js.map +1 -0
  44. package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
  45. package/dist/chunk-VOQIDTCJ.js.map +1 -0
  46. package/dist/chunk-WAI2IYIQ.js +554 -0
  47. package/dist/chunk-WAI2IYIQ.js.map +1 -0
  48. package/dist/cli.d.ts +2 -1
  49. package/dist/cli.js +3555 -214
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client-config.d.ts +1 -1
  52. package/dist/client-config.js +1 -1
  53. package/dist/client-runtime.d.ts +10 -6
  54. package/dist/client-runtime.js +9 -6
  55. package/dist/contracts/generate.d.ts +2 -0
  56. package/dist/contracts/generate.js +247 -0
  57. package/dist/contracts/generate.js.map +1 -0
  58. package/dist/dr.d.ts +324 -0
  59. package/dist/dr.js +124 -0
  60. package/dist/dr.js.map +1 -0
  61. package/dist/gateway-CggQ8vTC.d.ts +78 -0
  62. package/dist/gateway-main.js +190 -8
  63. package/dist/gateway-main.js.map +1 -1
  64. package/dist/gateway.d.ts +4 -3
  65. package/dist/gateway.js +4 -3
  66. package/dist/index.js +11 -8
  67. package/dist/index.js.map +1 -1
  68. package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
  69. package/dist/server.d.ts +3 -2
  70. package/dist/server.js +13 -8
  71. package/dist/sqlite.d.ts +94 -12
  72. package/dist/sqlite.js +7 -4
  73. package/docs/architecture-v2.md +394 -0
  74. package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
  75. package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
  76. package/docs/decisions/0003-host-adapters-and-consumer-instance-keys.md +88 -0
  77. package/docs/ecosystem.md +86 -0
  78. package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
  79. package/docs/troubleshooting.md +89 -0
  80. package/llms.txt +107 -6
  81. package/openapi/agent-bridge-v2.openapi.json +9610 -0
  82. package/package.json +24 -4
  83. package/schemas/agent-bridge-v2.capabilities.json +1829 -0
  84. package/schemas/agent-bridge-v2.mcp.json +2074 -0
  85. package/schemas/agent-bridge-v2.schema.json +4560 -0
  86. package/sql/migrations/008_message_projects.sql +36 -0
  87. package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
  88. package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
  89. package/sql/migrations/011_credential_security.sql +875 -0
  90. package/sql/migrations/012_request_authority.sql +177 -0
  91. package/sql/migrations/013_row_isolation.sql +495 -0
  92. package/sql/migrations/014_owner_control_plane.sql +1641 -0
  93. package/sql/migrations/015_portable_archives.sql +1813 -0
  94. package/sql/migrations/016_native_dr_fence.sql +1005 -0
  95. package/bin/CLAUDE.md +0 -11
  96. package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
  97. package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
  98. package/dist/chunk-4KAOBC5X.js.map +0 -1
  99. package/dist/chunk-6MAZ6YLZ.js +0 -146
  100. package/dist/chunk-6MAZ6YLZ.js.map +0 -1
  101. package/dist/chunk-AXL2I7FH.js.map +0 -1
  102. package/dist/chunk-BV6W4AJK.js +0 -211
  103. package/dist/chunk-BV6W4AJK.js.map +0 -1
  104. package/dist/chunk-CKSKUBDQ.js +0 -153
  105. package/dist/chunk-CKSKUBDQ.js.map +0 -1
  106. package/dist/chunk-JOUBDRYX.js +0 -272
  107. package/dist/chunk-JOUBDRYX.js.map +0 -1
  108. package/dist/chunk-JTC5YURG.js +0 -49
  109. package/dist/chunk-JTC5YURG.js.map +0 -1
  110. package/dist/chunk-O26W5QHS.js +0 -409
  111. package/dist/chunk-O26W5QHS.js.map +0 -1
  112. package/dist/chunk-WEULPMUY.js +0 -988
  113. package/dist/chunk-WEULPMUY.js.map +0 -1
  114. package/dist/chunk-YD6YJATN.js.map +0 -1
  115. package/dist/gateway-BULozv3K.d.ts +0 -54
@@ -0,0 +1,875 @@
1
+ begin;
2
+
3
+ select pg_advisory_xact_lock(1646705660);
4
+
5
+ create or replace function agent_bridge.canonicalize_scopes(requested_scopes text[])
6
+ returns text[]
7
+ language sql immutable strict parallel safe set search_path = '' as $$
8
+ select coalesce(array_agg(scope order by scope), '{}'::text[])
9
+ from (select distinct unnest(requested_scopes) as scope) canonical
10
+ $$;
11
+
12
+ create table if not exists agent_bridge.credential_scope_sets (
13
+ name text primary key,
14
+ scopes text[] not null,
15
+ created_at timestamptz not null default now(),
16
+ constraint credential_scope_sets_name check (name ~ '^[a-z][a-z0-9-]{0,63}$'),
17
+ constraint credential_scope_sets_scopes check (
18
+ scopes = agent_bridge.canonicalize_scopes(scopes)
19
+ and scopes <@ array[
20
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
21
+ 'gateway:metrics','messages:read','messages:write','presence:read',
22
+ 'presence:write','receipts:write','status:read'
23
+ ]::text[]
24
+ )
25
+ );
26
+
27
+ insert into agent_bridge.credential_scope_sets (name, scopes) values
28
+ ('release-a-full', array[
29
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
30
+ 'gateway:metrics','messages:read','messages:write','presence:read',
31
+ 'presence:write','receipts:write','status:read'
32
+ ]::text[])
33
+ on conflict (name) do nothing;
34
+
35
+ do $scope_seed$
36
+ begin
37
+ if not exists (
38
+ select 1 from agent_bridge.credential_scope_sets
39
+ where name='release-a-full' and scopes=array[
40
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
41
+ 'gateway:metrics','messages:read','messages:write','presence:read',
42
+ 'presence:write','receipts:write','status:read'
43
+ ]::text[]
44
+ ) then
45
+ raise exception 'credential scope seed conflicts with required state';
46
+ end if;
47
+ end
48
+ $scope_seed$;
49
+
50
+ alter table agent_bridge.credentials
51
+ add column if not exists scopes text[],
52
+ add column if not exists scope_set_name text references agent_bridge.credential_scope_sets(name),
53
+ add column if not exists replaces_credential_id uuid references agent_bridge.credentials(id),
54
+ add column if not exists revoked_by text,
55
+ add column if not exists revocation_reason text,
56
+ add column if not exists expiry_grace_until timestamptz;
57
+
58
+ update agent_bridge.credentials
59
+ set scopes = array[
60
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
61
+ 'gateway:metrics','messages:read','messages:write','presence:read',
62
+ 'presence:write','receipts:write','status:read'
63
+ ]::text[],
64
+ scope_set_name = 'release-a-full'
65
+ where scopes is null;
66
+
67
+ alter table agent_bridge.credentials
68
+ alter column scopes set default array[
69
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
70
+ 'gateway:metrics','messages:read','messages:write','presence:read',
71
+ 'presence:write','receipts:write','status:read'
72
+ ]::text[],
73
+ alter column scopes set not null;
74
+
75
+ do $credential_constraints$
76
+ begin
77
+ if not exists (
78
+ select 1 from pg_constraint
79
+ where conrelid='agent_bridge.credentials'::regclass
80
+ and conname='credentials_scopes_canonical'
81
+ ) then
82
+ alter table agent_bridge.credentials add constraint credentials_scopes_canonical check (
83
+ scopes = agent_bridge.canonicalize_scopes(scopes)
84
+ and scopes <@ array[
85
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
86
+ 'gateway:metrics','messages:read','messages:write','presence:read',
87
+ 'presence:write','receipts:write','status:read'
88
+ ]::text[]
89
+ );
90
+ end if;
91
+ if not exists (
92
+ select 1 from pg_constraint
93
+ where conrelid='agent_bridge.credentials'::regclass
94
+ and conname='credentials_replacement_not_self'
95
+ ) then
96
+ alter table agent_bridge.credentials add constraint credentials_replacement_not_self
97
+ check (replaces_credential_id is distinct from id);
98
+ end if;
99
+ if not exists (
100
+ select 1 from pg_constraint
101
+ where conrelid='agent_bridge.credentials'::regclass
102
+ and conname='credentials_grace_shortens_expiry'
103
+ ) then
104
+ alter table agent_bridge.credentials add constraint credentials_grace_shortens_expiry check (
105
+ expiry_grace_until is null or expires_at is null or expiry_grace_until <= expires_at
106
+ );
107
+ end if;
108
+ end
109
+ $credential_constraints$;
110
+
111
+ create unique index if not exists credentials_replacement_lineage
112
+ on agent_bridge.credentials(replaces_credential_id)
113
+ where replaces_credential_id is not null;
114
+
115
+ create or replace function agent_bridge.validate_credential_security()
116
+ returns trigger
117
+ language plpgsql set search_path = '' as $$
118
+ declare
119
+ predecessor agent_bridge.credentials%rowtype;
120
+ profile_scopes text[];
121
+ begin
122
+ if tg_op='UPDATE' then
123
+ if old.id is distinct from new.id
124
+ or old.workspace_id is distinct from new.workspace_id
125
+ or old.agent_id is distinct from new.agent_id
126
+ or old.token_hash is distinct from new.token_hash then
127
+ raise exception 'credential identity is immutable';
128
+ end if;
129
+ if old.replaces_credential_id is distinct from new.replaces_credential_id then
130
+ raise exception 'credential replacement lineage is immutable';
131
+ end if;
132
+ if old.revoked_at is not null and (
133
+ old.revoked_at is distinct from new.revoked_at
134
+ or old.revoked_by is distinct from new.revoked_by
135
+ or old.revocation_reason is distinct from new.revocation_reason
136
+ ) then
137
+ raise exception 'credential revocation is immutable';
138
+ end if;
139
+ if old.revoked_at is null and new.revoked_at is not null
140
+ and coalesce(current_setting('agent_bridge.lifecycle_authorized',true),'') <> 'revocation' then
141
+ raise exception 'credential revocation requires the lifecycle function';
142
+ end if;
143
+ if (
144
+ old.revoked_by is distinct from new.revoked_by
145
+ or old.revocation_reason is distinct from new.revocation_reason
146
+ ) and coalesce(current_setting('agent_bridge.lifecycle_authorized',true),'') <> 'revocation' then
147
+ raise exception 'credential revocation metadata requires the lifecycle function';
148
+ end if;
149
+ if old.expiry_grace_until is distinct from new.expiry_grace_until
150
+ and coalesce(current_setting('agent_bridge.lifecycle_authorized',true),'') <> 'replacement' then
151
+ raise exception 'credential grace requires the lifecycle function';
152
+ end if;
153
+ if old.expiry_grace_until is not null and (
154
+ new.expiry_grace_until is null
155
+ or new.expiry_grace_until > old.expiry_grace_until
156
+ ) then
157
+ raise exception 'credential grace may only move earlier';
158
+ end if;
159
+ end if;
160
+
161
+ if new.scope_set_name is not null then
162
+ select scopes into profile_scopes
163
+ from agent_bridge.credential_scope_sets
164
+ where name=new.scope_set_name;
165
+ if not found or profile_scopes is distinct from new.scopes then
166
+ raise exception 'credential scopes do not match the named scope set';
167
+ end if;
168
+ end if;
169
+
170
+ if new.replaces_credential_id is not null then
171
+ select * into predecessor
172
+ from agent_bridge.credentials
173
+ where id=new.replaces_credential_id;
174
+ if not found
175
+ or predecessor.workspace_id is distinct from new.workspace_id
176
+ or predecessor.agent_id is distinct from new.agent_id then
177
+ raise exception 'replacement credentials must use the same workspace and agent';
178
+ end if;
179
+ if exists (
180
+ with recursive ancestry(id, replaces_credential_id) as (
181
+ select id, replaces_credential_id from agent_bridge.credentials
182
+ where id=new.replaces_credential_id
183
+ union all
184
+ select credential.id, credential.replaces_credential_id
185
+ from agent_bridge.credentials credential
186
+ join ancestry on credential.id=ancestry.replaces_credential_id
187
+ )
188
+ select 1 from ancestry where id=new.id
189
+ ) then
190
+ raise exception 'credential replacement cycle is not allowed';
191
+ end if;
192
+ if tg_op='INSERT'
193
+ and coalesce(current_setting('agent_bridge.lifecycle_authorized',true),'') <> 'replacement' then
194
+ raise exception 'credential replacement requires the lifecycle function';
195
+ end if;
196
+ end if;
197
+
198
+ if tg_op='INSERT' and (
199
+ new.revoked_at is not null or new.revoked_by is not null
200
+ or new.revocation_reason is not null or new.expiry_grace_until is not null
201
+ ) then
202
+ raise exception 'credential lifecycle state requires a lifecycle function';
203
+ end if;
204
+
205
+ if new.expiry_grace_until is not null and not exists (
206
+ select 1 from agent_bridge.credentials successor
207
+ where successor.replaces_credential_id=new.id
208
+ ) then
209
+ raise exception 'credential grace requires a successor';
210
+ end if;
211
+ return new;
212
+ end
213
+ $$;
214
+
215
+ drop trigger if exists credentials_validate_security on agent_bridge.credentials;
216
+ create trigger credentials_validate_security
217
+ before insert or update on agent_bridge.credentials
218
+ for each row execute function agent_bridge.validate_credential_security();
219
+
220
+ create or replace function agent_bridge.reject_credential_delete()
221
+ returns trigger
222
+ language plpgsql set search_path = '' as $$
223
+ begin
224
+ raise exception 'credentials are append-only';
225
+ end
226
+ $$;
227
+
228
+ drop trigger if exists credentials_append_only on agent_bridge.credentials;
229
+ create trigger credentials_append_only
230
+ before delete on agent_bridge.credentials
231
+ for each row execute function agent_bridge.reject_credential_delete();
232
+
233
+ create or replace function agent_bridge.reject_scope_set_mutation()
234
+ returns trigger
235
+ language plpgsql set search_path = '' as $$
236
+ begin
237
+ raise exception 'credential scope sets are immutable';
238
+ end
239
+ $$;
240
+
241
+ drop trigger if exists credential_scope_sets_immutable on agent_bridge.credential_scope_sets;
242
+ create trigger credential_scope_sets_immutable
243
+ before update or delete on agent_bridge.credential_scope_sets
244
+ for each row execute function agent_bridge.reject_scope_set_mutation();
245
+
246
+ create table if not exists agent_bridge.security_events (
247
+ sequence bigint generated always as identity primary key,
248
+ event_id uuid not null default gen_random_uuid() unique,
249
+ event_type text not null,
250
+ outcome text not null,
251
+ reason_code text not null,
252
+ workspace_id text not null references agent_bridge.workspaces(id),
253
+ principal text not null,
254
+ actor_principal text not null,
255
+ credential_id uuid references agent_bridge.credentials(id),
256
+ related_credential_id uuid references agent_bridge.credentials(id),
257
+ operation_id text,
258
+ request_id uuid,
259
+ policy_id text,
260
+ retry_after_seconds integer,
261
+ created_at timestamptz not null default clock_timestamp(),
262
+ constraint security_events_type check (event_type in (
263
+ 'scope_denied','rate_denied','credential_replaced',
264
+ 'credential_replacement_failed','credential_revoked'
265
+ )),
266
+ constraint security_events_outcome check (outcome in ('denied','succeeded','failed')),
267
+ constraint security_events_reason check (reason_code in (
268
+ 'missing_scope','rate_limit_exceeded','credential_rotated',
269
+ 'invalid_replacement','credential_revoked'
270
+ )),
271
+ constraint security_events_retry check (retry_after_seconds is null or retry_after_seconds >= 1),
272
+ constraint security_events_actor check (
273
+ length(actor_principal) between 1 and 128 and actor_principal !~ '[[:cntrl:]]'
274
+ )
275
+ );
276
+
277
+ create index if not exists security_events_workspace_sequence
278
+ on agent_bridge.security_events(workspace_id, sequence desc);
279
+ create index if not exists security_events_credential_sequence
280
+ on agent_bridge.security_events(credential_id, sequence desc);
281
+ create index if not exists security_events_created
282
+ on agent_bridge.security_events(created_at, sequence);
283
+
284
+ create or replace function agent_bridge.reject_security_event_mutation()
285
+ returns trigger
286
+ language plpgsql set search_path = '' as $$
287
+ begin
288
+ raise exception 'security events are append-only';
289
+ end
290
+ $$;
291
+
292
+ drop trigger if exists security_events_append_only on agent_bridge.security_events;
293
+ create trigger security_events_append_only
294
+ before update or delete or truncate on agent_bridge.security_events
295
+ for each statement execute function agent_bridge.reject_security_event_mutation();
296
+
297
+ create table if not exists agent_bridge.rate_limit_policies (
298
+ policy_id text primary key,
299
+ operation_id text unique,
300
+ capacity integer not null,
301
+ refill_per_second numeric(20,6) not null,
302
+ enabled boolean not null default true,
303
+ constraint rate_limit_policy_shape check (
304
+ (policy_id='global' and operation_id is null)
305
+ or (policy_id='operation:' || operation_id and operation_id is not null)
306
+ ),
307
+ constraint rate_limit_policy_operation check (
308
+ operation_id is null or operation_id in (
309
+ 'capabilities','status','gateway_metrics','publish_message','history',
310
+ 'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
311
+ 'cancel_delivery','requeue_delivery','extend_delivery',
312
+ 'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence'
313
+ )
314
+ ),
315
+ constraint rate_limit_policy_capacity check (capacity > 0),
316
+ constraint rate_limit_policy_refill check (refill_per_second > 0)
317
+ );
318
+
319
+ do $policy_constraints$
320
+ begin
321
+ if not exists (
322
+ select 1 from pg_constraint
323
+ where conrelid='agent_bridge.rate_limit_policies'::regclass
324
+ and conname='rate_limit_policy_operation'
325
+ ) then
326
+ alter table agent_bridge.rate_limit_policies add constraint rate_limit_policy_operation check (
327
+ operation_id is null or operation_id in (
328
+ 'capabilities','status','gateway_metrics','publish_message','history',
329
+ 'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
330
+ 'cancel_delivery','requeue_delivery','extend_delivery',
331
+ 'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence'
332
+ )
333
+ );
334
+ end if;
335
+ end
336
+ $policy_constraints$;
337
+
338
+ insert into agent_bridge.rate_limit_policies
339
+ (policy_id, operation_id, capacity, refill_per_second) values
340
+ ('global',null,300,50),
341
+ ('operation:capabilities','capabilities',30,1),
342
+ ('operation:status','status',30,1),
343
+ ('operation:gateway_metrics','gateway_metrics',30,1),
344
+ ('operation:publish_message','publish_message',120,20),
345
+ ('operation:history','history',120,20),
346
+ ('operation:record_receipt','record_receipt',120,20),
347
+ ('operation:claim_delivery','claim_delivery',120,20),
348
+ ('operation:list_deliveries','list_deliveries',120,20),
349
+ ('operation:list_delivery_events','list_delivery_events',120,20),
350
+ ('operation:cancel_delivery','cancel_delivery',60,10),
351
+ ('operation:requeue_delivery','requeue_delivery',60,10),
352
+ ('operation:extend_delivery','extend_delivery',120,20),
353
+ ('operation:acknowledge_delivery','acknowledge_delivery',120,20),
354
+ ('operation:negative_acknowledge_delivery','negative_acknowledge_delivery',120,20),
355
+ ('operation:heartbeat','heartbeat',120,20),
356
+ ('operation:presence','presence',120,20)
357
+ on conflict (policy_id) do nothing;
358
+
359
+ do $policy_seed$
360
+ declare
361
+ begin
362
+ if (select count(*) from agent_bridge.rate_limit_policies) <> 17 or exists (
363
+ with expected(policy_id,operation_id,capacity,refill_per_second) as (values
364
+ ('global',null::text,300,50::numeric),
365
+ ('operation:capabilities','capabilities',30,1),
366
+ ('operation:status','status',30,1),
367
+ ('operation:gateway_metrics','gateway_metrics',30,1),
368
+ ('operation:publish_message','publish_message',120,20),
369
+ ('operation:history','history',120,20),
370
+ ('operation:record_receipt','record_receipt',120,20),
371
+ ('operation:claim_delivery','claim_delivery',120,20),
372
+ ('operation:list_deliveries','list_deliveries',120,20),
373
+ ('operation:list_delivery_events','list_delivery_events',120,20),
374
+ ('operation:cancel_delivery','cancel_delivery',60,10),
375
+ ('operation:requeue_delivery','requeue_delivery',60,10),
376
+ ('operation:extend_delivery','extend_delivery',120,20),
377
+ ('operation:acknowledge_delivery','acknowledge_delivery',120,20),
378
+ ('operation:negative_acknowledge_delivery','negative_acknowledge_delivery',120,20),
379
+ ('operation:heartbeat','heartbeat',120,20),
380
+ ('operation:presence','presence',120,20)
381
+ )
382
+ select 1 from expected
383
+ left join agent_bridge.rate_limit_policies policy using (policy_id)
384
+ where policy.policy_id is null
385
+ or policy.operation_id is distinct from expected.operation_id
386
+ or policy.capacity is distinct from expected.capacity
387
+ or policy.refill_per_second is distinct from expected.refill_per_second
388
+ or not policy.enabled
389
+ ) then
390
+ raise exception 'rate limit policy seed conflicts with required state';
391
+ end if;
392
+ end
393
+ $policy_seed$;
394
+
395
+ create table if not exists agent_bridge.rate_limit_buckets (
396
+ credential_id uuid not null references agent_bridge.credentials(id) on delete restrict,
397
+ policy_id text not null references agent_bridge.rate_limit_policies(policy_id) on delete restrict,
398
+ tokens numeric(30,6) not null,
399
+ updated_at timestamptz not null,
400
+ primary key (credential_id, policy_id),
401
+ constraint rate_limit_buckets_tokens check (tokens >= 0)
402
+ );
403
+
404
+ create index if not exists rate_limit_buckets_cleanup
405
+ on agent_bridge.rate_limit_buckets(updated_at);
406
+
407
+ create or replace function agent_bridge.record_scope_denial(
408
+ requested_credential_id uuid,
409
+ requested_operation_id text,
410
+ requested_request_id uuid
411
+ ) returns void
412
+ language plpgsql security definer set search_path = '' as $$
413
+ declare
414
+ credential_workspace text;
415
+ credential_principal text;
416
+ begin
417
+ if requested_operation_id is null or requested_operation_id not in (
418
+ 'capabilities','status','gateway_metrics','publish_message','history',
419
+ 'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
420
+ 'cancel_delivery','requeue_delivery','extend_delivery',
421
+ 'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence'
422
+ ) then
423
+ raise exception using errcode='55000', message='operation security policy is unavailable';
424
+ end if;
425
+ if not exists (
426
+ select 1 from agent_bridge.rate_limit_policies
427
+ where operation_id=requested_operation_id and enabled
428
+ ) then
429
+ raise exception using errcode='55000', message='operation security policy is unavailable';
430
+ end if;
431
+ select credential.workspace_id, agent.principal
432
+ into credential_workspace, credential_principal
433
+ from agent_bridge.credentials credential
434
+ join agent_bridge.agents agent
435
+ on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
436
+ where credential.id=requested_credential_id;
437
+ if not found then
438
+ raise exception 'credential is unavailable for security audit';
439
+ end if;
440
+ insert into agent_bridge.security_events (
441
+ event_type,outcome,reason_code,workspace_id,principal,actor_principal,
442
+ credential_id,operation_id,request_id
443
+ ) values (
444
+ 'scope_denied','denied','missing_scope',credential_workspace,
445
+ credential_principal,credential_principal,requested_credential_id,
446
+ requested_operation_id,requested_request_id
447
+ );
448
+ end
449
+ $$;
450
+
451
+ create or replace function agent_bridge.consume_rate_limit(
452
+ requested_credential_id uuid,
453
+ requested_operation_id text,
454
+ requested_request_id uuid
455
+ ) returns table(
456
+ allowed boolean,
457
+ limit_value integer,
458
+ remaining_value integer,
459
+ retry_after_seconds numeric,
460
+ denied_policy_id text
461
+ )
462
+ language plpgsql security definer set search_path = '' as $$
463
+ declare
464
+ global_policy agent_bridge.rate_limit_policies%rowtype;
465
+ operation_policy agent_bridge.rate_limit_policies%rowtype;
466
+ request_time timestamptz := clock_timestamp();
467
+ global_available numeric(30,6);
468
+ operation_available numeric(30,6);
469
+ global_after numeric(30,6);
470
+ operation_after numeric(30,6);
471
+ global_retry numeric(30,6) := 0;
472
+ operation_retry numeric(30,6) := 0;
473
+ credential_workspace text;
474
+ credential_principal text;
475
+ begin
476
+ if requested_operation_id is null or requested_operation_id not in (
477
+ 'capabilities','status','gateway_metrics','publish_message','history',
478
+ 'record_receipt','claim_delivery','list_deliveries','list_delivery_events',
479
+ 'cancel_delivery','requeue_delivery','extend_delivery',
480
+ 'acknowledge_delivery','negative_acknowledge_delivery','heartbeat','presence'
481
+ ) then
482
+ raise exception using errcode='55000', message='operation rate limit policy is unavailable';
483
+ end if;
484
+ select * into global_policy
485
+ from agent_bridge.rate_limit_policies
486
+ where policy_id='global' and enabled
487
+ for share;
488
+ if not found then
489
+ raise exception using errcode='55000', message='global rate limit policy is unavailable';
490
+ end if;
491
+ select * into operation_policy
492
+ from agent_bridge.rate_limit_policies
493
+ where operation_id=requested_operation_id and enabled
494
+ for share;
495
+ if not found then
496
+ raise exception using errcode='55000', message='operation rate limit policy is unavailable';
497
+ end if;
498
+
499
+ insert into agent_bridge.rate_limit_buckets(credential_id,policy_id,tokens,updated_at)
500
+ values
501
+ (requested_credential_id,global_policy.policy_id,global_policy.capacity,request_time),
502
+ (requested_credential_id,operation_policy.policy_id,operation_policy.capacity,request_time)
503
+ on conflict do nothing;
504
+
505
+ perform 1 from agent_bridge.rate_limit_buckets
506
+ where credential_id=requested_credential_id
507
+ and policy_id in (global_policy.policy_id,operation_policy.policy_id)
508
+ order by policy_id
509
+ for update;
510
+
511
+ select least(
512
+ global_policy.capacity::numeric,
513
+ greatest(0::numeric, bucket.tokens)
514
+ + greatest(0::numeric, extract(epoch from request_time-bucket.updated_at)::numeric)
515
+ * global_policy.refill_per_second
516
+ ) into global_available
517
+ from agent_bridge.rate_limit_buckets bucket
518
+ where bucket.credential_id=requested_credential_id
519
+ and bucket.policy_id=global_policy.policy_id;
520
+
521
+ select least(
522
+ operation_policy.capacity::numeric,
523
+ greatest(0::numeric, bucket.tokens)
524
+ + greatest(0::numeric, extract(epoch from request_time-bucket.updated_at)::numeric)
525
+ * operation_policy.refill_per_second
526
+ ) into operation_available
527
+ from agent_bridge.rate_limit_buckets bucket
528
+ where bucket.credential_id=requested_credential_id
529
+ and bucket.policy_id=operation_policy.policy_id;
530
+
531
+ allowed := global_available >= 1 and operation_available >= 1;
532
+ global_after := case when allowed then global_available-1 else global_available end;
533
+ operation_after := case when allowed then operation_available-1 else operation_available end;
534
+
535
+ update agent_bridge.rate_limit_buckets bucket
536
+ set tokens=case
537
+ when bucket.policy_id=global_policy.policy_id then global_after
538
+ else operation_after
539
+ end,
540
+ updated_at=request_time
541
+ where bucket.credential_id=requested_credential_id
542
+ and bucket.policy_id in (global_policy.policy_id,operation_policy.policy_id);
543
+
544
+ limit_value := least(global_policy.capacity,operation_policy.capacity);
545
+ remaining_value := floor(least(global_after,operation_after))::integer;
546
+ retry_after_seconds := 0;
547
+ denied_policy_id := null;
548
+
549
+ if not allowed then
550
+ if global_available < 1 then
551
+ global_retry := (1-global_available)/global_policy.refill_per_second;
552
+ end if;
553
+ if operation_available < 1 then
554
+ operation_retry := (1-operation_available)/operation_policy.refill_per_second;
555
+ end if;
556
+ retry_after_seconds := greatest(global_retry,operation_retry);
557
+ denied_policy_id := case
558
+ when global_retry >= operation_retry then global_policy.policy_id
559
+ else operation_policy.policy_id
560
+ end;
561
+
562
+ select credential.workspace_id, agent.principal
563
+ into credential_workspace, credential_principal
564
+ from agent_bridge.credentials credential
565
+ join agent_bridge.agents agent
566
+ on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
567
+ where credential.id=requested_credential_id;
568
+ if not found then
569
+ raise exception 'credential is unavailable for rate audit';
570
+ end if;
571
+ insert into agent_bridge.security_events (
572
+ event_type,outcome,reason_code,workspace_id,principal,actor_principal,
573
+ credential_id,operation_id,request_id,policy_id,retry_after_seconds
574
+ ) values (
575
+ 'rate_denied','denied','rate_limit_exceeded',credential_workspace,
576
+ credential_principal,credential_principal,requested_credential_id,
577
+ requested_operation_id,requested_request_id,denied_policy_id,
578
+ greatest(1,ceil(retry_after_seconds)::integer)
579
+ );
580
+ end if;
581
+ return next;
582
+ end
583
+ $$;
584
+
585
+ create or replace function agent_bridge.replace_credential(
586
+ requested_predecessor_id uuid,
587
+ requested_token_hash char(64),
588
+ requested_scopes text[],
589
+ requested_scope_set_name text,
590
+ requested_label text,
591
+ requested_expires_at timestamptz,
592
+ requested_grace_until timestamptz,
593
+ requested_actor text,
594
+ requested_request_id uuid
595
+ ) returns table(succeeded boolean, credential_id uuid, failure_code text)
596
+ language plpgsql security definer set search_path = '' as $$
597
+ declare
598
+ predecessor agent_bridge.credentials%rowtype;
599
+ predecessor_principal text;
600
+ successor_id uuid;
601
+ begin
602
+ if requested_actor is null or length(requested_actor) not between 1 and 128
603
+ or requested_actor ~ '[[:cntrl:]]' then
604
+ raise exception 'invalid replacement actor';
605
+ end if;
606
+ select credential.* into predecessor
607
+ from agent_bridge.credentials credential
608
+ join agent_bridge.agents agent
609
+ on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
610
+ where credential.id=requested_predecessor_id
611
+ for update of credential;
612
+ if not found then
613
+ succeeded := false; credential_id := null; failure_code := 'predecessor_not_found';
614
+ return next; return;
615
+ end if;
616
+ select agent.principal into predecessor_principal
617
+ from agent_bridge.agents agent
618
+ where agent.id=predecessor.agent_id
619
+ and agent.workspace_id=predecessor.workspace_id;
620
+ if predecessor.expires_at is not null
621
+ and requested_grace_until>predecessor.expires_at then
622
+ succeeded := false; credential_id := null; failure_code := 'invalid_grace';
623
+ else
624
+ begin
625
+ perform set_config('agent_bridge.lifecycle_authorized','replacement',true);
626
+ insert into agent_bridge.credentials (
627
+ workspace_id,agent_id,token_hash,label,expires_at,scopes,scope_set_name,
628
+ replaces_credential_id
629
+ ) values (
630
+ predecessor.workspace_id,predecessor.agent_id,requested_token_hash,
631
+ requested_label,requested_expires_at,requested_scopes,
632
+ requested_scope_set_name,requested_predecessor_id
633
+ ) returning id into successor_id;
634
+ update agent_bridge.credentials
635
+ set expiry_grace_until=requested_grace_until
636
+ where id=requested_predecessor_id;
637
+ insert into agent_bridge.security_events (
638
+ event_type,outcome,reason_code,workspace_id,principal,actor_principal,
639
+ credential_id,related_credential_id,request_id
640
+ ) values (
641
+ 'credential_replaced','succeeded','credential_rotated',
642
+ predecessor.workspace_id,predecessor_principal,requested_actor,
643
+ successor_id,requested_predecessor_id,requested_request_id
644
+ );
645
+ perform set_config('agent_bridge.lifecycle_authorized','',true);
646
+ succeeded := true; credential_id := successor_id; failure_code := null;
647
+ return next; return;
648
+ exception when others then
649
+ succeeded := false; credential_id := null; failure_code := 'invalid_replacement';
650
+ end;
651
+ end if;
652
+
653
+ insert into agent_bridge.security_events (
654
+ event_type,outcome,reason_code,workspace_id,principal,actor_principal,
655
+ credential_id,request_id
656
+ ) values (
657
+ 'credential_replacement_failed','failed','invalid_replacement',
658
+ predecessor.workspace_id,predecessor_principal,requested_actor,
659
+ requested_predecessor_id,requested_request_id
660
+ );
661
+ return next;
662
+ end
663
+ $$;
664
+
665
+ create or replace function agent_bridge.revoke_credential(
666
+ requested_credential_id uuid,
667
+ requested_actor text,
668
+ requested_reason_code text,
669
+ requested_request_id uuid
670
+ ) returns boolean
671
+ language plpgsql security definer set search_path = '' as $$
672
+ declare
673
+ credential_workspace text;
674
+ credential_principal text;
675
+ begin
676
+ if requested_actor is null or length(requested_actor) not between 1 and 128
677
+ or requested_actor ~ '[[:cntrl:]]'
678
+ or requested_reason_code not in ('operator_request','rotation','compromise','retired') then
679
+ raise exception 'invalid credential revocation request';
680
+ end if;
681
+ perform set_config('agent_bridge.lifecycle_authorized','revocation',true);
682
+ update agent_bridge.credentials credential
683
+ set revoked_at=clock_timestamp(), revoked_by=requested_actor,
684
+ revocation_reason=requested_reason_code
685
+ from agent_bridge.agents agent
686
+ where credential.id=requested_credential_id
687
+ and credential.revoked_at is null
688
+ and agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
689
+ returning credential.workspace_id,agent.principal
690
+ into credential_workspace,credential_principal;
691
+ if not found then
692
+ perform set_config('agent_bridge.lifecycle_authorized','',true);
693
+ return false;
694
+ end if;
695
+ perform set_config('agent_bridge.lifecycle_authorized','',true);
696
+ insert into agent_bridge.security_events (
697
+ event_type,outcome,reason_code,workspace_id,principal,actor_principal,
698
+ credential_id,request_id
699
+ ) values (
700
+ 'credential_revoked','succeeded','credential_revoked',credential_workspace,
701
+ credential_principal,requested_actor,requested_credential_id,requested_request_id
702
+ );
703
+ return true;
704
+ end
705
+ $$;
706
+
707
+ create or replace function agent_bridge.security_schema_ready()
708
+ returns boolean
709
+ language plpgsql stable security definer set search_path = '' as $$
710
+ declare
711
+ runtime_role text := 'agent_bridge_runtime_' || substr(md5(current_database()), 1, 16);
712
+ begin
713
+ return
714
+ (select scopes=array[
715
+ 'deliveries:claim','deliveries:manage','deliveries:read','deliveries:settle',
716
+ 'gateway:metrics','messages:read','messages:write','presence:read',
717
+ 'presence:write','receipts:write','status:read'
718
+ ]::text[] from agent_bridge.credential_scope_sets where name='release-a-full')
719
+ and not exists (
720
+ with expected(policy_id,operation_id,capacity,refill_per_second) as (values
721
+ ('global',null::text,300,50::numeric),
722
+ ('operation:capabilities','capabilities',30,1),
723
+ ('operation:status','status',30,1),
724
+ ('operation:gateway_metrics','gateway_metrics',30,1),
725
+ ('operation:publish_message','publish_message',120,20),
726
+ ('operation:history','history',120,20),
727
+ ('operation:record_receipt','record_receipt',120,20),
728
+ ('operation:claim_delivery','claim_delivery',120,20),
729
+ ('operation:list_deliveries','list_deliveries',120,20),
730
+ ('operation:list_delivery_events','list_delivery_events',120,20),
731
+ ('operation:cancel_delivery','cancel_delivery',60,10),
732
+ ('operation:requeue_delivery','requeue_delivery',60,10),
733
+ ('operation:extend_delivery','extend_delivery',120,20),
734
+ ('operation:acknowledge_delivery','acknowledge_delivery',120,20),
735
+ ('operation:negative_acknowledge_delivery','negative_acknowledge_delivery',120,20),
736
+ ('operation:heartbeat','heartbeat',120,20),
737
+ ('operation:presence','presence',120,20)
738
+ )
739
+ select 1 from expected
740
+ left join agent_bridge.rate_limit_policies policy using (policy_id)
741
+ where policy.policy_id is null
742
+ or policy.operation_id is distinct from expected.operation_id
743
+ or policy.capacity is distinct from expected.capacity
744
+ or policy.refill_per_second is distinct from expected.refill_per_second
745
+ or not policy.enabled
746
+ )
747
+ and (select count(*)=17 from agent_bridge.rate_limit_policies)
748
+ and not exists (
749
+ select 1 from (values
750
+ ('security_events_append_only','agent_bridge.security_events'::regclass),
751
+ ('credentials_validate_security','agent_bridge.credentials'::regclass),
752
+ ('credentials_append_only','agent_bridge.credentials'::regclass),
753
+ ('credential_scope_sets_immutable','agent_bridge.credential_scope_sets'::regclass)
754
+ ) required(trigger_name,relation_id)
755
+ left join pg_catalog.pg_trigger trigger
756
+ on trigger.tgname=required.trigger_name
757
+ and trigger.tgrelid=required.relation_id
758
+ and not trigger.tgisinternal
759
+ where trigger.oid is null or trigger.tgenabled <> 'O'
760
+ )
761
+ and not exists (
762
+ select 1 from (values
763
+ ('agent_bridge.credentials'::regclass,'credentials_scopes_canonical'),
764
+ ('agent_bridge.credentials'::regclass,'credentials_replacement_not_self'),
765
+ ('agent_bridge.credentials'::regclass,'credentials_grace_shortens_expiry'),
766
+ ('agent_bridge.credential_scope_sets'::regclass,'credential_scope_sets_scopes'),
767
+ ('agent_bridge.security_events'::regclass,'security_events_type'),
768
+ ('agent_bridge.security_events'::regclass,'security_events_outcome'),
769
+ ('agent_bridge.security_events'::regclass,'security_events_reason'),
770
+ ('agent_bridge.security_events'::regclass,'security_events_retry'),
771
+ ('agent_bridge.security_events'::regclass,'security_events_actor'),
772
+ ('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_shape'),
773
+ ('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_operation'),
774
+ ('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_capacity'),
775
+ ('agent_bridge.rate_limit_policies'::regclass,'rate_limit_policy_refill'),
776
+ ('agent_bridge.rate_limit_buckets'::regclass,'rate_limit_buckets_tokens')
777
+ ) required(relation_id,constraint_name)
778
+ left join pg_catalog.pg_constraint constraint_record
779
+ on constraint_record.conrelid=required.relation_id
780
+ and constraint_record.conname=required.constraint_name
781
+ where constraint_record.oid is null or not constraint_record.convalidated
782
+ )
783
+ and exists (
784
+ select 1 from pg_catalog.pg_attribute attribute
785
+ where attribute.attrelid='agent_bridge.credentials'::regclass
786
+ and attribute.attname='scopes' and attribute.attnotnull
787
+ )
788
+ and pg_catalog.to_regclass('agent_bridge.credentials_replacement_lineage') is not null
789
+ and pg_catalog.to_regclass('agent_bridge.rate_limit_buckets_cleanup') is not null
790
+ and pg_catalog.to_regprocedure('agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)') is not null
791
+ and pg_catalog.to_regprocedure('agent_bridge.revoke_credential(uuid,text,text,uuid)') is not null
792
+ and (select count(*)=5 and bool_and(
793
+ procedure.prosecdef
794
+ and procedure.proconfig @> array['search_path=""']::text[]
795
+ ) from pg_catalog.pg_proc procedure
796
+ where procedure.oid in (
797
+ 'agent_bridge.record_scope_denial(uuid,text,uuid)'::regprocedure,
798
+ 'agent_bridge.consume_rate_limit(uuid,text,uuid)'::regprocedure,
799
+ 'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)'::regprocedure,
800
+ 'agent_bridge.revoke_credential(uuid,text,text,uuid)'::regprocedure,
801
+ 'agent_bridge.security_schema_ready()'::regprocedure
802
+ ))
803
+ and not exists (
804
+ select 1
805
+ from pg_catalog.pg_proc procedure
806
+ cross join lateral pg_catalog.aclexplode(
807
+ coalesce(procedure.proacl,pg_catalog.acldefault('f',procedure.proowner))
808
+ ) access
809
+ where procedure.oid in (
810
+ 'agent_bridge.record_scope_denial(uuid,text,uuid)'::regprocedure,
811
+ 'agent_bridge.consume_rate_limit(uuid,text,uuid)'::regprocedure,
812
+ 'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)'::regprocedure,
813
+ 'agent_bridge.revoke_credential(uuid,text,text,uuid)'::regprocedure,
814
+ 'agent_bridge.security_schema_ready()'::regprocedure
815
+ ) and access.grantee=0 and access.privilege_type='EXECUTE'
816
+ )
817
+ and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.record_scope_denial(uuid,text,uuid)','EXECUTE')
818
+ and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.consume_rate_limit(uuid,text,uuid)','EXECUTE')
819
+ and pg_catalog.has_function_privilege(runtime_role,'agent_bridge.security_schema_ready()','EXECUTE')
820
+ and not pg_catalog.has_function_privilege(runtime_role,'agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid)','EXECUTE')
821
+ and not pg_catalog.has_function_privilege(runtime_role,'agent_bridge.revoke_credential(uuid,text,text,uuid)','EXECUTE')
822
+ and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.credential_scope_sets','SELECT,INSERT,UPDATE,DELETE')
823
+ and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.security_events','SELECT,INSERT,UPDATE,DELETE')
824
+ and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.rate_limit_policies','SELECT,INSERT,UPDATE,DELETE')
825
+ and not pg_catalog.has_table_privilege(runtime_role,'agent_bridge.rate_limit_buckets','SELECT,INSERT,UPDATE,DELETE');
826
+ end
827
+ $$;
828
+
829
+ revoke all on agent_bridge.credential_scope_sets, agent_bridge.security_events,
830
+ agent_bridge.rate_limit_policies, agent_bridge.rate_limit_buckets from public;
831
+ revoke all on all sequences in schema agent_bridge from public;
832
+ revoke all on function agent_bridge.canonicalize_scopes(text[]) from public;
833
+ revoke all on function agent_bridge.record_scope_denial(uuid,text,uuid) from public;
834
+ revoke all on function agent_bridge.consume_rate_limit(uuid,text,uuid) from public;
835
+ revoke all on function agent_bridge.replace_credential(uuid,character,text[],text,text,timestamptz,timestamptz,text,uuid) from public;
836
+ revoke all on function agent_bridge.revoke_credential(uuid,text,text,uuid) from public;
837
+ revoke all on function agent_bridge.security_schema_ready() from public;
838
+
839
+ do $roles$
840
+ declare
841
+ role_name text;
842
+ runtime_role text := 'agent_bridge_runtime_' || substr(md5(current_database()), 1, 16);
843
+ begin
844
+ foreach role_name in array array['anon', 'authenticated'] loop
845
+ if exists (select 1 from pg_roles where rolname=role_name) then
846
+ execute format('revoke all on agent_bridge.credential_scope_sets, agent_bridge.security_events, agent_bridge.rate_limit_policies, agent_bridge.rate_limit_buckets from %I', role_name);
847
+ execute format('revoke all on function agent_bridge.record_scope_denial(uuid,text,uuid) from %I', role_name);
848
+ execute format('revoke all on function agent_bridge.consume_rate_limit(uuid,text,uuid) from %I', role_name);
849
+ execute format('revoke all on function agent_bridge.security_schema_ready() from %I', role_name);
850
+ end if;
851
+ end loop;
852
+ execute format('revoke all on agent_bridge.credential_scope_sets, agent_bridge.security_events, agent_bridge.rate_limit_policies, agent_bridge.rate_limit_buckets from %I', runtime_role);
853
+ execute format('grant execute on function agent_bridge.record_scope_denial(uuid,text,uuid) to %I', runtime_role);
854
+ execute format('grant execute on function agent_bridge.consume_rate_limit(uuid,text,uuid) to %I', runtime_role);
855
+ execute format('grant execute on function agent_bridge.security_schema_ready() to %I', runtime_role);
856
+ end
857
+ $roles$;
858
+
859
+ insert into agent_bridge.schema_migrations(version,name,checksum)
860
+ values (11,'credential_security','__AGENT_BRIDGE_MIGRATION_CHECKSUM__')
861
+ on conflict (version) do update set applied_at=agent_bridge.schema_migrations.applied_at
862
+ where agent_bridge.schema_migrations.name=excluded.name
863
+ and agent_bridge.schema_migrations.checksum=excluded.checksum;
864
+
865
+ do $migration$
866
+ begin
867
+ if not exists (select 1 from agent_bridge.schema_migrations
868
+ where version=11 and name='credential_security'
869
+ and checksum='__AGENT_BRIDGE_MIGRATION_CHECKSUM__') then
870
+ raise exception 'migration 11_credential_security conflicts with recorded schema state';
871
+ end if;
872
+ end
873
+ $migration$;
874
+
875
+ commit;