@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,1813 @@
1
+ begin;
2
+
3
+ select pg_advisory_xact_lock(1646705660);
4
+
5
+ do $preflight$
6
+ begin
7
+ if not agent_bridge.security_schema_ready()
8
+ or not agent_bridge.owner_control_plane_ready()
9
+ or not exists(
10
+ select 1 from agent_bridge.row_isolation_attestations attestation
11
+ where attestation.name='domain-v1'
12
+ and attestation.catalog_definition=agent_bridge.row_isolation_catalog_definition()
13
+ ) then
14
+ raise exception 'portable archive prerequisite readiness validation failed';
15
+ end if;
16
+ end
17
+ $preflight$;
18
+
19
+ do $roles$
20
+ declare
21
+ role_name text := 'agent_bridge_archive_operator_'||substr(md5(current_database()),1,16);
22
+ begin
23
+ if not exists(select 1 from pg_catalog.pg_roles where rolname=role_name) then
24
+ execute format(
25
+ 'create role %I nologin nosuperuser nocreatedb nocreaterole noreplication nobypassrls',
26
+ role_name
27
+ );
28
+ end if;
29
+ execute format(
30
+ 'alter role %I nologin nosuperuser nocreatedb nocreaterole noreplication nobypassrls',
31
+ role_name
32
+ );
33
+ execute format('grant %I to %I with admin option',role_name,current_user);
34
+ end
35
+ $roles$;
36
+
37
+ create table agent_bridge.archive_membership_events (
38
+ sequence bigint generated always as identity primary key,
39
+ request_id uuid not null unique,
40
+ action text not null,
41
+ member_role name not null,
42
+ actor name not null,
43
+ created_at timestamptz not null default clock_timestamp(),
44
+ constraint archive_membership_events_action check(action in ('register','revoke'))
45
+ );
46
+
47
+ create table agent_bridge.archive_operations (
48
+ sequence bigint generated always as identity primary key,
49
+ request_id uuid not null,
50
+ phase text not null,
51
+ operation text not null,
52
+ workspace text not null,
53
+ client_verified_digest char(64),
54
+ message_count bigint,
55
+ receipt_count bigint,
56
+ message_inserted_count bigint,
57
+ receipt_inserted_count bigint,
58
+ server_content_binding char(64),
59
+ outcome text,
60
+ failure_code text,
61
+ published_at timestamptz,
62
+ apply boolean,
63
+ actor name not null,
64
+ created_at timestamptz not null default clock_timestamp(),
65
+ constraint archive_operations_request_phase unique(request_id,phase),
66
+ constraint archive_operations_phase check(phase in ('begin','complete')),
67
+ constraint archive_operations_operation check(operation in ('export','import')),
68
+ constraint archive_operations_digest check(
69
+ (phase='begin' and operation='export' and client_verified_digest is null)
70
+ or (phase='complete' and outcome='abandoned' and client_verified_digest is null)
71
+ or (client_verified_digest ~ '^[0-9a-f]{64}$')
72
+ ),
73
+ constraint archive_operations_counts check(
74
+ (phase='begin' and (
75
+ (operation='export' and message_count is null and receipt_count is null)
76
+ or (operation='import' and message_count>=0 and receipt_count>=0)
77
+ ) and message_inserted_count is null and receipt_inserted_count is null
78
+ and server_content_binding is null and outcome is null and failure_code is null
79
+ and published_at is null
80
+ and apply is null)
81
+ or (phase='complete' and message_count>=0 and receipt_count>=0
82
+ and message_inserted_count>=0 and message_inserted_count<=message_count
83
+ and receipt_inserted_count>=0 and receipt_inserted_count<=receipt_count
84
+ and server_content_binding ~ '^[0-9a-f]{64}$' and apply is not null
85
+ and outcome in ('published','client_reconciled','applied','dry-run') and failure_code is null
86
+ and ((outcome in ('published','client_reconciled') and published_at is not null)
87
+ or (outcome in ('applied','dry-run') and published_at is null)))
88
+ or (phase='complete' and outcome='abandoned'
89
+ and failure_code ~ '^[a-z][a-z0-9_]{0,63}$' and client_verified_digest is null
90
+ and message_count=0 and receipt_count=0 and message_inserted_count=0
91
+ and receipt_inserted_count=0 and server_content_binding=
92
+ encode(sha256(convert_to('','UTF8')),'hex') and published_at is null and apply=false)
93
+ ),
94
+ constraint archive_operations_outcome check(
95
+ outcome is null or outcome in ('published','client_reconciled','applied','dry-run','abandoned')
96
+ )
97
+ );
98
+
99
+ create table agent_bridge.archive_operation_batches (
100
+ sequence bigint generated always as identity primary key,
101
+ request_id uuid not null,
102
+ record_kind text not null,
103
+ batch_ordinal bigint not null,
104
+ record_count bigint not null,
105
+ batch_fingerprint char(64) not null,
106
+ chain_binding char(64) not null,
107
+ actor name not null,
108
+ created_at timestamptz not null default clock_timestamp(),
109
+ constraint archive_operation_batches_identity unique(request_id,record_kind,batch_ordinal),
110
+ constraint archive_operation_batches_kind check(record_kind in ('message','receipt')),
111
+ constraint archive_operation_batches_ordinal check(batch_ordinal>=0),
112
+ constraint archive_operation_batches_count check(record_count between 1 and 1000),
113
+ constraint archive_operation_batches_fingerprint check(batch_fingerprint ~ '^[0-9a-f]{64}$'),
114
+ constraint archive_operation_batches_chain check(chain_binding ~ '^[0-9a-f]{64}$')
115
+ );
116
+
117
+ create table agent_bridge.archive_import_records (
118
+ sequence bigint generated always as identity primary key,
119
+ request_id uuid not null,
120
+ record_kind text not null,
121
+ record_key text not null,
122
+ batch_ordinal bigint not null,
123
+ semantic_fingerprint char(64) not null,
124
+ inserted boolean not null,
125
+ actor name not null,
126
+ created_at timestamptz not null default clock_timestamp(),
127
+ constraint archive_import_records_identity unique(request_id,record_kind,record_key),
128
+ constraint archive_import_records_kind check(record_kind in ('message','receipt')),
129
+ constraint archive_import_records_key check(record_key<>''),
130
+ constraint archive_import_records_ordinal check(batch_ordinal>=0),
131
+ constraint archive_import_records_fingerprint check(semantic_fingerprint ~ '^[0-9a-f]{64}$')
132
+ );
133
+
134
+ create table agent_bridge.archive_transaction_authorizations (
135
+ backend_pid integer not null,
136
+ transaction_id text not null,
137
+ request_id uuid not null,
138
+ operation text not null,
139
+ workspace text not null,
140
+ client_verified_digest char(64),
141
+ actor name not null,
142
+ messages_consumed boolean not null default false,
143
+ receipts_consumed boolean not null default false,
144
+ created_at timestamptz not null default clock_timestamp(),
145
+ primary key(backend_pid,transaction_id,request_id),
146
+ constraint archive_transaction_authorizations_backend check(backend_pid>0),
147
+ constraint archive_transaction_authorizations_transaction check(transaction_id~'^[0-9]+$'),
148
+ constraint archive_transaction_authorizations_operation check(operation in ('export','import')),
149
+ constraint archive_transaction_authorizations_digest check(
150
+ (operation='export' and client_verified_digest is null)
151
+ or (operation='import' and client_verified_digest~'^[0-9a-f]{64}$')
152
+ )
153
+ );
154
+
155
+ create table agent_bridge.portable_archive_attestations (
156
+ name text primary key,
157
+ catalog_definition text not null,
158
+ attested_at timestamptz not null default now(),
159
+ constraint portable_archive_attestation_name check(name='portable-archive-v1')
160
+ );
161
+
162
+ create index archive_operations_workspace_sequence
163
+ on agent_bridge.archive_operations(workspace,sequence desc);
164
+ create index archive_membership_events_member_sequence
165
+ on agent_bridge.archive_membership_events(member_role,sequence desc);
166
+ create index archive_operation_batches_request_sequence
167
+ on agent_bridge.archive_operation_batches(request_id,sequence);
168
+ create index archive_import_records_request_sequence
169
+ on agent_bridge.archive_import_records(request_id,sequence);
170
+
171
+ create or replace function agent_bridge.reject_archive_ledger_mutation()
172
+ returns trigger language plpgsql set search_path = '' as $$
173
+ begin
174
+ raise exception 'portable archive ledgers are append-only';
175
+ end
176
+ $$;
177
+
178
+ create trigger archive_membership_events_append_only
179
+ before update or delete or truncate on agent_bridge.archive_membership_events
180
+ for each statement execute function agent_bridge.reject_archive_ledger_mutation();
181
+ create trigger archive_operations_append_only
182
+ before update or delete or truncate on agent_bridge.archive_operations
183
+ for each statement execute function agent_bridge.reject_archive_ledger_mutation();
184
+ create trigger archive_operation_batches_append_only
185
+ before update or delete or truncate on agent_bridge.archive_operation_batches
186
+ for each statement execute function agent_bridge.reject_archive_ledger_mutation();
187
+ create trigger archive_import_records_append_only
188
+ before update or delete or truncate on agent_bridge.archive_import_records
189
+ for each statement execute function agent_bridge.reject_archive_ledger_mutation();
190
+ create trigger portable_archive_attestations_append_only
191
+ before update or delete or truncate on agent_bridge.portable_archive_attestations
192
+ for each statement execute function agent_bridge.reject_archive_ledger_mutation();
193
+
194
+ create or replace function agent_bridge.assert_archive_actor()
195
+ returns void language plpgsql security definer set search_path = '' as $$
196
+ declare
197
+ suffix text := substr(md5(current_database()),1,16);
198
+ schema_owner name;
199
+ archive_role name := ('agent_bridge_archive_operator_'||suffix)::name;
200
+ archive_allowed boolean := false;
201
+ begin
202
+ if not agent_bridge.portable_archive_ready() then
203
+ raise exception 'portable archive readiness validation failed';
204
+ end if;
205
+ select pg_catalog.pg_get_userbyid(namespace.nspowner)::name into schema_owner
206
+ from pg_catalog.pg_namespace namespace where namespace.nspname='agent_bridge';
207
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
208
+ 'archive-membership'||chr(31)||session_user||chr(31)||'global',1646705662
209
+ ));
210
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
211
+ 'archive-membership'||chr(31)||session_user||chr(31)||'operator',1646705662
212
+ ));
213
+
214
+ with latest as (
215
+ select event.action from agent_bridge.archive_membership_events event
216
+ where event.member_role=session_user order by event.sequence desc limit 1
217
+ ), direct_membership as (
218
+ select membership.admin_option,
219
+ coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
220
+ coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
221
+ grantor.rolname grantor_role
222
+ from pg_catalog.pg_auth_members membership
223
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
224
+ join pg_catalog.pg_roles member on member.oid=membership.member
225
+ join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
226
+ where member.rolname=session_user and granted.rolname=archive_role
227
+ )
228
+ select coalesce(bool_or(
229
+ membership.inherit_option and membership.set_option
230
+ and membership.grantor_role=schema_owner and (
231
+ (session_user=schema_owner and membership.admin_option)
232
+ or (latest.action='register' and not membership.admin_option)
233
+ )
234
+ ),false) into archive_allowed
235
+ from direct_membership membership left join latest on true;
236
+
237
+ if session_user<>schema_owner and (exists (
238
+ select 1 from pg_catalog.pg_roles inherited
239
+ where inherited.rolname<>session_user
240
+ and pg_catalog.pg_has_role(session_user,inherited.rolname,'MEMBER')
241
+ and inherited.rolname<>archive_role
242
+ ) or exists (
243
+ select 1 from pg_catalog.pg_roles candidate
244
+ where candidate.rolname not in (session_user,schema_owner)
245
+ and not candidate.rolsuper
246
+ and pg_catalog.pg_has_role(candidate.rolname,session_user,'MEMBER')
247
+ ) or exists (
248
+ select 1 from pg_catalog.pg_roles actor where actor.rolname=session_user and (
249
+ not actor.rolcanlogin or actor.rolsuper or actor.rolcreaterole or actor.rolcreatedb
250
+ or actor.rolreplication or actor.rolbypassrls
251
+ )
252
+ )) then
253
+ raise exception 'archive actor has an unsafe membership graph';
254
+ end if;
255
+ if not archive_allowed then
256
+ raise exception 'archive actor is not registered';
257
+ end if;
258
+ end
259
+ $$;
260
+
261
+ create or replace function agent_bridge.register_archive_member(
262
+ requested_request_id uuid, requested_member_role name
263
+ ) returns table(replayed boolean)
264
+ language plpgsql security definer set search_path = '' as $$
265
+ declare
266
+ suffix text := substr(md5(current_database()),1,16);
267
+ archive_role name := ('agent_bridge_archive_operator_'||suffix)::name;
268
+ prior agent_bridge.archive_membership_events%rowtype;
269
+ member_record pg_catalog.pg_roles%rowtype;
270
+ begin
271
+ if not agent_bridge.portable_archive_ready() then
272
+ raise exception 'portable archive readiness validation failed';
273
+ end if;
274
+ if requested_request_id is null or requested_member_role is null
275
+ or requested_member_role in (current_user::name,archive_role) then
276
+ raise exception 'invalid archive membership registration';
277
+ end if;
278
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
279
+ 'archive-membership'||chr(31)||requested_member_role||chr(31)||'global',1646705662
280
+ ));
281
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
282
+ 'archive-membership'||chr(31)||requested_member_role||chr(31)||'operator',1646705662
283
+ ));
284
+ perform pg_catalog.pg_advisory_xact_lock(
285
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
286
+ );
287
+ select * into prior from agent_bridge.archive_membership_events
288
+ where request_id=requested_request_id;
289
+ if found then
290
+ if prior.action<>'register' or prior.member_role<>requested_member_role then
291
+ raise exception 'request id was already used with different content';
292
+ end if;
293
+ return query select true;
294
+ return;
295
+ end if;
296
+ select * into member_record from pg_catalog.pg_roles where rolname=requested_member_role;
297
+ if not found or not member_record.rolcanlogin or member_record.rolsuper
298
+ or member_record.rolcreaterole or member_record.rolcreatedb
299
+ or member_record.rolreplication or member_record.rolbypassrls then
300
+ raise exception 'archive membership target is not an eligible login';
301
+ end if;
302
+ if exists (
303
+ select 1 from pg_catalog.pg_roles inherited
304
+ where inherited.rolname<>requested_member_role
305
+ and pg_catalog.pg_has_role(requested_member_role,inherited.rolname,'MEMBER')
306
+ and inherited.rolname<>archive_role
307
+ ) or exists (
308
+ select 1 from pg_catalog.pg_roles candidate
309
+ where candidate.rolname not in (requested_member_role,current_user)
310
+ and not candidate.rolsuper
311
+ and pg_catalog.pg_has_role(candidate.rolname,requested_member_role,'MEMBER')
312
+ ) then
313
+ raise exception 'archive membership target has an unsafe membership graph';
314
+ end if;
315
+ if exists (
316
+ select 1 from pg_catalog.pg_auth_members membership
317
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
318
+ join pg_catalog.pg_roles member on member.oid=membership.member
319
+ join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
320
+ where granted.rolname=archive_role and member.rolname=requested_member_role and (
321
+ membership.admin_option
322
+ or not coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true)
323
+ or not coalesce((to_jsonb(membership)->>'set_option')::boolean,true)
324
+ or grantor.rolname<>current_user
325
+ )
326
+ ) then
327
+ raise exception 'archive membership target has an unsafe direct grant';
328
+ end if;
329
+ execute format('grant %I to %I',archive_role,requested_member_role);
330
+ insert into agent_bridge.archive_membership_events(request_id,action,member_role,actor)
331
+ values(requested_request_id,'register',requested_member_role,session_user);
332
+ return query select false;
333
+ end
334
+ $$;
335
+
336
+ create or replace function agent_bridge.revoke_archive_member(
337
+ requested_request_id uuid, requested_member_role name
338
+ ) returns table(replayed boolean)
339
+ language plpgsql security definer set search_path = '' as $$
340
+ declare
341
+ suffix text := substr(md5(current_database()),1,16);
342
+ archive_role name := ('agent_bridge_archive_operator_'||suffix)::name;
343
+ prior agent_bridge.archive_membership_events%rowtype;
344
+ begin
345
+ if not agent_bridge.portable_archive_ready() then
346
+ raise exception 'portable archive readiness validation failed';
347
+ end if;
348
+ if requested_request_id is null or requested_member_role is null
349
+ or requested_member_role in (current_user::name,archive_role) then
350
+ raise exception 'invalid archive membership revocation';
351
+ end if;
352
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
353
+ 'archive-membership'||chr(31)||requested_member_role||chr(31)||'global',1646705662
354
+ ));
355
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
356
+ 'archive-membership'||chr(31)||requested_member_role||chr(31)||'operator',1646705662
357
+ ));
358
+ perform pg_catalog.pg_advisory_xact_lock(
359
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
360
+ );
361
+ select * into prior from agent_bridge.archive_membership_events
362
+ where request_id=requested_request_id;
363
+ if found then
364
+ if prior.action<>'revoke' or prior.member_role<>requested_member_role then
365
+ raise exception 'request id was already used with different content';
366
+ end if;
367
+ return query select true;
368
+ return;
369
+ end if;
370
+ if exists(select 1 from pg_catalog.pg_roles where rolname=requested_member_role) then
371
+ execute format('revoke %I from %I',archive_role,requested_member_role);
372
+ end if;
373
+ insert into agent_bridge.archive_membership_events(request_id,action,member_role,actor)
374
+ values(requested_request_id,'revoke',requested_member_role,session_user);
375
+ return query select false;
376
+ end
377
+ $$;
378
+
379
+ create or replace function agent_bridge.archive_assert_operation(
380
+ requested_request_id uuid, requested_operation text, requested_workspace text,
381
+ requested_digest character(64)
382
+ ) returns void language plpgsql security definer set search_path = '' as $$
383
+ declare
384
+ binding agent_bridge.archive_operations%rowtype;
385
+ begin
386
+ perform agent_bridge.assert_archive_actor();
387
+ if requested_request_id is null or requested_operation not in ('export','import')
388
+ or requested_workspace is null or requested_workspace='' or requested_workspace<>btrim(requested_workspace)
389
+ or (requested_operation='import' and (
390
+ requested_digest is null or requested_digest!~'^[0-9a-f]{64}$'
391
+ )) or (requested_operation='export' and requested_digest is not null) then
392
+ raise exception 'archive operation binding is invalid';
393
+ end if;
394
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
395
+ 'portable-archive'||chr(31)||requested_workspace,1646705663
396
+ ));
397
+ perform pg_catalog.pg_advisory_xact_lock(
398
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
399
+ );
400
+ select * into binding from agent_bridge.archive_operations
401
+ where request_id=requested_request_id and phase='begin';
402
+ if not found or binding.operation<>requested_operation
403
+ or binding.workspace<>requested_workspace
404
+ or binding.client_verified_digest is distinct from requested_digest
405
+ or binding.actor<>session_user then
406
+ raise exception 'archive operation binding does not match the active request';
407
+ end if;
408
+ if exists(select 1 from agent_bridge.archive_operations
409
+ where request_id=requested_request_id and phase='complete') then
410
+ raise exception 'archive operation is final';
411
+ end if;
412
+ end
413
+ $$;
414
+
415
+ create or replace function agent_bridge.archive_authorize_transaction(
416
+ requested_request_id uuid, requested_operation text, requested_workspace text,
417
+ requested_digest character(64)
418
+ ) returns void language plpgsql security definer set search_path = '' as $$
419
+ declare prior agent_bridge.archive_transaction_authorizations%rowtype;
420
+ begin
421
+ perform agent_bridge.archive_assert_operation(
422
+ requested_request_id,requested_operation,requested_workspace,requested_digest
423
+ );
424
+ select * into prior from agent_bridge.archive_transaction_authorizations transaction_auth
425
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
426
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
427
+ and transaction_auth.request_id=requested_request_id;
428
+ if found then
429
+ if prior.operation<>requested_operation or prior.workspace<>requested_workspace
430
+ or prior.client_verified_digest is distinct from requested_digest
431
+ or prior.actor<>session_user then
432
+ raise exception 'archive transaction authorization conflicts with prior content';
433
+ end if;
434
+ return;
435
+ end if;
436
+ insert into agent_bridge.archive_transaction_authorizations(
437
+ backend_pid,transaction_id,request_id,operation,workspace,client_verified_digest,actor
438
+ ) values (
439
+ pg_catalog.pg_backend_pid(),pg_catalog.pg_current_xact_id()::text,
440
+ requested_request_id,requested_operation,requested_workspace,requested_digest,session_user
441
+ );
442
+ end
443
+ $$;
444
+
445
+ create or replace function agent_bridge.archive_assert_transaction(
446
+ requested_request_id uuid, requested_operation text, requested_workspace text,
447
+ requested_digest character(64)
448
+ ) returns void language plpgsql security definer set search_path = '' as $$
449
+ begin
450
+ if not exists(select 1 from agent_bridge.archive_transaction_authorizations transaction_auth
451
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
452
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
453
+ and transaction_auth.request_id=requested_request_id
454
+ and transaction_auth.operation=requested_operation
455
+ and transaction_auth.workspace=requested_workspace
456
+ and transaction_auth.client_verified_digest is not distinct from requested_digest
457
+ and transaction_auth.actor=session_user) then
458
+ raise exception 'archive transaction is not authorized';
459
+ end if;
460
+ end
461
+ $$;
462
+
463
+ create or replace function agent_bridge.archive_close_transaction_authorization(
464
+ requested_request_id uuid
465
+ ) returns void language plpgsql security definer set search_path = '' as $$
466
+ begin
467
+ delete from agent_bridge.archive_transaction_authorizations transaction_auth
468
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
469
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
470
+ and transaction_auth.request_id=requested_request_id and transaction_auth.actor=session_user;
471
+ if not found then raise exception 'archive transaction authorization is missing'; end if;
472
+ end
473
+ $$;
474
+
475
+ create or replace function agent_bridge.archive_begin_operation(
476
+ requested_request_id uuid, requested_operation text, requested_workspace text,
477
+ requested_digest character(64), requested_message_count bigint,
478
+ requested_receipt_count bigint
479
+ ) returns table(
480
+ replayed boolean, completed boolean, outcome text, message_count bigint,
481
+ receipt_count bigint, message_inserted_count bigint, receipt_inserted_count bigint,
482
+ apply boolean, client_verified_digest character(64), published_at text
483
+ )
484
+ language plpgsql security definer set search_path = '' as $$
485
+ declare
486
+ binding agent_bridge.archive_operations%rowtype;
487
+ terminal agent_bridge.archive_operations%rowtype;
488
+ begin
489
+ perform agent_bridge.assert_archive_actor();
490
+ if requested_request_id is null or requested_operation not in ('export','import')
491
+ or requested_workspace is null or requested_workspace='' or requested_workspace<>btrim(requested_workspace)
492
+ or (requested_operation='import' and (
493
+ requested_digest is null or requested_digest!~'^[0-9a-f]{64}$'
494
+ or requested_message_count is null or requested_message_count<0
495
+ or requested_receipt_count is null or requested_receipt_count<0
496
+ )) or (requested_operation='export' and (
497
+ requested_digest is not null or requested_message_count is not null
498
+ or requested_receipt_count is not null
499
+ )) then
500
+ raise exception 'archive operation binding is invalid';
501
+ end if;
502
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
503
+ 'portable-archive'||chr(31)||requested_workspace,1646705663
504
+ ));
505
+ perform pg_catalog.pg_advisory_xact_lock(
506
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
507
+ );
508
+ if not exists(select 1 from agent_bridge.workspaces where id=requested_workspace) then
509
+ raise exception 'archive workspace does not exist';
510
+ end if;
511
+ select * into binding from agent_bridge.archive_operations
512
+ where request_id=requested_request_id and phase='begin';
513
+ if found then
514
+ if binding.operation<>requested_operation or binding.workspace<>requested_workspace
515
+ or binding.client_verified_digest is distinct from requested_digest
516
+ or binding.message_count is distinct from requested_message_count
517
+ or binding.receipt_count is distinct from requested_receipt_count
518
+ or binding.actor<>session_user then
519
+ raise exception 'request id was already used with different content';
520
+ end if;
521
+ select * into terminal from agent_bridge.archive_operations
522
+ where request_id=requested_request_id and phase='complete';
523
+ if found and terminal.outcome='abandoned' then
524
+ raise exception 'archive operation was abandoned';
525
+ end if;
526
+ return query select true,found,terminal.outcome,terminal.message_count,
527
+ terminal.receipt_count,terminal.message_inserted_count,
528
+ terminal.receipt_inserted_count,terminal.apply,terminal.client_verified_digest,
529
+ case when terminal.published_at is null then null else to_char(
530
+ terminal.published_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
531
+ ) end;
532
+ return;
533
+ end if;
534
+ insert into agent_bridge.archive_operations(
535
+ request_id,phase,operation,workspace,client_verified_digest,message_count,receipt_count,actor
536
+ ) values (
537
+ requested_request_id,'begin',requested_operation,requested_workspace,
538
+ requested_digest,requested_message_count,requested_receipt_count,session_user
539
+ );
540
+ return query select false,false,null::text,null::bigint,null::bigint,
541
+ null::bigint,null::bigint,null::boolean,null::character(64),null::text;
542
+ end
543
+ $$;
544
+
545
+ create or replace function agent_bridge.archive_export_messages(
546
+ requested_request_id uuid, requested_workspace text,
547
+ after_created_at timestamptz, after_id uuid, requested_limit integer
548
+ ) returns table(
549
+ sequence bigint,id uuid,workspace text,project text,source text,type text,content text,
550
+ content_type text,data jsonb,targets jsonb,thread_id text,reply_to_id text,
551
+ correlation_id text,causation_id text,priority text,expires_at text,idempotency_key text,
552
+ atrib_receipt_id text,informed_by jsonb,metadata jsonb,delivery_mode text,
553
+ delivery_max_attempts integer,delivery_retry_base_delay_ms integer,
554
+ delivery_retry_max_delay_ms integer,delivery_retry_jitter_ratio double precision,
555
+ delivery_not_before text,created_at text
556
+ ) language plpgsql security definer set search_path = '' set timezone = 'UTC' as $$
557
+ declare returned_rows bigint;
558
+ begin
559
+ perform agent_bridge.archive_assert_transaction(
560
+ requested_request_id,'export',requested_workspace,null
561
+ );
562
+ if requested_limit is null or requested_limit not between 1 and 500
563
+ or ((after_created_at is null)<>(after_id is null)) then
564
+ raise exception 'archive export message page is invalid';
565
+ end if;
566
+ if coalesce((select pg_catalog.octet_length(to_jsonb(message)::text)+512
567
+ from agent_bridge.messages message where message.workspace=requested_workspace
568
+ and (after_created_at is null or (message.created_at,message.id)>(after_created_at,after_id))
569
+ order by message.created_at,message.id limit 1),0)>3670016 then
570
+ raise exception 'archive message exceeds the export record byte budget';
571
+ end if;
572
+ return query with size_candidate as materialized (
573
+ select message.id,message.created_at,
574
+ pg_catalog.octet_length(to_jsonb(message)::text)+512 raw_bytes
575
+ from agent_bridge.messages message where message.workspace=requested_workspace
576
+ and (after_created_at is null or (message.created_at,message.id)>(after_created_at,after_id))
577
+ order by message.created_at,message.id limit requested_limit
578
+ ), admitted as (
579
+ select size_candidate.*,
580
+ sum(size_candidate.raw_bytes) over(order by size_candidate.created_at,size_candidate.id)
581
+ archive_cumulative_bytes
582
+ from size_candidate
583
+ )
584
+ select message.sequence,message.id,message.workspace,message.project,message.source,message.type,
585
+ message.content,message.content_type,message.data,message.targets,message.thread_id,
586
+ message.reply_to_id,message.correlation_id,message.causation_id,message.priority,
587
+ case when message.expires_at is null then null else
588
+ to_char(message.expires_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') end,
589
+ message.idempotency_key,message.atrib_receipt_id,message.informed_by,message.metadata,
590
+ message.delivery_mode,message.delivery_max_attempts,message.delivery_retry_base_delay_ms,
591
+ message.delivery_retry_max_delay_ms,message.delivery_retry_jitter_ratio,
592
+ case when message.delivery_not_before is null then null else
593
+ to_char(message.delivery_not_before at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"') end,
594
+ to_char(message.created_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
595
+ from admitted join agent_bridge.messages message on message.id=admitted.id
596
+ where admitted.archive_cumulative_bytes<=4194304
597
+ order by admitted.created_at,admitted.id;
598
+ get diagnostics returned_rows = row_count;
599
+ if returned_rows=0 then
600
+ update agent_bridge.archive_transaction_authorizations transaction_auth
601
+ set messages_consumed=true
602
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
603
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
604
+ and transaction_auth.request_id=requested_request_id and transaction_auth.actor=session_user;
605
+ if not found then raise exception 'archive transaction authorization is missing'; end if;
606
+ end if;
607
+ end
608
+ $$;
609
+
610
+ create or replace function agent_bridge.archive_export_receipts(
611
+ requested_request_id uuid, requested_workspace text, after_created_at timestamptz,
612
+ after_message_id uuid, after_principal text, requested_limit integer
613
+ ) returns table(message_id uuid,principal text,read_at text,message_created_at text)
614
+ language plpgsql security definer set search_path = '' set timezone = 'UTC' as $$
615
+ declare returned_rows bigint;
616
+ begin
617
+ perform agent_bridge.archive_assert_transaction(
618
+ requested_request_id,'export',requested_workspace,null
619
+ );
620
+ if requested_limit is null or requested_limit not between 1 and 1000
621
+ or ((after_created_at is null)<>(after_message_id is null))
622
+ or ((after_created_at is null)<>(after_principal is null)) then
623
+ raise exception 'archive export receipt page is invalid';
624
+ end if;
625
+ if coalesce((select pg_catalog.octet_length(jsonb_build_object(
626
+ 'message_id',receipt.message_id,'principal',receipt.principal,
627
+ 'read_at',receipt.read_at,'message_created_at',message.created_at
628
+ )::text)+256 from agent_bridge.receipts receipt
629
+ join agent_bridge.messages message on message.workspace=receipt.workspace
630
+ and message.id=receipt.message_id
631
+ where receipt.workspace=requested_workspace
632
+ and (after_created_at is null or (message.created_at,message.id,receipt.principal)>
633
+ (after_created_at,after_message_id,after_principal))
634
+ order by message.created_at,message.id,receipt.principal limit 1),0)>1048576 then
635
+ raise exception 'archive receipt exceeds the export record byte budget';
636
+ end if;
637
+ return query with size_candidate as materialized (
638
+ select receipt.message_id,receipt.principal,message.created_at,
639
+ pg_catalog.octet_length(jsonb_build_object(
640
+ 'message_id',receipt.message_id,'principal',receipt.principal,
641
+ 'read_at',receipt.read_at,'message_created_at',message.created_at
642
+ )::text)+256 raw_bytes
643
+ from agent_bridge.receipts receipt
644
+ join agent_bridge.messages message on message.workspace=receipt.workspace
645
+ and message.id=receipt.message_id
646
+ where receipt.workspace=requested_workspace
647
+ and (after_created_at is null or (message.created_at,message.id,receipt.principal)>
648
+ (after_created_at,after_message_id,after_principal))
649
+ order by message.created_at,message.id,receipt.principal limit requested_limit
650
+ ), admitted as (
651
+ select size_candidate.*,
652
+ sum(size_candidate.raw_bytes)
653
+ over(order by size_candidate.created_at,size_candidate.message_id,size_candidate.principal)
654
+ archive_cumulative_bytes
655
+ from size_candidate
656
+ )
657
+ select receipt.message_id,receipt.principal,
658
+ to_char(receipt.read_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"'),
659
+ to_char(message.created_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
660
+ from admitted join agent_bridge.receipts receipt on receipt.workspace=requested_workspace
661
+ and receipt.message_id=admitted.message_id and receipt.principal=admitted.principal
662
+ join agent_bridge.messages message on message.workspace=receipt.workspace
663
+ and message.id=receipt.message_id
664
+ where admitted.archive_cumulative_bytes<=4194304
665
+ order by admitted.created_at,admitted.message_id,admitted.principal;
666
+ get diagnostics returned_rows = row_count;
667
+ if returned_rows=0 then
668
+ update agent_bridge.archive_transaction_authorizations transaction_auth
669
+ set receipts_consumed=true
670
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
671
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
672
+ and transaction_auth.request_id=requested_request_id and transaction_auth.actor=session_user;
673
+ if not found then raise exception 'archive transaction authorization is missing'; end if;
674
+ end if;
675
+ end
676
+ $$;
677
+
678
+ create or replace function agent_bridge.archive_json_depth(requested_value jsonb)
679
+ returns integer language sql immutable strict set search_path = '' as $$
680
+ with recursive walk(value,depth) as (
681
+ select requested_value,0
682
+ union all
683
+ select child.value,walk.depth+1 from walk
684
+ cross join lateral (
685
+ select element.value from jsonb_array_elements(case when jsonb_typeof(walk.value)='array'
686
+ then walk.value else '[]'::jsonb end) element
687
+ union all
688
+ select property.value from jsonb_each(case when jsonb_typeof(walk.value)='object'
689
+ then walk.value else '{}'::jsonb end) property
690
+ ) child
691
+ where walk.depth<=16
692
+ )
693
+ select coalesce(max(depth),0) from walk
694
+ $$;
695
+
696
+ create or replace function agent_bridge.archive_import_messages(
697
+ requested_request_id uuid, requested_workspace text, requested_digest character(64),
698
+ requested_batch_ordinal bigint, requested_batch jsonb
699
+ ) returns table(processed bigint,inserted bigint)
700
+ language plpgsql security definer set search_path = '' set timezone = 'UTC' as $$
701
+ declare
702
+ item jsonb;
703
+ existing agent_bridge.messages%rowtype;
704
+ delivery jsonb;
705
+ data_value jsonb;
706
+ metadata_value jsonb;
707
+ message_id uuid;
708
+ created_at_value timestamptz;
709
+ expires_at_value timestamptz;
710
+ not_before_value timestamptz;
711
+ prior_batch agent_bridge.archive_operation_batches%rowtype;
712
+ prior_record agent_bridge.archive_import_records%rowtype;
713
+ batch_fingerprint_value text;
714
+ previous_chain_binding text;
715
+ chain_binding_value text;
716
+ item_fingerprint text;
717
+ was_inserted boolean;
718
+ processed_value bigint := 0;
719
+ inserted_value bigint := 0;
720
+ begin
721
+ perform agent_bridge.archive_assert_transaction(
722
+ requested_request_id,'import',requested_workspace,requested_digest
723
+ );
724
+ if requested_batch is null or jsonb_typeof(requested_batch)<>'array'
725
+ or requested_batch_ordinal is null or requested_batch_ordinal<0
726
+ or jsonb_array_length(requested_batch)=0
727
+ or jsonb_array_length(requested_batch)>1000
728
+ or pg_catalog.pg_column_size(requested_batch)>4194304 then
729
+ raise exception 'archive message batch is invalid';
730
+ end if;
731
+ batch_fingerprint_value := encode(sha256(convert_to(requested_batch::text,'UTF8')),'hex');
732
+ select * into prior_batch from agent_bridge.archive_operation_batches
733
+ where request_id=requested_request_id and record_kind='message'
734
+ and batch_ordinal=requested_batch_ordinal;
735
+ if found then
736
+ if prior_batch.batch_fingerprint<>batch_fingerprint_value
737
+ or prior_batch.record_count<>jsonb_array_length(requested_batch)
738
+ or prior_batch.actor<>session_user then
739
+ raise exception 'archive message batch ordinal was already used with different content';
740
+ end if;
741
+ return query select prior_batch.record_count,
742
+ coalesce((select count(*) from agent_bridge.archive_import_records record
743
+ where record.request_id=requested_request_id and record.record_kind='message'
744
+ and record.batch_ordinal=requested_batch_ordinal and record.inserted),0)::bigint;
745
+ return;
746
+ end if;
747
+ if requested_batch_ordinal<>coalesce((select max(batch.batch_ordinal)+1
748
+ from agent_bridge.archive_operation_batches batch
749
+ where batch.request_id=requested_request_id and batch.record_kind='message'),0)
750
+ or exists(select 1 from agent_bridge.archive_operation_batches batch
751
+ where batch.request_id=requested_request_id and batch.record_kind='receipt') then
752
+ raise exception 'archive message batches must be contiguous and precede receipts';
753
+ end if;
754
+ if requested_batch_ordinal=0 then
755
+ previous_chain_binding := encode(sha256(convert_to('','UTF8')),'hex');
756
+ else
757
+ select batch.chain_binding into previous_chain_binding
758
+ from agent_bridge.archive_operation_batches batch
759
+ where batch.request_id=requested_request_id and batch.record_kind='message'
760
+ and batch.batch_ordinal=requested_batch_ordinal-1;
761
+ end if;
762
+ chain_binding_value := encode(sha256(convert_to(
763
+ previous_chain_binding||chr(31)||'message'||chr(31)||requested_batch_ordinal::text||
764
+ chr(31)||jsonb_array_length(requested_batch)::text||chr(31)||batch_fingerprint_value,
765
+ 'UTF8')),'hex');
766
+ for item in select value from jsonb_array_elements(requested_batch) loop
767
+ was_inserted := false;
768
+ if jsonb_typeof(item)<>'object'
769
+ or (select count(*) from jsonb_object_keys(item))<>20
770
+ or not item ?& array[
771
+ 'atribReceiptId','causationId','content','contentType','correlationId','createdAt',
772
+ 'data','deliveryPolicy','expiresAt','id','idempotencyKey','informedBy','metadata',
773
+ 'priority','project','replyToId','source','targets','threadId','type'
774
+ ]
775
+ or exists(select 1 from unnest(array[
776
+ 'id','source','type','content','contentType','priority','createdAt'
777
+ ]) key where jsonb_typeof(item->key)<>'string')
778
+ or exists(select 1 from unnest(array[
779
+ 'project','threadId','replyToId','correlationId','causationId','expiresAt',
780
+ 'idempotencyKey','atribReceiptId'
781
+ ]) key where jsonb_typeof(item->key) not in ('string','null'))
782
+ or jsonb_typeof(item->'targets')<>'array'
783
+ or jsonb_typeof(item->'informedBy')<>'array'
784
+ or jsonb_typeof(item->'deliveryPolicy')<>'object'
785
+ or exists(select 1 from jsonb_array_elements(item->'targets') value
786
+ where jsonb_typeof(value)<>'string')
787
+ or exists(select 1 from jsonb_array_elements(item->'informedBy') value
788
+ where jsonb_typeof(value)<>'string')
789
+ or item->>'createdAt' !~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{6}Z$'
790
+ or (item->>'expiresAt' is not null and item->>'expiresAt'
791
+ !~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{6}Z$')
792
+ or (item#>>'{deliveryPolicy,notBefore}' is not null
793
+ and item#>>'{deliveryPolicy,notBefore}'
794
+ !~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{6}Z$') then
795
+ raise exception 'archive message record is invalid';
796
+ end if;
797
+ if item->>'id' !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
798
+ or exists(select 1 from unnest(array['source','type','contentType']) key
799
+ where item->>key='' or item->>key<>btrim(item->>key) or char_length(item->>key)>128)
800
+ or item->>'content' is null or btrim(item->>'content')=''
801
+ or pg_catalog.octet_length(item->>'content')>65536
802
+ or item->>'priority' not in ('info','high','urgent')
803
+ or (item->>'project' is not null and (
804
+ item->>'project'='' or item->>'project'<>btrim(item->>'project')
805
+ or char_length(item->>'project')>128
806
+ ))
807
+ or exists(select 1 from unnest(array[
808
+ 'threadId','replyToId','correlationId','causationId'
809
+ ]) key where item->>key is not null and (
810
+ item->>key='' or item->>key<>btrim(item->>key) or char_length(item->>key)>128
811
+ ))
812
+ or (item->>'idempotencyKey' is not null and (
813
+ item->>'idempotencyKey'='' or item->>'idempotencyKey'<>btrim(item->>'idempotencyKey')
814
+ or char_length(item->>'idempotencyKey')>256
815
+ ))
816
+ or (item->>'atribReceiptId' is not null and item->>'atribReceiptId'
817
+ !~ '^[A-Za-z0-9_-]{43}[.][A-Za-z0-9_-]{43}$')
818
+ or pg_catalog.octet_length((item->'data')::text)>3670016
819
+ or pg_catalog.octet_length((item->'metadata')::text)>3670016
820
+ or agent_bridge.archive_json_depth(item->'data')>16
821
+ or agent_bridge.archive_json_depth(item->'metadata')>16
822
+ or jsonb_array_length(item->'targets')>64
823
+ or (select count(distinct value#>>'{}') from jsonb_array_elements(item->'targets') value)
824
+ <>jsonb_array_length(item->'targets')
825
+ or exists(select 1 from jsonb_array_elements(item->'targets') value where
826
+ value#>>'{}'='' or value#>>'{}'<>btrim(value#>>'{}') or char_length(value#>>'{}')>128)
827
+ or jsonb_array_length(item->'informedBy')>64
828
+ or (select count(distinct value#>>'{}') from jsonb_array_elements(item->'informedBy') value)
829
+ <>jsonb_array_length(item->'informedBy')
830
+ or exists(select 1 from jsonb_array_elements(item->'informedBy') value
831
+ where value#>>'{}' !~ '^sha256:[0-9a-f]{64}$') then
832
+ raise exception 'archive message violates the persisted domain contract';
833
+ end if;
834
+ message_id := (item->>'id')::uuid;
835
+ item_fingerprint := encode(sha256(convert_to(item::text,'UTF8')),'hex');
836
+ select * into prior_record from agent_bridge.archive_import_records record
837
+ where record.request_id=requested_request_id and record.record_kind='message'
838
+ and record.record_key=message_id::text;
839
+ if found then
840
+ if prior_record.semantic_fingerprint<>item_fingerprint then
841
+ raise exception 'archive message id was repeated with different content';
842
+ end if;
843
+ raise exception 'archive message id was repeated across batches';
844
+ end if;
845
+ created_at_value := (item->>'createdAt')::timestamptz;
846
+ expires_at_value := case when item->>'expiresAt' is null then null
847
+ else (item->>'expiresAt')::timestamptz end;
848
+ not_before_value := case when item#>>'{deliveryPolicy,notBefore}' is null then null
849
+ else (item#>>'{deliveryPolicy,notBefore}')::timestamptz end;
850
+ if to_char(created_at_value at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
851
+ <>item->>'createdAt'
852
+ or (expires_at_value is not null and to_char(expires_at_value at time zone 'UTC',
853
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')<>item->>'expiresAt')
854
+ or (not_before_value is not null and to_char(not_before_value at time zone 'UTC',
855
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')<>item#>>'{deliveryPolicy,notBefore}') then
856
+ raise exception 'archive message timestamp is not canonical';
857
+ end if;
858
+ delivery := item->'deliveryPolicy';
859
+ data_value := case when jsonb_typeof(item->'data')='null' then null else item->'data' end;
860
+ metadata_value := case when jsonb_typeof(item->'metadata')='null' then null else item->'metadata' end;
861
+ if delivery->>'mode' not in ('mailbox','leased')
862
+ or (delivery->>'mode'='mailbox' and delivery<>jsonb_build_object('mode','mailbox'))
863
+ or (not_before_value is not null and expires_at_value is not null
864
+ and not_before_value>=expires_at_value)
865
+ or (delivery->>'mode'='leased' and (
866
+ jsonb_array_length(item->'targets')=0
867
+ or not delivery ?& array[
868
+ 'mode','maxAttempts','retryBaseDelayMs','retryMaxDelayMs','retryJitterRatio'
869
+ ]
870
+ or jsonb_typeof(delivery->'maxAttempts')<>'number'
871
+ or jsonb_typeof(delivery->'retryBaseDelayMs')<>'number'
872
+ or jsonb_typeof(delivery->'retryMaxDelayMs')<>'number'
873
+ or jsonb_typeof(delivery->'retryJitterRatio')<>'number'
874
+ or exists(select 1 from jsonb_object_keys(delivery) key
875
+ where key not in ('mode','maxAttempts','retryBaseDelayMs','retryMaxDelayMs',
876
+ 'retryJitterRatio','notBefore'))
877
+ or (delivery ? 'notBefore' and jsonb_typeof(delivery->'notBefore')<>'string')
878
+ or (delivery->>'maxAttempts')::numeric<>trunc((delivery->>'maxAttempts')::numeric)
879
+ or (delivery->>'maxAttempts')::numeric not between 1 and 100
880
+ or (delivery->>'retryBaseDelayMs')::numeric<>
881
+ trunc((delivery->>'retryBaseDelayMs')::numeric)
882
+ or (delivery->>'retryBaseDelayMs')::numeric not between 1 and 3600000
883
+ or (delivery->>'retryMaxDelayMs')::numeric<>
884
+ trunc((delivery->>'retryMaxDelayMs')::numeric)
885
+ or (delivery->>'retryMaxDelayMs')::numeric<
886
+ (delivery->>'retryBaseDelayMs')::numeric
887
+ or (delivery->>'retryMaxDelayMs')::numeric>86400000
888
+ or (delivery->>'retryJitterRatio')::numeric not between 0 and 1
889
+ )) then
890
+ raise exception 'archive message delivery policy is invalid';
891
+ end if;
892
+ select * into existing from agent_bridge.messages where id=message_id;
893
+ if found then
894
+ if existing.workspace is distinct from requested_workspace
895
+ or existing.project is distinct from item->>'project'
896
+ or existing.source is distinct from item->>'source'
897
+ or existing.type is distinct from item->>'type'
898
+ or existing.content is distinct from item->>'content'
899
+ or existing.content_type is distinct from item->>'contentType'
900
+ or existing.data is distinct from data_value
901
+ or existing.targets is distinct from item->'targets'
902
+ or existing.thread_id is distinct from item->>'threadId'
903
+ or existing.reply_to_id is distinct from item->>'replyToId'
904
+ or existing.correlation_id is distinct from item->>'correlationId'
905
+ or existing.causation_id is distinct from item->>'causationId'
906
+ or existing.priority is distinct from item->>'priority'
907
+ or existing.expires_at is distinct from expires_at_value
908
+ or existing.idempotency_key is distinct from item->>'idempotencyKey'
909
+ or existing.atrib_receipt_id is distinct from item->>'atribReceiptId'
910
+ or coalesce(existing.informed_by,'[]'::jsonb) is distinct from item->'informedBy'
911
+ or existing.metadata is distinct from metadata_value
912
+ or existing.delivery_mode is distinct from delivery->>'mode'
913
+ or existing.delivery_max_attempts is distinct from (delivery->>'maxAttempts')::integer
914
+ or existing.delivery_retry_base_delay_ms is distinct from (delivery->>'retryBaseDelayMs')::integer
915
+ or existing.delivery_retry_max_delay_ms is distinct from (delivery->>'retryMaxDelayMs')::integer
916
+ or existing.delivery_retry_jitter_ratio is distinct from (delivery->>'retryJitterRatio')::double precision
917
+ or existing.delivery_not_before is distinct from not_before_value
918
+ or existing.created_at is distinct from created_at_value then
919
+ raise exception 'archive message conflicts with existing immutable content';
920
+ end if;
921
+ else
922
+ insert into agent_bridge.messages(
923
+ id,workspace,project,source,type,content,content_type,data,targets,thread_id,
924
+ reply_to_id,correlation_id,causation_id,priority,expires_at,idempotency_key,
925
+ atrib_receipt_id,informed_by,metadata,delivery_mode,delivery_max_attempts,
926
+ delivery_retry_base_delay_ms,delivery_retry_max_delay_ms,
927
+ delivery_retry_jitter_ratio,delivery_not_before,created_at
928
+ ) values (
929
+ message_id,requested_workspace,item->>'project',item->>'source',item->>'type',
930
+ item->>'content',item->>'contentType',data_value,item->'targets',item->>'threadId',
931
+ item->>'replyToId',item->>'correlationId',item->>'causationId',item->>'priority',
932
+ expires_at_value,item->>'idempotencyKey',item->>'atribReceiptId',item->'informedBy',
933
+ metadata_value,delivery->>'mode',(delivery->>'maxAttempts')::integer,
934
+ (delivery->>'retryBaseDelayMs')::integer,(delivery->>'retryMaxDelayMs')::integer,
935
+ (delivery->>'retryJitterRatio')::double precision,not_before_value,created_at_value
936
+ );
937
+ inserted_value := inserted_value+1;
938
+ was_inserted := true;
939
+ end if;
940
+ insert into agent_bridge.archive_import_records(
941
+ request_id,record_kind,record_key,batch_ordinal,semantic_fingerprint,inserted,actor
942
+ ) values (
943
+ requested_request_id,'message',message_id::text,requested_batch_ordinal,
944
+ item_fingerprint,was_inserted,session_user
945
+ );
946
+ processed_value := processed_value+1;
947
+ end loop;
948
+ insert into agent_bridge.archive_operation_batches(
949
+ request_id,record_kind,batch_ordinal,record_count,batch_fingerprint,chain_binding,actor
950
+ ) values (
951
+ requested_request_id,'message',requested_batch_ordinal,
952
+ jsonb_array_length(requested_batch),batch_fingerprint_value,chain_binding_value,session_user
953
+ );
954
+ return query select processed_value,inserted_value;
955
+ end
956
+ $$;
957
+
958
+ create or replace function agent_bridge.archive_import_receipts(
959
+ requested_request_id uuid, requested_workspace text, requested_digest character(64),
960
+ requested_batch_ordinal bigint, requested_batch jsonb
961
+ ) returns table(processed bigint,inserted bigint)
962
+ language plpgsql security definer set search_path = '' set timezone = 'UTC' as $$
963
+ declare
964
+ item jsonb;
965
+ message_id_value uuid;
966
+ read_at_value timestamptz;
967
+ existing_read_at timestamptz;
968
+ prior_batch agent_bridge.archive_operation_batches%rowtype;
969
+ prior_record agent_bridge.archive_import_records%rowtype;
970
+ batch_fingerprint_value text;
971
+ previous_chain_binding text;
972
+ chain_binding_value text;
973
+ item_fingerprint text;
974
+ record_key_value text;
975
+ was_inserted boolean;
976
+ processed_value bigint := 0;
977
+ inserted_value bigint := 0;
978
+ begin
979
+ perform agent_bridge.archive_assert_transaction(
980
+ requested_request_id,'import',requested_workspace,requested_digest
981
+ );
982
+ if requested_batch is null or jsonb_typeof(requested_batch)<>'array'
983
+ or requested_batch_ordinal is null or requested_batch_ordinal<0
984
+ or jsonb_array_length(requested_batch)=0
985
+ or jsonb_array_length(requested_batch)>1000
986
+ or pg_catalog.pg_column_size(requested_batch)>4194304 then
987
+ raise exception 'archive receipt batch is invalid';
988
+ end if;
989
+ if (select count(*) from agent_bridge.archive_import_records record
990
+ where record.request_id=requested_request_id and record.record_kind='message')
991
+ is distinct from (select operation.message_count from agent_bridge.archive_operations operation
992
+ where operation.request_id=requested_request_id and operation.phase='begin') then
993
+ raise exception 'archive receipts require the complete unique message pass';
994
+ end if;
995
+ batch_fingerprint_value := encode(sha256(convert_to(requested_batch::text,'UTF8')),'hex');
996
+ select * into prior_batch from agent_bridge.archive_operation_batches
997
+ where request_id=requested_request_id and record_kind='receipt'
998
+ and batch_ordinal=requested_batch_ordinal;
999
+ if found then
1000
+ if prior_batch.batch_fingerprint<>batch_fingerprint_value
1001
+ or prior_batch.record_count<>jsonb_array_length(requested_batch)
1002
+ or prior_batch.actor<>session_user then
1003
+ raise exception 'archive receipt batch ordinal was already used with different content';
1004
+ end if;
1005
+ return query select prior_batch.record_count,
1006
+ coalesce((select count(*) from agent_bridge.archive_import_records record
1007
+ where record.request_id=requested_request_id and record.record_kind='receipt'
1008
+ and record.batch_ordinal=requested_batch_ordinal and record.inserted),0)::bigint;
1009
+ return;
1010
+ end if;
1011
+ if requested_batch_ordinal<>coalesce((select max(batch.batch_ordinal)+1
1012
+ from agent_bridge.archive_operation_batches batch
1013
+ where batch.request_id=requested_request_id and batch.record_kind='receipt'),0) then
1014
+ raise exception 'archive receipt batches must be contiguous';
1015
+ end if;
1016
+ if requested_batch_ordinal=0 then
1017
+ select batch.chain_binding into previous_chain_binding
1018
+ from agent_bridge.archive_operation_batches batch
1019
+ where batch.request_id=requested_request_id and batch.record_kind='message'
1020
+ order by batch.batch_ordinal desc limit 1;
1021
+ previous_chain_binding := coalesce(previous_chain_binding,
1022
+ encode(sha256(convert_to('','UTF8')),'hex'));
1023
+ else
1024
+ select batch.chain_binding into previous_chain_binding
1025
+ from agent_bridge.archive_operation_batches batch
1026
+ where batch.request_id=requested_request_id and batch.record_kind='receipt'
1027
+ and batch.batch_ordinal=requested_batch_ordinal-1;
1028
+ end if;
1029
+ chain_binding_value := encode(sha256(convert_to(
1030
+ previous_chain_binding||chr(31)||'receipt'||chr(31)||requested_batch_ordinal::text||
1031
+ chr(31)||jsonb_array_length(requested_batch)::text||chr(31)||batch_fingerprint_value,
1032
+ 'UTF8')),'hex');
1033
+ for item in select value from jsonb_array_elements(requested_batch) loop
1034
+ was_inserted := false;
1035
+ if jsonb_typeof(item)<>'object'
1036
+ or (select count(*) from jsonb_object_keys(item))<>3
1037
+ or not item ?& array['messageId','principal','readAt']
1038
+ or jsonb_typeof(item->'messageId')<>'string'
1039
+ or jsonb_typeof(item->'principal')<>'string'
1040
+ or jsonb_typeof(item->'readAt')<>'string'
1041
+ or item->>'principal'=''
1042
+ or item->>'principal'<>btrim(item->>'principal')
1043
+ or char_length(item->>'principal')>128
1044
+ or item->>'messageId' !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
1045
+ or item->>'readAt' is null
1046
+ or item->>'readAt' !~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{6}Z$' then
1047
+ raise exception 'archive receipt record is invalid';
1048
+ end if;
1049
+ message_id_value := (item->>'messageId')::uuid;
1050
+ read_at_value := (item->>'readAt')::timestamptz;
1051
+ if to_char(read_at_value at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
1052
+ <>item->>'readAt' then
1053
+ raise exception 'archive receipt timestamp is not canonical';
1054
+ end if;
1055
+ record_key_value := message_id_value::text||chr(31)||(item->>'principal');
1056
+ item_fingerprint := encode(sha256(convert_to(item::text,'UTF8')),'hex');
1057
+ select * into prior_record from agent_bridge.archive_import_records record
1058
+ where record.request_id=requested_request_id and record.record_kind='receipt'
1059
+ and record.record_key=record_key_value;
1060
+ if found then
1061
+ if prior_record.semantic_fingerprint<>item_fingerprint then
1062
+ raise exception 'archive receipt key was repeated with different content';
1063
+ end if;
1064
+ raise exception 'archive receipt key was repeated across batches';
1065
+ end if;
1066
+ if not exists(select 1 from agent_bridge.messages message
1067
+ where message.workspace=requested_workspace and message.id=message_id_value
1068
+ and (message.targets='[]'::jsonb or message.targets ? (item->>'principal'))) then
1069
+ raise exception 'archive receipt principal is not eligible to read the destination message';
1070
+ end if;
1071
+ select receipt.read_at into existing_read_at from agent_bridge.receipts receipt
1072
+ where receipt.workspace=requested_workspace and receipt.message_id=message_id_value
1073
+ and receipt.principal=item->>'principal';
1074
+ if found then
1075
+ if existing_read_at is distinct from read_at_value then
1076
+ raise exception 'archive receipt conflicts with existing content';
1077
+ end if;
1078
+ else
1079
+ insert into agent_bridge.receipts(workspace,message_id,principal,read_at)
1080
+ values(requested_workspace,message_id_value,item->>'principal',read_at_value);
1081
+ inserted_value := inserted_value+1;
1082
+ was_inserted := true;
1083
+ end if;
1084
+ insert into agent_bridge.archive_import_records(
1085
+ request_id,record_kind,record_key,batch_ordinal,semantic_fingerprint,inserted,actor
1086
+ ) values (
1087
+ requested_request_id,'receipt',record_key_value,requested_batch_ordinal,
1088
+ item_fingerprint,was_inserted,session_user
1089
+ );
1090
+ processed_value := processed_value+1;
1091
+ end loop;
1092
+ insert into agent_bridge.archive_operation_batches(
1093
+ request_id,record_kind,batch_ordinal,record_count,batch_fingerprint,chain_binding,actor
1094
+ ) values (
1095
+ requested_request_id,'receipt',requested_batch_ordinal,
1096
+ jsonb_array_length(requested_batch),batch_fingerprint_value,chain_binding_value,session_user
1097
+ );
1098
+ return query select processed_value,inserted_value;
1099
+ end
1100
+ $$;
1101
+
1102
+ create or replace function agent_bridge.archive_complete_export(
1103
+ requested_request_id uuid, requested_workspace text, requested_digest character(64),
1104
+ requested_message_count bigint, requested_receipt_count bigint,
1105
+ requested_published_at timestamptz, requested_completion_kind text
1106
+ ) returns table(replayed boolean)
1107
+ language plpgsql security definer set search_path = '' as $$
1108
+ declare
1109
+ binding agent_bridge.archive_operations%rowtype;
1110
+ prior agent_bridge.archive_operations%rowtype;
1111
+ content_binding_value text;
1112
+ begin
1113
+ perform agent_bridge.archive_assert_transaction(
1114
+ requested_request_id,'export',requested_workspace,null
1115
+ );
1116
+ if requested_digest is null or requested_digest!~'^[0-9a-f]{64}$'
1117
+ or requested_message_count is null or requested_message_count<0
1118
+ or requested_receipt_count is null or requested_receipt_count<0
1119
+ or requested_published_at is null
1120
+ or requested_completion_kind<>'published'
1121
+ or not exists(select 1 from agent_bridge.archive_transaction_authorizations transaction_auth
1122
+ where transaction_auth.backend_pid=pg_catalog.pg_backend_pid()
1123
+ and transaction_auth.transaction_id=pg_catalog.pg_current_xact_id()::text
1124
+ and transaction_auth.request_id=requested_request_id and transaction_auth.actor=session_user
1125
+ and transaction_auth.messages_consumed and transaction_auth.receipts_consumed) then
1126
+ raise exception 'archive completion counts are invalid';
1127
+ end if;
1128
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
1129
+ 'portable-archive'||chr(31)||requested_workspace,1646705663
1130
+ ));
1131
+ perform pg_catalog.pg_advisory_xact_lock(
1132
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
1133
+ );
1134
+ select * into binding from agent_bridge.archive_operations
1135
+ where request_id=requested_request_id and phase='begin';
1136
+ if not found or binding.workspace<>requested_workspace
1137
+ or binding.operation<>'export' or binding.actor<>session_user then
1138
+ raise exception 'archive operation binding does not match the active request';
1139
+ end if;
1140
+ content_binding_value := encode(sha256(convert_to(
1141
+ requested_digest||chr(31)||requested_message_count::text||chr(31)||
1142
+ requested_receipt_count::text||chr(31)||to_char(
1143
+ requested_published_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
1144
+ )||chr(31)||requested_completion_kind,'UTF8')),'hex');
1145
+ select * into prior from agent_bridge.archive_operations
1146
+ where request_id=requested_request_id and phase='complete';
1147
+ if found then
1148
+ if prior.operation<>'export' or prior.workspace<>requested_workspace
1149
+ or prior.client_verified_digest<>requested_digest or prior.message_count<>requested_message_count
1150
+ or prior.receipt_count<>requested_receipt_count or prior.apply<>true
1151
+ or prior.outcome<>'published'
1152
+ or prior.server_content_binding<>content_binding_value
1153
+ or prior.published_at is distinct from requested_published_at
1154
+ or prior.actor<>session_user then
1155
+ raise exception 'request id was already completed with different content';
1156
+ end if;
1157
+ return query select true;
1158
+ return;
1159
+ end if;
1160
+ insert into agent_bridge.archive_operations(
1161
+ request_id,phase,operation,workspace,client_verified_digest,message_count,receipt_count,
1162
+ message_inserted_count,receipt_inserted_count,server_content_binding,
1163
+ outcome,published_at,apply,actor
1164
+ ) values (
1165
+ requested_request_id,'complete','export',requested_workspace,requested_digest,
1166
+ requested_message_count,requested_receipt_count,0,0,content_binding_value,
1167
+ 'published',requested_published_at,true,session_user
1168
+ );
1169
+ return query select false;
1170
+ end
1171
+ $$;
1172
+
1173
+ create or replace function agent_bridge.archive_reconcile_export(
1174
+ requested_request_id uuid, requested_workspace text, requested_digest character(64),
1175
+ requested_message_count bigint, requested_receipt_count bigint,
1176
+ requested_published_at timestamptz
1177
+ ) returns table(replayed boolean)
1178
+ language plpgsql security definer set search_path = '' as $$
1179
+ declare
1180
+ binding agent_bridge.archive_operations%rowtype;
1181
+ prior agent_bridge.archive_operations%rowtype;
1182
+ content_binding_value text;
1183
+ begin
1184
+ perform agent_bridge.assert_archive_actor();
1185
+ if requested_digest is null or requested_digest!~'^[0-9a-f]{64}$'
1186
+ or requested_message_count is null or requested_message_count<0
1187
+ or requested_receipt_count is null or requested_receipt_count<0
1188
+ or requested_published_at is null then
1189
+ raise exception 'archive reconciliation metadata is invalid';
1190
+ end if;
1191
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
1192
+ 'portable-archive'||chr(31)||requested_workspace,1646705663
1193
+ ));
1194
+ perform pg_catalog.pg_advisory_xact_lock(
1195
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
1196
+ );
1197
+ select * into binding from agent_bridge.archive_operations
1198
+ where request_id=requested_request_id and phase='begin';
1199
+ if not found or binding.workspace<>requested_workspace
1200
+ or binding.operation<>'export' or binding.actor<>session_user then
1201
+ raise exception 'archive operation binding does not match the active request';
1202
+ end if;
1203
+ content_binding_value := encode(sha256(convert_to(
1204
+ requested_digest||chr(31)||requested_message_count::text||chr(31)||
1205
+ requested_receipt_count::text||chr(31)||to_char(
1206
+ requested_published_at at time zone 'UTC','YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
1207
+ )||chr(31)||'reconciled','UTF8')),'hex');
1208
+ select * into prior from agent_bridge.archive_operations
1209
+ where request_id=requested_request_id and phase='complete';
1210
+ if found then
1211
+ if prior.operation<>'export' or prior.workspace<>requested_workspace
1212
+ or prior.client_verified_digest<>requested_digest or prior.message_count<>requested_message_count
1213
+ or prior.receipt_count<>requested_receipt_count or prior.apply<>true
1214
+ or prior.outcome<>'client_reconciled' or prior.server_content_binding<>content_binding_value
1215
+ or prior.published_at is distinct from requested_published_at
1216
+ or prior.actor<>session_user then
1217
+ raise exception 'request id was already completed with different content';
1218
+ end if;
1219
+ return query select true;
1220
+ return;
1221
+ end if;
1222
+ insert into agent_bridge.archive_operations(
1223
+ request_id,phase,operation,workspace,client_verified_digest,message_count,receipt_count,
1224
+ message_inserted_count,receipt_inserted_count,server_content_binding,
1225
+ outcome,published_at,apply,actor
1226
+ ) values (
1227
+ requested_request_id,'complete','export',requested_workspace,requested_digest,
1228
+ requested_message_count,requested_receipt_count,0,0,content_binding_value,
1229
+ 'client_reconciled',requested_published_at,true,session_user
1230
+ );
1231
+ return query select false;
1232
+ end
1233
+ $$;
1234
+
1235
+ create or replace function agent_bridge.archive_complete_import(
1236
+ requested_request_id uuid, requested_workspace text, requested_apply boolean
1237
+ ) returns table(
1238
+ replayed boolean, message_count bigint, receipt_count bigint,
1239
+ message_inserted_count bigint, receipt_inserted_count bigint
1240
+ )
1241
+ language plpgsql security definer set search_path = '' as $$
1242
+ declare
1243
+ binding agent_bridge.archive_operations%rowtype;
1244
+ prior agent_bridge.archive_operations%rowtype;
1245
+ actual_message_count bigint;
1246
+ actual_receipt_count bigint;
1247
+ actual_message_inserted bigint;
1248
+ actual_receipt_inserted bigint;
1249
+ content_binding_value text;
1250
+ begin
1251
+ if requested_apply is null then raise exception 'archive completion mode is invalid'; end if;
1252
+ select * into binding from agent_bridge.archive_operations
1253
+ where request_id=requested_request_id and phase='begin';
1254
+ if not found or binding.workspace<>requested_workspace or binding.operation<>'import'
1255
+ or binding.actor<>session_user then
1256
+ raise exception 'archive operation binding does not match the active request';
1257
+ end if;
1258
+ perform agent_bridge.archive_assert_transaction(
1259
+ requested_request_id,'import',requested_workspace,binding.client_verified_digest
1260
+ );
1261
+ select * into prior from agent_bridge.archive_operations
1262
+ where request_id=requested_request_id and phase='complete';
1263
+ if found then
1264
+ if prior.outcome='abandoned' then raise exception 'archive operation was abandoned'; end if;
1265
+ if prior.operation<>'import' or prior.workspace<>requested_workspace
1266
+ or prior.apply<>requested_apply or prior.actor<>session_user then
1267
+ raise exception 'request id was already completed with different content';
1268
+ end if;
1269
+ return query select true,prior.message_count,prior.receipt_count,
1270
+ prior.message_inserted_count,prior.receipt_inserted_count;
1271
+ return;
1272
+ end if;
1273
+ select count(*)::bigint,count(*) filter(where record.inserted)::bigint
1274
+ into actual_message_count,actual_message_inserted
1275
+ from agent_bridge.archive_import_records record
1276
+ where record.request_id=requested_request_id and record.record_kind='message';
1277
+ select count(*)::bigint,count(*) filter(where record.inserted)::bigint
1278
+ into actual_receipt_count,actual_receipt_inserted
1279
+ from agent_bridge.archive_import_records record
1280
+ where record.request_id=requested_request_id and record.record_kind='receipt';
1281
+ if actual_message_count<>binding.message_count or actual_receipt_count<>binding.receipt_count then
1282
+ raise exception 'archive import record counts do not match the bound footer';
1283
+ end if;
1284
+ select batch.chain_binding into content_binding_value
1285
+ from agent_bridge.archive_operation_batches batch
1286
+ where batch.request_id=requested_request_id
1287
+ order by batch.sequence desc limit 1;
1288
+ content_binding_value := encode(sha256(convert_to(
1289
+ coalesce(content_binding_value,encode(sha256(convert_to('','UTF8')),'hex'))||chr(31)||
1290
+ actual_message_count::text||chr(31)||actual_receipt_count::text||chr(31)||
1291
+ actual_message_inserted::text||chr(31)||actual_receipt_inserted::text,
1292
+ 'UTF8')),'hex');
1293
+ insert into agent_bridge.archive_operations(
1294
+ request_id,phase,operation,workspace,client_verified_digest,message_count,receipt_count,
1295
+ message_inserted_count,receipt_inserted_count,server_content_binding,
1296
+ outcome,apply,actor
1297
+ ) values (
1298
+ requested_request_id,'complete','import',requested_workspace,binding.client_verified_digest,
1299
+ actual_message_count,actual_receipt_count,actual_message_inserted,actual_receipt_inserted,
1300
+ content_binding_value,case when requested_apply then 'applied' else 'dry-run' end,
1301
+ requested_apply,session_user
1302
+ );
1303
+ perform agent_bridge.archive_close_transaction_authorization(requested_request_id);
1304
+ return query select false,actual_message_count,actual_receipt_count,
1305
+ actual_message_inserted,actual_receipt_inserted;
1306
+ end
1307
+ $$;
1308
+
1309
+ create or replace function agent_bridge.archive_abandon_operation(
1310
+ requested_request_id uuid, requested_workspace text, requested_failure_code text
1311
+ ) returns table(replayed boolean)
1312
+ language plpgsql security definer set search_path = '' as $$
1313
+ declare
1314
+ binding agent_bridge.archive_operations%rowtype;
1315
+ terminal agent_bridge.archive_operations%rowtype;
1316
+ schema_owner name;
1317
+ begin
1318
+ perform agent_bridge.assert_archive_actor();
1319
+ if requested_failure_code is null
1320
+ or requested_failure_code!~'^[a-z][a-z0-9_]{0,63}$' then
1321
+ raise exception 'archive abandonment code is invalid';
1322
+ end if;
1323
+ perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
1324
+ 'portable-archive'||chr(31)||requested_workspace,1646705663
1325
+ ));
1326
+ perform pg_catalog.pg_advisory_xact_lock(
1327
+ pg_catalog.hashtextextended(requested_request_id::text,1646705660)
1328
+ );
1329
+ select pg_catalog.pg_get_userbyid(namespace.nspowner)::name into schema_owner
1330
+ from pg_catalog.pg_namespace namespace where namespace.nspname='agent_bridge';
1331
+ select * into binding from agent_bridge.archive_operations
1332
+ where request_id=requested_request_id and phase='begin';
1333
+ if not found or binding.workspace<>requested_workspace or binding.operation<>'export'
1334
+ or (binding.actor<>session_user and not (
1335
+ session_user=schema_owner and requested_failure_code='owner_reconciled'
1336
+ )) or (requested_failure_code='owner_reconciled' and session_user<>schema_owner) then
1337
+ raise exception 'archive operation binding does not match the active request';
1338
+ end if;
1339
+ select * into terminal from agent_bridge.archive_operations
1340
+ where request_id=requested_request_id and phase='complete';
1341
+ if found then
1342
+ if terminal.outcome<>'abandoned' or terminal.failure_code<>requested_failure_code then
1343
+ raise exception 'archive operation is already final';
1344
+ end if;
1345
+ return query select true;
1346
+ return;
1347
+ end if;
1348
+ insert into agent_bridge.archive_operations(
1349
+ request_id,phase,operation,workspace,client_verified_digest,message_count,receipt_count,
1350
+ message_inserted_count,receipt_inserted_count,server_content_binding,
1351
+ outcome,failure_code,apply,actor
1352
+ ) values (
1353
+ requested_request_id,'complete','export',requested_workspace,null,0,0,0,0,
1354
+ encode(sha256(convert_to('','UTF8')),'hex'),
1355
+ 'abandoned',requested_failure_code,false,session_user
1356
+ );
1357
+ return query select false;
1358
+ end
1359
+ $$;
1360
+
1361
+ create or replace function agent_bridge.portable_archive_catalog_definition()
1362
+ returns text language sql stable set search_path = '' set timezone = 'UTC' as $$
1363
+ with names as (select
1364
+ ('agent_bridge_archive_operator_'||substr(md5(current_database()),1,16))::name archive_role,
1365
+ namespace.oid schema_oid,namespace.nspowner schema_owner
1366
+ from pg_catalog.pg_namespace namespace where namespace.nspname='agent_bridge'
1367
+ ), protected_functions(function_name) as (values
1368
+ ('reject_archive_ledger_mutation'),('assert_archive_actor'),('register_archive_member'),
1369
+ ('revoke_archive_member'),('archive_assert_operation'),('archive_authorize_transaction'),
1370
+ ('archive_assert_transaction'),('archive_close_transaction_authorization'),('archive_begin_operation'),
1371
+ ('archive_export_messages'),('archive_export_receipts'),('archive_json_depth'),('archive_import_messages'),
1372
+ ('archive_import_receipts'),('archive_complete_export'),('archive_reconcile_export'),
1373
+ ('archive_complete_import'),
1374
+ ('archive_abandon_operation'),('portable_archive_catalog_definition'),
1375
+ ('portable_archive_ready')
1376
+ ), protected_tables(oid) as (
1377
+ select relation.oid from pg_catalog.pg_class relation
1378
+ where relation.relnamespace=(select schema_oid from names) and relation.relname in (
1379
+ 'archive_membership_events','archive_operations','archive_operation_batches',
1380
+ 'archive_import_records','archive_transaction_authorizations','portable_archive_attestations'
1381
+ )
1382
+ ), protected_relations(oid) as (
1383
+ select oid from protected_tables
1384
+ union
1385
+ select index_record.indexrelid from pg_catalog.pg_index index_record
1386
+ where index_record.indrelid in (select oid from protected_tables)
1387
+ union
1388
+ select dependency.objid from pg_catalog.pg_depend dependency
1389
+ join pg_catalog.pg_class sequence_relation on sequence_relation.oid=dependency.objid
1390
+ where dependency.refobjid in (select oid from protected_tables)
1391
+ and sequence_relation.relkind='S'
1392
+ ), catalog_objects(kind,identity,definition) as (
1393
+ select 'archive_role',role.rolname,concat_ws(':',role.rolsuper,role.rolinherit,
1394
+ role.rolcreaterole,role.rolcreatedb,role.rolcanlogin,role.rolreplication,
1395
+ role.rolbypassrls,role.rolconnlimit)
1396
+ from pg_catalog.pg_roles role where role.rolname=(select archive_role from names)
1397
+ union all
1398
+ select 'owner_membership',granted.rolname||'->'||member.rolname,
1399
+ grantor.rolname||':'||membership.admin_option::text||':'||
1400
+ coalesce(to_jsonb(membership)->>'inherit_option','')||':'||
1401
+ coalesce(to_jsonb(membership)->>'set_option','')
1402
+ from pg_catalog.pg_auth_members membership
1403
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
1404
+ join pg_catalog.pg_roles member on member.oid=membership.member
1405
+ join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
1406
+ where granted.rolname=(select archive_role from names)
1407
+ and member.oid=(select schema_owner from names)
1408
+ union all
1409
+ select 'schema','agent_bridge',pg_catalog.pg_get_userbyid(namespace.nspowner)
1410
+ from pg_catalog.pg_namespace namespace where namespace.oid=(select schema_oid from names)
1411
+ union all
1412
+ select 'schema_acl','agent_bridge',pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
1413
+ case when privilege.grantee=0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(privilege.grantee) end
1414
+ ||':'||privilege.privilege_type||':'||privilege.is_grantable::text
1415
+ from pg_catalog.pg_namespace namespace
1416
+ cross join lateral pg_catalog.aclexplode(coalesce(namespace.nspacl,
1417
+ pg_catalog.acldefault('n',namespace.nspowner))) privilege
1418
+ where namespace.oid=(select schema_oid from names)
1419
+ union all
1420
+ select 'default_acl',default_acl.oid::text,
1421
+ pg_catalog.pg_get_userbyid(default_acl.defaclrole)||':'||default_acl.defaclobjtype::text||':'||
1422
+ coalesce(default_acl.defaclnamespace::regnamespace::text,'global')||':'||
1423
+ pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
1424
+ case when privilege.grantee=0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(privilege.grantee) end
1425
+ ||':'||privilege.privilege_type||':'||privilege.is_grantable::text
1426
+ from pg_catalog.pg_default_acl default_acl
1427
+ cross join lateral pg_catalog.aclexplode(default_acl.defaclacl) privilege
1428
+ where default_acl.defaclnamespace=(select schema_oid from names)
1429
+ or default_acl.defaclrole=(select schema_owner from names)
1430
+ union all
1431
+ select 'relation',relation.oid::regclass::text,
1432
+ pg_catalog.pg_get_userbyid(relation.relowner)||':'||relation.relkind::text||':'||
1433
+ relation.relpersistence::text||':'||relation.relrowsecurity::text||':'||
1434
+ relation.relforcerowsecurity::text||':'||relation.relreplident::text
1435
+ from pg_catalog.pg_class relation where relation.oid in (select oid from protected_relations)
1436
+ union all
1437
+ select 'relation_acl',relation.oid::regclass::text,
1438
+ pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
1439
+ case when privilege.grantee=0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(privilege.grantee) end
1440
+ ||':'||privilege.privilege_type||':'||privilege.is_grantable::text
1441
+ from pg_catalog.pg_class relation
1442
+ cross join lateral pg_catalog.aclexplode(coalesce(relation.relacl,
1443
+ pg_catalog.acldefault(case when relation.relkind='S' then 'S'::"char" else 'r'::"char" end,
1444
+ relation.relowner))) privilege
1445
+ where relation.oid in (select oid from protected_relations)
1446
+ and relation.relkind in ('r','p','v','m','f','S')
1447
+ union all
1448
+ select 'column',relation.oid::regclass::text||'.'||attribute.attname,
1449
+ attribute.attnum||':'||attribute.atttypid::regtype::text||':'||attribute.atttypmod||':'||
1450
+ attribute.attnotnull::text||':'||attribute.attidentity::text||':'||attribute.attgenerated::text||':'||
1451
+ attribute.attstorage::text||':'||attribute.attcompression::text||':'||attribute.attcollation||':'||
1452
+ coalesce(pg_catalog.pg_get_expr(default_value.adbin,default_value.adrelid),'')
1453
+ from pg_catalog.pg_attribute attribute
1454
+ join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
1455
+ left join pg_catalog.pg_attrdef default_value on default_value.adrelid=attribute.attrelid
1456
+ and default_value.adnum=attribute.attnum
1457
+ where relation.oid in (select oid from protected_tables)
1458
+ and attribute.attnum>0 and not attribute.attisdropped
1459
+ union all
1460
+ select 'column_acl',relation.oid::regclass::text||'.'||attribute.attname,
1461
+ pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
1462
+ case when privilege.grantee=0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(privilege.grantee) end
1463
+ ||':'||privilege.privilege_type||':'||privilege.is_grantable::text
1464
+ from pg_catalog.pg_attribute attribute join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
1465
+ cross join lateral pg_catalog.aclexplode(attribute.attacl) privilege
1466
+ where relation.oid in (select oid from protected_tables)
1467
+ and attribute.attnum>0 and not attribute.attisdropped and attribute.attacl is not null
1468
+ union all
1469
+ select 'constraint',constraint_record.conrelid::regclass::text||'.'||constraint_record.conname,
1470
+ constraint_record.contype::text||':'||constraint_record.condeferrable::text||':'||
1471
+ constraint_record.condeferred::text||':'||constraint_record.convalidated::text||':'||
1472
+ pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
1473
+ from pg_catalog.pg_constraint constraint_record
1474
+ where constraint_record.conrelid in (select oid from protected_tables)
1475
+ union all
1476
+ select 'index',index_record.indexrelid::regclass::text,
1477
+ pg_catalog.pg_get_userbyid(index_relation.relowner)||':'||index_relation.relkind::text||':'||
1478
+ index_record.indisunique::text||':'||index_record.indisprimary::text||':'||
1479
+ index_record.indisvalid::text||':'||index_record.indisready::text||':'||
1480
+ index_record.indislive::text||':'||access_method.amname||':'||
1481
+ pg_catalog.pg_get_indexdef(index_record.indexrelid)
1482
+ from pg_catalog.pg_index index_record
1483
+ join pg_catalog.pg_class table_relation on table_relation.oid=index_record.indrelid
1484
+ join pg_catalog.pg_class index_relation on index_relation.oid=index_record.indexrelid
1485
+ join pg_catalog.pg_am access_method on access_method.oid=index_relation.relam
1486
+ where table_relation.oid in (select oid from protected_tables)
1487
+ union all
1488
+ select 'trigger',trigger.tgrelid::regclass::text||'.'||trigger.tgname,
1489
+ trigger.tgenabled::text||':'||pg_catalog.pg_get_userbyid(procedure.proowner)||':'||
1490
+ pg_catalog.pg_get_triggerdef(trigger.oid,true)
1491
+ from pg_catalog.pg_trigger trigger join pg_catalog.pg_class relation on relation.oid=trigger.tgrelid
1492
+ join pg_catalog.pg_proc procedure on procedure.oid=trigger.tgfoid
1493
+ where not trigger.tgisinternal and relation.oid in (select oid from protected_tables)
1494
+ union all
1495
+ select 'policy',policy.polrelid::regclass::text||'.'||policy.polname,
1496
+ policy.polcmd::text||':'||policy.polpermissive::text||':'||policy.polroles::text||':'||
1497
+ coalesce(pg_catalog.pg_get_expr(policy.polqual,policy.polrelid),'')||':'||
1498
+ coalesce(pg_catalog.pg_get_expr(policy.polwithcheck,policy.polrelid),'')
1499
+ from pg_catalog.pg_policy policy join pg_catalog.pg_class relation on relation.oid=policy.polrelid
1500
+ where relation.oid in (select oid from protected_tables)
1501
+ union all
1502
+ select 'function',procedure.oid::regprocedure::text,
1503
+ pg_catalog.pg_get_userbyid(procedure.proowner)||':'||procedure.prokind::text||':'||
1504
+ procedure.prosecdef::text||':'||procedure.proleakproof::text||':'||procedure.provolatile::text||':'||
1505
+ procedure.proparallel::text||':'||procedure.proisstrict::text||':'||
1506
+ coalesce(array_to_string(procedure.proconfig,','),'')||':'||
1507
+ pg_catalog.pg_get_functiondef(procedure.oid)
1508
+ from pg_catalog.pg_proc procedure where procedure.pronamespace=(select schema_oid from names)
1509
+ and procedure.proname in (select function_name from protected_functions)
1510
+ union all
1511
+ select 'function_acl',procedure.oid::regprocedure::text,
1512
+ pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
1513
+ case when privilege.grantee=0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(privilege.grantee) end
1514
+ ||':'||privilege.privilege_type||':'||privilege.is_grantable::text
1515
+ from pg_catalog.pg_proc procedure
1516
+ cross join lateral pg_catalog.aclexplode(coalesce(procedure.proacl,
1517
+ pg_catalog.acldefault('f',procedure.proowner))) privilege
1518
+ where procedure.pronamespace=(select schema_oid from names)
1519
+ and procedure.proname in (select function_name from protected_functions)
1520
+ union all
1521
+ select 'sequence',sequence_record.seqrelid::regclass::text,
1522
+ sequence_record.seqtypid::regtype::text||':'||sequence_record.seqstart||':'||
1523
+ sequence_record.seqincrement||':'||sequence_record.seqmax||':'||sequence_record.seqmin||':'||
1524
+ sequence_record.seqcache||':'||sequence_record.seqcycle::text
1525
+ from pg_catalog.pg_sequence sequence_record join pg_catalog.pg_class relation
1526
+ on relation.oid=sequence_record.seqrelid
1527
+ where relation.oid in (select oid from protected_relations)
1528
+ )
1529
+ select string_agg(kind||E'\x1f'||identity||E'\x1f'||definition,E'\x1e'
1530
+ order by kind,identity,definition) from catalog_objects
1531
+ $$;
1532
+
1533
+ create or replace function agent_bridge.portable_archive_ready()
1534
+ returns boolean language sql stable security definer set search_path = '' as $$
1535
+ with names as (select
1536
+ ('agent_bridge_archive_operator_'||substr(md5(current_database()),1,16))::name archive_role,
1537
+ (select pg_catalog.pg_get_userbyid(nspowner)::name from pg_catalog.pg_namespace
1538
+ where nspname='agent_bridge') schema_owner
1539
+ ), latest_registry as (
1540
+ select distinct on (event.member_role) event.member_role,event.action
1541
+ from agent_bridge.archive_membership_events event order by event.member_role,event.sequence desc
1542
+ ), active_registry as (
1543
+ select member_role from latest_registry where action='register'
1544
+ ), expected_memberships as (
1545
+ select names.archive_role granted_role,names.schema_owner member_role,true admin_option from names
1546
+ union all select names.archive_role,registry.member_role,false from active_registry registry cross join names
1547
+ ), actual_memberships as (
1548
+ select granted.rolname::name granted_role,member.rolname::name member_role,
1549
+ membership.admin_option,
1550
+ coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
1551
+ coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
1552
+ grantor.rolname::name grantor_role
1553
+ from pg_catalog.pg_auth_members membership
1554
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
1555
+ join pg_catalog.pg_roles member on member.oid=membership.member
1556
+ join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
1557
+ where granted.rolname=(select archive_role from names)
1558
+ )
1559
+ select current_setting('server_version_num')::integer/10000=any(array[15,16,17,18])
1560
+ and exists(select 1 from pg_catalog.pg_roles role,names where role.rolname=names.archive_role
1561
+ and not role.rolcanlogin and not role.rolsuper and not role.rolcreatedb
1562
+ and not role.rolcreaterole and not role.rolreplication and not role.rolbypassrls)
1563
+ and (select count(*)=1 and bool_and(
1564
+ attestation.catalog_definition=agent_bridge.portable_archive_catalog_definition()
1565
+ ) from agent_bridge.portable_archive_attestations attestation
1566
+ where attestation.name='portable-archive-v1')
1567
+ and not exists(
1568
+ (select granted_role,member_role,admin_option from actual_memberships
1569
+ except select granted_role,member_role,admin_option from expected_memberships)
1570
+ union all
1571
+ (select granted_role,member_role,admin_option from expected_memberships
1572
+ except select granted_role,member_role,admin_option from actual_memberships)
1573
+ )
1574
+ and not exists(select 1 from actual_memberships membership,names
1575
+ where not membership.inherit_option or not membership.set_option
1576
+ or membership.grantor_role<>names.schema_owner)
1577
+ and not exists(select 1 from active_registry registry
1578
+ left join pg_catalog.pg_roles role on role.rolname=registry.member_role
1579
+ where role.oid is null or not role.rolcanlogin or role.rolsuper or role.rolcreaterole
1580
+ or role.rolcreatedb or role.rolreplication or role.rolbypassrls)
1581
+ and not exists(select 1 from active_registry registry
1582
+ join pg_catalog.pg_roles inherited on inherited.rolname<>registry.member_role
1583
+ cross join names
1584
+ where pg_catalog.pg_has_role(registry.member_role,inherited.rolname,'MEMBER')
1585
+ and inherited.rolname<>names.archive_role)
1586
+ and not exists(select 1 from active_registry registry cross join pg_catalog.pg_roles candidate,names
1587
+ where candidate.rolname not in (registry.member_role,names.schema_owner)
1588
+ and not candidate.rolsuper
1589
+ and pg_catalog.pg_has_role(candidate.rolname,registry.member_role,'MEMBER'))
1590
+ and has_schema_privilege((select archive_role from names),'agent_bridge','USAGE')
1591
+ and not has_schema_privilege((select archive_role from names),'agent_bridge','CREATE')
1592
+ and not exists(select 1 from pg_catalog.pg_class relation
1593
+ join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
1594
+ where namespace.nspname='agent_bridge' and relation.relkind in ('r','p','v','m','f')
1595
+ and has_table_privilege((select archive_role from names),relation.oid,
1596
+ 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER'))
1597
+ and not exists(select 1 from pg_catalog.pg_class sequence_record
1598
+ join pg_catalog.pg_namespace namespace on namespace.oid=sequence_record.relnamespace
1599
+ where namespace.nspname='agent_bridge'
1600
+ and case when sequence_record.relkind='S' then has_sequence_privilege(
1601
+ (select archive_role from names),sequence_record.oid,'USAGE,SELECT,UPDATE'
1602
+ ) else false end)
1603
+ and not exists(select 1 from pg_catalog.pg_proc procedure
1604
+ join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
1605
+ where namespace.nspname='agent_bridge' and has_function_privilege('public',procedure.oid,'EXECUTE'))
1606
+ and not exists(select 1 from pg_catalog.pg_proc procedure
1607
+ join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
1608
+ where namespace.nspname='agent_bridge'
1609
+ and has_function_privilege((select archive_role from names),procedure.oid,'EXECUTE')
1610
+ and procedure.oid not in (
1611
+ 'agent_bridge.archive_begin_operation(uuid,text,text,character,bigint,bigint)'::regprocedure,
1612
+ 'agent_bridge.archive_authorize_transaction(uuid,text,text,character)'::regprocedure,
1613
+ 'agent_bridge.archive_close_transaction_authorization(uuid)'::regprocedure,
1614
+ 'agent_bridge.archive_export_messages(uuid,text,timestamp with time zone,uuid,integer)'::regprocedure,
1615
+ 'agent_bridge.archive_export_receipts(uuid,text,timestamp with time zone,uuid,text,integer)'::regprocedure,
1616
+ 'agent_bridge.archive_import_messages(uuid,text,character,bigint,jsonb)'::regprocedure,
1617
+ 'agent_bridge.archive_import_receipts(uuid,text,character,bigint,jsonb)'::regprocedure,
1618
+ 'agent_bridge.archive_complete_export(uuid,text,character,bigint,bigint,timestamp with time zone,text)'::regprocedure,
1619
+ 'agent_bridge.archive_reconcile_export(uuid,text,character,bigint,bigint,timestamp with time zone)'::regprocedure,
1620
+ 'agent_bridge.archive_complete_import(uuid,text,boolean)'::regprocedure,
1621
+ 'agent_bridge.archive_abandon_operation(uuid,text,text)'::regprocedure,
1622
+ 'agent_bridge.portable_archive_ready()'::regprocedure
1623
+ ))
1624
+ and has_function_privilege((select archive_role from names),
1625
+ 'agent_bridge.archive_begin_operation(uuid,text,text,character,bigint,bigint)','EXECUTE')
1626
+ and has_function_privilege((select archive_role from names),
1627
+ 'agent_bridge.archive_authorize_transaction(uuid,text,text,character)','EXECUTE')
1628
+ and has_function_privilege((select archive_role from names),
1629
+ 'agent_bridge.archive_close_transaction_authorization(uuid)','EXECUTE')
1630
+ and has_function_privilege((select archive_role from names),
1631
+ 'agent_bridge.archive_export_messages(uuid,text,timestamp with time zone,uuid,integer)','EXECUTE')
1632
+ and has_function_privilege((select archive_role from names),
1633
+ 'agent_bridge.archive_export_receipts(uuid,text,timestamp with time zone,uuid,text,integer)','EXECUTE')
1634
+ and has_function_privilege((select archive_role from names),
1635
+ 'agent_bridge.archive_import_messages(uuid,text,character,bigint,jsonb)','EXECUTE')
1636
+ and has_function_privilege((select archive_role from names),
1637
+ 'agent_bridge.archive_import_receipts(uuid,text,character,bigint,jsonb)','EXECUTE')
1638
+ and has_function_privilege((select archive_role from names),
1639
+ 'agent_bridge.archive_complete_export(uuid,text,character,bigint,bigint,timestamp with time zone,text)','EXECUTE')
1640
+ and has_function_privilege((select archive_role from names),
1641
+ 'agent_bridge.archive_reconcile_export(uuid,text,character,bigint,bigint,timestamp with time zone)','EXECUTE')
1642
+ and has_function_privilege((select archive_role from names),
1643
+ 'agent_bridge.archive_complete_import(uuid,text,boolean)','EXECUTE')
1644
+ and has_function_privilege((select archive_role from names),
1645
+ 'agent_bridge.archive_abandon_operation(uuid,text,text)','EXECUTE')
1646
+ and has_function_privilege((select archive_role from names),
1647
+ 'agent_bridge.portable_archive_ready()','EXECUTE')
1648
+ $$;
1649
+
1650
+ revoke all on agent_bridge.archive_membership_events,agent_bridge.archive_operations,
1651
+ agent_bridge.archive_operation_batches,agent_bridge.archive_import_records,
1652
+ agent_bridge.archive_transaction_authorizations,agent_bridge.portable_archive_attestations from public;
1653
+ revoke all on sequence agent_bridge.archive_membership_events_sequence_seq,
1654
+ agent_bridge.archive_operations_sequence_seq,agent_bridge.archive_operation_batches_sequence_seq,
1655
+ agent_bridge.archive_import_records_sequence_seq from public;
1656
+ revoke execute on all functions in schema agent_bridge from public;
1657
+
1658
+ do $grants$
1659
+ declare
1660
+ suffix text := substr(md5(current_database()),1,16);
1661
+ archive_role text := 'agent_bridge_archive_operator_'||suffix;
1662
+ runtime_role text := 'agent_bridge_runtime_'||suffix;
1663
+ relation_record record;
1664
+ column_name text;
1665
+ begin
1666
+ execute format('revoke all on schema agent_bridge from %I',archive_role);
1667
+ for relation_record in select relation.relname,relation.relkind from pg_catalog.pg_class relation
1668
+ join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
1669
+ where namespace.nspname='agent_bridge' and relation.relkind in ('r','p','v','S','m','f')
1670
+ loop
1671
+ if relation_record.relkind='S' then
1672
+ execute format('revoke all on sequence agent_bridge.%I from %I',relation_record.relname,archive_role);
1673
+ else
1674
+ execute format('revoke all on table agent_bridge.%I from %I',relation_record.relname,archive_role);
1675
+ for column_name in select attribute.attname from pg_catalog.pg_attribute attribute
1676
+ join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
1677
+ join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
1678
+ where namespace.nspname='agent_bridge' and relation.relname=relation_record.relname
1679
+ and attribute.attnum>0 and not attribute.attisdropped
1680
+ loop
1681
+ execute format('revoke select(%I),insert(%I),update(%I),references(%I) on agent_bridge.%I from %I',
1682
+ column_name,column_name,column_name,column_name,relation_record.relname,archive_role);
1683
+ end loop;
1684
+ end if;
1685
+ end loop;
1686
+ execute format('revoke execute on all functions in schema agent_bridge from %I',archive_role);
1687
+ execute format('grant usage on schema agent_bridge to %I',archive_role);
1688
+ execute format('grant execute on function agent_bridge.archive_begin_operation(uuid,text,text,character,bigint,bigint),agent_bridge.archive_authorize_transaction(uuid,text,text,character),agent_bridge.archive_close_transaction_authorization(uuid),agent_bridge.archive_export_messages(uuid,text,timestamp with time zone,uuid,integer),agent_bridge.archive_export_receipts(uuid,text,timestamp with time zone,uuid,text,integer),agent_bridge.archive_import_messages(uuid,text,character,bigint,jsonb),agent_bridge.archive_import_receipts(uuid,text,character,bigint,jsonb),agent_bridge.archive_complete_export(uuid,text,character,bigint,bigint,timestamp with time zone,text),agent_bridge.archive_reconcile_export(uuid,text,character,bigint,bigint,timestamp with time zone),agent_bridge.archive_complete_import(uuid,text,boolean),agent_bridge.archive_abandon_operation(uuid,text,text),agent_bridge.portable_archive_ready() to %I',archive_role);
1689
+ execute format('grant execute on function agent_bridge.portable_archive_ready() to %I',runtime_role);
1690
+ end
1691
+ $grants$;
1692
+
1693
+ create or replace function agent_bridge.owner_control_plane_ready()
1694
+ returns boolean language sql stable security definer set search_path = '' as $$
1695
+ with names as (select
1696
+ ('agent_bridge_control_owner_'||substr(md5(current_database()),1,16))::name owner_role,
1697
+ ('agent_bridge_control_operator_'||substr(md5(current_database()),1,16))::name operator_role,
1698
+ ('agent_bridge_control_auditor_'||substr(md5(current_database()),1,16))::name auditor_role,
1699
+ (select pg_catalog.pg_get_userbyid(nspowner)::name from pg_catalog.pg_namespace
1700
+ where nspname='agent_bridge') schema_owner
1701
+ ), latest_registry as (
1702
+ select distinct on (event.member_role,event.control_role)
1703
+ event.member_role,event.control_role,event.action
1704
+ from agent_bridge.control_membership_events event
1705
+ order by event.member_role,event.control_role,event.sequence desc
1706
+ ), active_registry as (
1707
+ select member_role,control_role from latest_registry where action='register'
1708
+ ), expected_memberships as (
1709
+ select names.owner_role granted_role,names.schema_owner member_role,true admin_option
1710
+ from names
1711
+ union all select names.operator_role,names.schema_owner,true from names
1712
+ union all select names.auditor_role,names.schema_owner,true from names
1713
+ union all
1714
+ select case registry.control_role when 'operator' then names.operator_role
1715
+ else names.auditor_role end,registry.member_role,false
1716
+ from active_registry registry cross join names
1717
+ ), actual_memberships as (
1718
+ select granted.rolname::name granted_role,member.rolname::name member_role,
1719
+ membership.admin_option,
1720
+ coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
1721
+ coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
1722
+ grantor.rolname::name grantor_role
1723
+ from pg_catalog.pg_auth_members membership
1724
+ join pg_catalog.pg_roles granted on granted.oid=membership.roleid
1725
+ join pg_catalog.pg_roles member on member.oid=membership.member
1726
+ join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
1727
+ where granted.rolname in (
1728
+ (select owner_role from names),(select operator_role from names),(select auditor_role from names)
1729
+ )
1730
+ ), authority_closure as (
1731
+ select control_role.granted_role,candidate.rolname::name member_role
1732
+ from names
1733
+ cross join lateral (values
1734
+ (names.owner_role),(names.operator_role),(names.auditor_role)
1735
+ ) control_role(granted_role)
1736
+ cross join pg_catalog.pg_roles candidate
1737
+ where candidate.rolname not in (names.owner_role,names.operator_role,names.auditor_role)
1738
+ and (not candidate.rolsuper or candidate.rolname=names.schema_owner)
1739
+ and pg_catalog.pg_has_role(candidate.rolname,control_role.granted_role,'MEMBER')
1740
+ )
1741
+ select
1742
+ current_setting('server_version_num')::integer/10000=any(array[15,16,17,18])
1743
+ and (select count(*)=1 and bool_and(
1744
+ attestation.catalog_definition=agent_bridge.owner_control_catalog_definition()
1745
+ ) from agent_bridge.owner_control_attestations attestation
1746
+ where attestation.name='owner-control-v2')
1747
+ and not exists (
1748
+ (select granted_role,member_role,admin_option from actual_memberships
1749
+ except select granted_role,member_role,admin_option from expected_memberships)
1750
+ union all
1751
+ (select granted_role,member_role,admin_option from expected_memberships
1752
+ except select granted_role,member_role,admin_option from actual_memberships)
1753
+ )
1754
+ and not exists (
1755
+ select 1 from actual_memberships membership,names
1756
+ where not membership.inherit_option or not membership.set_option
1757
+ or membership.grantor_role<>names.schema_owner
1758
+ )
1759
+ and not exists (
1760
+ select 1 from active_registry registry
1761
+ left join pg_catalog.pg_roles role_record on role_record.rolname=registry.member_role
1762
+ where role_record.oid is null or not role_record.rolcanlogin or role_record.rolsuper
1763
+ or role_record.rolcreaterole or role_record.rolcreatedb
1764
+ or role_record.rolreplication or role_record.rolbypassrls
1765
+ )
1766
+ and not exists (
1767
+ (select granted_role,member_role from authority_closure
1768
+ except select granted_role,member_role from expected_memberships)
1769
+ union all
1770
+ (select granted_role,member_role from expected_memberships
1771
+ except select granted_role,member_role from authority_closure)
1772
+ )
1773
+ and not exists (
1774
+ select 1 from active_registry registry
1775
+ join pg_catalog.pg_roles inherited on inherited.rolname<>registry.member_role
1776
+ where pg_catalog.pg_has_role(registry.member_role,inherited.rolname,'MEMBER')
1777
+ and not exists (
1778
+ select 1 from active_registry permitted
1779
+ cross join names
1780
+ where permitted.member_role=registry.member_role
1781
+ and inherited.rolname=case permitted.control_role
1782
+ when 'operator' then names.operator_role else names.auditor_role end
1783
+ )
1784
+ )
1785
+ and not exists (
1786
+ select 1 from active_registry registry
1787
+ cross join pg_catalog.pg_roles candidate
1788
+ cross join names
1789
+ where candidate.rolname not in (registry.member_role,names.schema_owner)
1790
+ and not candidate.rolsuper
1791
+ and pg_catalog.pg_has_role(candidate.rolname,registry.member_role,'MEMBER')
1792
+ )
1793
+ $$;
1794
+
1795
+ insert into agent_bridge.owner_control_attestations(name,catalog_definition)
1796
+ values('owner-control-v2',agent_bridge.owner_control_catalog_definition());
1797
+
1798
+ insert into agent_bridge.portable_archive_attestations(name,catalog_definition)
1799
+ values('portable-archive-v1',agent_bridge.portable_archive_catalog_definition());
1800
+
1801
+ do $preflight$
1802
+ begin
1803
+ if not agent_bridge.portable_archive_ready()
1804
+ or not agent_bridge.owner_control_plane_ready() then
1805
+ raise exception 'portable archive readiness validation failed';
1806
+ end if;
1807
+ end
1808
+ $preflight$;
1809
+
1810
+ insert into agent_bridge.schema_migrations(version,name,checksum)
1811
+ values(15,'portable_archives','__AGENT_BRIDGE_MIGRATION_CHECKSUM__');
1812
+
1813
+ commit;