@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.
- package/CHANGELOG.md +86 -4
- package/README.md +567 -55
- package/ROADMAP.md +114 -0
- package/SECURITY.md +90 -0
- package/SKILL.md +68 -2
- package/bin/agent-bridge +2 -2
- package/clients/claude-desktop.json +3 -1
- package/dist/archive.d.ts +163 -0
- package/dist/archive.js +50 -0
- package/dist/archive.js.map +1 -0
- package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
- package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
- package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
- package/dist/bridge.d.ts +76 -17
- package/dist/bridge.js +52 -24
- package/dist/chunk-2DFNOLMO.js +618 -0
- package/dist/chunk-2DFNOLMO.js.map +1 -0
- package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
- package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
- package/dist/chunk-45BZGB77.js +709 -0
- package/dist/chunk-45BZGB77.js.map +1 -0
- package/dist/chunk-4VB44KJU.js +230 -0
- package/dist/chunk-4VB44KJU.js.map +1 -0
- package/dist/chunk-5GMB3A77.js +1013 -0
- package/dist/chunk-5GMB3A77.js.map +1 -0
- package/dist/chunk-A6W4QALF.js +1258 -0
- package/dist/chunk-A6W4QALF.js.map +1 -0
- package/dist/chunk-BZLMF5O5.js +328 -0
- package/dist/chunk-BZLMF5O5.js.map +1 -0
- package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
- package/dist/chunk-CEXBMOI5.js.map +1 -0
- package/dist/chunk-GADH244D.js +527 -0
- package/dist/chunk-GADH244D.js.map +1 -0
- package/dist/chunk-MBPZXTJA.js +2428 -0
- package/dist/chunk-MBPZXTJA.js.map +1 -0
- package/dist/chunk-NCO7RLBM.js +146 -0
- package/dist/chunk-NCO7RLBM.js.map +1 -0
- package/dist/chunk-Q6PPMXJC.js +1050 -0
- package/dist/chunk-Q6PPMXJC.js.map +1 -0
- package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
- package/dist/chunk-SPSMZOCJ.js.map +1 -0
- package/dist/chunk-UAHMP6GC.js +54 -0
- package/dist/chunk-UAHMP6GC.js.map +1 -0
- package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
- package/dist/chunk-VOQIDTCJ.js.map +1 -0
- package/dist/chunk-WAI2IYIQ.js +554 -0
- package/dist/chunk-WAI2IYIQ.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +3555 -214
- package/dist/cli.js.map +1 -1
- package/dist/client-config.d.ts +1 -1
- package/dist/client-config.js +1 -1
- package/dist/client-runtime.d.ts +10 -6
- package/dist/client-runtime.js +9 -6
- package/dist/contracts/generate.d.ts +2 -0
- package/dist/contracts/generate.js +247 -0
- package/dist/contracts/generate.js.map +1 -0
- package/dist/dr.d.ts +324 -0
- package/dist/dr.js +124 -0
- package/dist/dr.js.map +1 -0
- package/dist/gateway-CggQ8vTC.d.ts +78 -0
- package/dist/gateway-main.js +190 -8
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +4 -3
- package/dist/gateway.js +4 -3
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +13 -8
- package/dist/sqlite.d.ts +94 -12
- package/dist/sqlite.js +7 -4
- package/docs/architecture-v2.md +394 -0
- package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
- package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
- package/docs/decisions/0003-host-adapters-and-consumer-instance-keys.md +88 -0
- package/docs/ecosystem.md +86 -0
- package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
- package/docs/troubleshooting.md +89 -0
- package/llms.txt +107 -6
- package/openapi/agent-bridge-v2.openapi.json +9610 -0
- package/package.json +24 -4
- package/schemas/agent-bridge-v2.capabilities.json +1829 -0
- package/schemas/agent-bridge-v2.mcp.json +2074 -0
- package/schemas/agent-bridge-v2.schema.json +4560 -0
- package/sql/migrations/008_message_projects.sql +36 -0
- package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
- package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
- package/sql/migrations/011_credential_security.sql +875 -0
- package/sql/migrations/012_request_authority.sql +177 -0
- package/sql/migrations/013_row_isolation.sql +495 -0
- package/sql/migrations/014_owner_control_plane.sql +1641 -0
- package/sql/migrations/015_portable_archives.sql +1813 -0
- package/sql/migrations/016_native_dr_fence.sql +1005 -0
- package/bin/CLAUDE.md +0 -11
- package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
- package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
- package/dist/chunk-4KAOBC5X.js.map +0 -1
- package/dist/chunk-6MAZ6YLZ.js +0 -146
- package/dist/chunk-6MAZ6YLZ.js.map +0 -1
- package/dist/chunk-AXL2I7FH.js.map +0 -1
- package/dist/chunk-BV6W4AJK.js +0 -211
- package/dist/chunk-BV6W4AJK.js.map +0 -1
- package/dist/chunk-CKSKUBDQ.js +0 -153
- package/dist/chunk-CKSKUBDQ.js.map +0 -1
- package/dist/chunk-JOUBDRYX.js +0 -272
- package/dist/chunk-JOUBDRYX.js.map +0 -1
- package/dist/chunk-JTC5YURG.js +0 -49
- package/dist/chunk-JTC5YURG.js.map +0 -1
- package/dist/chunk-O26W5QHS.js +0 -409
- package/dist/chunk-O26W5QHS.js.map +0 -1
- package/dist/chunk-WEULPMUY.js +0 -988
- package/dist/chunk-WEULPMUY.js.map +0 -1
- package/dist/chunk-YD6YJATN.js.map +0 -1
- 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;
|