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