@creatornader/agent-bridge 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/README.md +76 -7
  3. package/ROADMAP.md +29 -0
  4. package/SECURITY.md +30 -0
  5. package/SKILL.md +5 -1
  6. package/dist/archive.d.ts +2 -2
  7. package/dist/{bridge-service-YTjGr91A.d.ts → bridge-service-CNBKdYXh.d.ts} +2 -1
  8. package/dist/{bridge-store-Boaj-ZbO.d.ts → bridge-store-ByJ6CaJk.d.ts} +1 -0
  9. package/dist/bridge.d.ts +15 -8
  10. package/dist/bridge.js +10 -8
  11. package/dist/{chunk-PDIHDBP3.js → chunk-7VJI2MXI.js} +116 -26
  12. package/dist/chunk-7VJI2MXI.js.map +1 -0
  13. package/dist/{chunk-4VB44KJU.js → chunk-AWE3DFZV.js} +3 -2
  14. package/dist/chunk-AWE3DFZV.js.map +1 -0
  15. package/dist/{chunk-IHZFI7TH.js → chunk-E4OWHGUE.js} +5 -2
  16. package/dist/chunk-E4OWHGUE.js.map +1 -0
  17. package/dist/{chunk-SYGONYMM.js → chunk-HUHG7XTB.js} +6 -5
  18. package/dist/chunk-HUHG7XTB.js.map +1 -0
  19. package/dist/{chunk-DQOZJJZ2.js → chunk-IVG4XWWS.js} +5 -5
  20. package/dist/{chunk-DQOZJJZ2.js.map → chunk-IVG4XWWS.js.map} +1 -1
  21. package/dist/{chunk-3YHVUPU2.js → chunk-P635FAN4.js} +4 -4
  22. package/dist/chunk-P635FAN4.js.map +1 -0
  23. package/dist/{chunk-6BTJE6D4.js → chunk-QNXHDOJ5.js} +56 -23
  24. package/dist/chunk-QNXHDOJ5.js.map +1 -0
  25. package/dist/{chunk-MCKKVNCE.js → chunk-QPC6JHA3.js} +5 -5
  26. package/dist/{chunk-SV3SVBYS.js → chunk-SN7PIVDF.js} +2 -2
  27. package/dist/{chunk-2DFNOLMO.js → chunk-XDNT6ZNZ.js} +9 -6
  28. package/dist/chunk-XDNT6ZNZ.js.map +1 -0
  29. package/dist/{chunk-LAHNQZDN.js → chunk-Z6JP6MYD.js} +2 -2
  30. package/dist/cli.js +9 -9
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-runtime.d.ts +3 -3
  33. package/dist/client-runtime.js +6 -6
  34. package/dist/contracts/generate.js +1 -1
  35. package/dist/dr.js +3 -3
  36. package/dist/{gateway-BQHpV-MZ.d.ts → gateway-CW-61lvs.d.ts} +2 -2
  37. package/dist/gateway-main.js +71 -7
  38. package/dist/gateway-main.js.map +1 -1
  39. package/dist/gateway.d.ts +3 -3
  40. package/dist/gateway.js +3 -3
  41. package/dist/index.js +7 -7
  42. package/dist/{postgres-bridge-store-WASumj26.d.ts → postgres-bridge-store-C9KG9Z69.d.ts} +1 -1
  43. package/dist/postgres-production-preflight.d.ts +32 -0
  44. package/dist/postgres-production-preflight.js +249 -0
  45. package/dist/postgres-production-preflight.js.map +1 -0
  46. package/dist/server.js +7 -7
  47. package/dist/{sqlite-edge-store-Bc15DvpX.d.ts → sqlite-edge-store-CdMIOrho.d.ts} +1 -1
  48. package/dist/sqlite.d.ts +3 -3
  49. package/dist/sqlite.js +2 -2
  50. package/docs/architecture-v2.md +36 -5
  51. package/docs/deployment.md +157 -0
  52. package/llms.txt +2 -1
  53. package/openapi/agent-bridge-v2.openapi.json +5 -1
  54. package/package.json +5 -1
  55. package/schemas/agent-bridge-v2.capabilities.json +2 -1
  56. package/schemas/agent-bridge-v2.mcp.json +5 -1
  57. package/schemas/agent-bridge-v2.schema.json +4 -0
  58. package/sql/migrations/007_runtime_role.sql +8 -4
  59. package/sql/migrations/013_row_isolation.sql +71 -8
  60. package/sql/migrations/014_owner_control_plane.sql +124 -31
  61. package/sql/migrations/015_portable_archives.sql +143 -41
  62. package/sql/migrations/016_native_dr_fence.sql +102 -35
  63. package/sql/migrations/018_endpoint_migration_challenges.sql +36 -10
  64. package/sql/migrations/019_endpoint_migration_same_successor.sql +34 -10
  65. package/sql/migrations/020_managed_authority_compat.sql +698 -0
  66. package/sql/migrations/021_native_backup_reader.sql +421 -0
  67. package/sql/setup.sql +1 -3
  68. package/dist/chunk-2DFNOLMO.js.map +0 -1
  69. package/dist/chunk-3YHVUPU2.js.map +0 -1
  70. package/dist/chunk-4VB44KJU.js.map +0 -1
  71. package/dist/chunk-6BTJE6D4.js.map +0 -1
  72. package/dist/chunk-IHZFI7TH.js.map +0 -1
  73. package/dist/chunk-PDIHDBP3.js.map +0 -1
  74. package/dist/chunk-SYGONYMM.js.map +0 -1
  75. /package/dist/{chunk-MCKKVNCE.js.map → chunk-QPC6JHA3.js.map} +0 -0
  76. /package/dist/{chunk-SV3SVBYS.js.map → chunk-SN7PIVDF.js.map} +0 -0
  77. /package/dist/{chunk-LAHNQZDN.js.map → chunk-Z6JP6MYD.js.map} +0 -0
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadMigrationPlan,
4
+ migrationRecordMatches
5
+ } from "./chunk-QNXHDOJ5.js";
6
+
7
+ // src/postgres-production-preflight.ts
8
+ import { fileURLToPath } from "url";
9
+ import { TLSSocket } from "tls";
10
+ import pg from "pg";
11
+ var SUPPORTED_MAJORS = /* @__PURE__ */ new Set([15, 16, 17, 18]);
12
+ var LEGACY_COLUMNS = /* @__PURE__ */ new Map([
13
+ ["id", "int8"],
14
+ ["source", "text"],
15
+ ["category", "text"],
16
+ ["content", "text"],
17
+ ["priority", "text"],
18
+ ["project", "text"],
19
+ ["metadata", "jsonb"],
20
+ ["created_at", "timestamptz"],
21
+ ["acked_by", "_text"]
22
+ ]);
23
+ function check(name, ok, detail) {
24
+ return { name, ok, detail };
25
+ }
26
+ function migrationPrefix(applied, expected) {
27
+ return applied.length <= expected.length && applied.every(
28
+ (entry, index) => expected[index] !== void 0 && migrationRecordMatches(entry, expected[index])
29
+ );
30
+ }
31
+ async function legacyChecks(db, checks) {
32
+ const owner = await db.query(`
33
+ SELECT relation.relowner=current_user::regrole::oid AS "ownedByCaller"
34
+ FROM pg_catalog.pg_class relation
35
+ WHERE relation.oid='public.shared_context'::regclass`);
36
+ checks.push(check(
37
+ "legacy.owner",
38
+ owner.rows[0]?.ownedByCaller === true,
39
+ "the migration authority owns public.shared_context"
40
+ ));
41
+ const columns = await db.query(`
42
+ SELECT column_name AS "columnName",udt_name AS "typeName"
43
+ FROM information_schema.columns
44
+ WHERE table_schema='public' AND table_name='shared_context'`);
45
+ const actual = new Map(columns.rows.map((row) => [row.columnName, row.typeName]));
46
+ const compatible = [...LEGACY_COLUMNS].every(([name, type]) => actual.get(name) === type);
47
+ checks.push(check(
48
+ "legacy.columns",
49
+ compatible,
50
+ "public.shared_context has the columns required by migration 006"
51
+ ));
52
+ if (!compatible) return void 0;
53
+ const legacy = await db.query(`
54
+ WITH mapped AS (
55
+ SELECT CASE
56
+ WHEN metadata #>> '{message_envelope,message_id}' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
57
+ THEN lower(metadata #>> '{message_envelope,message_id}')
58
+ ELSE 'legacy:' || id::text
59
+ END AS message_id
60
+ FROM public.shared_context
61
+ )
62
+ SELECT
63
+ (SELECT count(*)::text FROM mapped) AS "rowCount",
64
+ (SELECT count(*)::text FROM (
65
+ SELECT message_id FROM mapped GROUP BY message_id HAVING count(*)>1
66
+ ) duplicates) AS "duplicateMessageIds"`);
67
+ const result = legacy.rows[0];
68
+ const duplicates = result?.duplicateMessageIds ?? "unknown";
69
+ checks.push(check(
70
+ "legacy.message_ids",
71
+ duplicates === "0",
72
+ "legacy rows map to unique immutable message IDs"
73
+ ));
74
+ return result?.rowCount;
75
+ }
76
+ async function runPostgresProductionPreflight(db, migrationsDirectory, options = {}) {
77
+ const expected = await loadMigrationPlan(migrationsDirectory);
78
+ await db.query("BEGIN TRANSACTION READ ONLY");
79
+ try {
80
+ await db.query("SET LOCAL statement_timeout='15s'");
81
+ await db.query("SET LOCAL lock_timeout='2s'");
82
+ const authority = await db.query(`
83
+ SELECT
84
+ current_setting('server_version_num') AS "serverVersionNum",
85
+ pg_is_in_recovery() AS "inRecovery",
86
+ role.rolcanlogin AS "canLogin",
87
+ role.rolinherit AS inherits,
88
+ role.rolsuper AS "isSuperuser",
89
+ role.rolcreaterole AS "canCreateRole",
90
+ role.rolbypassrls AS "bypassesRls",
91
+ has_database_privilege(current_user,current_database(),'CREATE') AS "canCreateDatabaseObject",
92
+ to_regnamespace('agent_bridge')::text AS "bridgeSchema",
93
+ to_regclass('agent_bridge.schema_migrations')::text AS "migrationTable",
94
+ to_regclass('public.shared_context')::text AS "legacyTable",
95
+ coalesce((SELECT ssl FROM pg_catalog.pg_stat_ssl WHERE pid=pg_backend_pid()),false) AS ssl,
96
+ pg_database_size(current_database())::text AS "databaseBytes"
97
+ FROM pg_catalog.pg_roles role
98
+ WHERE role.rolname=current_user`);
99
+ const row = authority.rows[0];
100
+ if (!row) throw new Error("current PostgreSQL role could not be inspected");
101
+ const serverMajor = Math.floor(Number(row.serverVersionNum) / 1e4);
102
+ const ssl = row.ssl || options.clientTransportSsl === true;
103
+ const checks = [
104
+ check("postgres.version", SUPPORTED_MAJORS.has(serverMajor), "PostgreSQL major is supported"),
105
+ check("postgres.primary", !row.inRecovery, "connection targets a writable primary"),
106
+ check("authority.login", row.canLogin && row.inherits, "migration role can log in and inherits grants"),
107
+ check("authority.database_create", row.canCreateDatabaseObject, "migration role can create database objects"),
108
+ check("authority.roles", row.isSuperuser || row.canCreateRole, "migration role can administer restricted roles"),
109
+ check(
110
+ "authority.native_dr",
111
+ row.isSuperuser || row.bypassesRls,
112
+ "native DR source role can bypass row-level security"
113
+ ),
114
+ ...options.requireSsl ? [check(
115
+ "connection.ssl",
116
+ ssl,
117
+ row.ssl ? "PostgreSQL backend reports TLS" : options.clientTransportSsl ? "client transport uses TLS" : "connection does not use TLS"
118
+ )] : []
119
+ ];
120
+ let applied = [];
121
+ if (row.migrationTable) {
122
+ const result = await db.query(
123
+ "SELECT version,name,checksum FROM agent_bridge.schema_migrations ORDER BY version"
124
+ );
125
+ applied = result.rows.map((entry) => ({
126
+ version: Number(entry.version),
127
+ name: entry.name,
128
+ checksum: entry.checksum
129
+ }));
130
+ }
131
+ const prefix = migrationPrefix(applied, expected);
132
+ const unmanagedSchema = Boolean(row.bridgeSchema) && (!row.migrationTable || applied.length === 0);
133
+ const migrationState = unmanagedSchema || !prefix || !row.bridgeSchema && Boolean(row.migrationTable) ? "invalid" : applied.length === expected.length ? "current" : applied.length === 0 ? "uninitialized" : "upgradeable";
134
+ checks.push(check(
135
+ "schema.migrations",
136
+ migrationState !== "invalid",
137
+ migrationState === "invalid" ? "agent_bridge namespace or migration ledger conflicts with the release plan" : `migration state is ${migrationState}`
138
+ ));
139
+ if (!row.bridgeSchema) {
140
+ const collisions = await db.query(`
141
+ WITH names(role_name) AS (VALUES
142
+ ('agent_bridge_runtime_' || substr(md5(current_database()),1,16)),
143
+ ('agent_bridge_data_owner_' || substr(md5(current_database()),1,16)),
144
+ ('agent_bridge_context_reader_' || substr(md5(current_database()),1,16)),
145
+ ('agent_bridge_event_writer_' || substr(md5(current_database()),1,16)),
146
+ ('agent_bridge_control_owner_' || substr(md5(current_database()),1,16)),
147
+ ('agent_bridge_control_operator_' || substr(md5(current_database()),1,16)),
148
+ ('agent_bridge_control_auditor_' || substr(md5(current_database()),1,16)),
149
+ ('agent_bridge_archive_operator_' || substr(md5(current_database()),1,16)),
150
+ ('agent_bridge_backup_reader_' || substr(md5(current_database()),1,16))
151
+ )
152
+ SELECT count(*)::text AS count FROM names JOIN pg_catalog.pg_roles role
153
+ ON role.rolname=names.role_name`);
154
+ checks.push(check(
155
+ "schema.role_collisions",
156
+ collisions.rows[0]?.count === "0",
157
+ "no derived Agent Bridge roles predate the schema"
158
+ ));
159
+ }
160
+ let legacyRows;
161
+ if (row.legacyTable && applied.length < 6) {
162
+ legacyRows = await legacyChecks(db, checks);
163
+ }
164
+ await db.query("COMMIT");
165
+ return {
166
+ schema: "agent-bridge-postgres-production-preflight-v1",
167
+ ok: checks.every((entry) => entry.ok),
168
+ checks,
169
+ observations: {
170
+ serverMajor,
171
+ databaseBytes: row.databaseBytes,
172
+ migrationState,
173
+ appliedMigrationCount: applied.length,
174
+ requiredMigrationCount: expected.length,
175
+ legacyTable: Boolean(row.legacyTable),
176
+ ...legacyRows === void 0 ? {} : { legacyRows },
177
+ ssl
178
+ }
179
+ };
180
+ } catch (error) {
181
+ await db.query("ROLLBACK").catch(() => void 0);
182
+ throw error;
183
+ }
184
+ }
185
+ function parseArguments(args) {
186
+ const options = { json: false, help: false, requireSsl: false };
187
+ for (const argument of args) {
188
+ if (argument === "--json") options.json = true;
189
+ else if (argument === "--require-ssl") options.requireSsl = true;
190
+ else if (argument === "--help" || argument === "-h") options.help = true;
191
+ else throw new Error(`unknown argument: ${argument}`);
192
+ }
193
+ return options;
194
+ }
195
+ function safeError(error, secret) {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ return message.split(secret).join("[redacted]").replace(/postgres(?:ql)?:\/\/\S+/giu, "[redacted]");
198
+ }
199
+ async function main() {
200
+ const options = parseArguments(process.argv.slice(2));
201
+ if (options.help) {
202
+ process.stdout.write("Usage: npm run preflight:postgres:production -- [--json] [--require-ssl]\n");
203
+ return;
204
+ }
205
+ const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;
206
+ if (!databaseUrl) throw new Error("AGENT_BRIDGE_DATABASE_URL is required");
207
+ const client = new pg.Client({
208
+ connectionString: databaseUrl,
209
+ connectionTimeoutMillis: 1e4,
210
+ application_name: "agent-bridge-production-preflight"
211
+ });
212
+ try {
213
+ await client.connect();
214
+ const clientTransportSsl = client.connection.stream instanceof TLSSocket && client.connection.stream.encrypted;
215
+ const migrationsDirectory = fileURLToPath(new URL("../sql/migrations", import.meta.url));
216
+ const report = await runPostgresProductionPreflight({
217
+ query: (sql) => client.query(sql)
218
+ }, migrationsDirectory, {
219
+ requireSsl: options.requireSsl,
220
+ clientTransportSsl
221
+ });
222
+ if (options.json) process.stdout.write(`${JSON.stringify(report)}
223
+ `);
224
+ else {
225
+ for (const entry of report.checks) {
226
+ process.stdout.write(`${entry.ok ? "OK" : "ERROR"} ${entry.name}: ${entry.detail}
227
+ `);
228
+ }
229
+ process.stdout.write(`${report.ok ? "PostgreSQL production preflight passed" : "PostgreSQL production preflight failed"}
230
+ `);
231
+ }
232
+ if (!report.ok) process.exitCode = 1;
233
+ } catch (error) {
234
+ throw new Error(safeError(error, databaseUrl));
235
+ } finally {
236
+ await client.end().catch(() => void 0);
237
+ }
238
+ }
239
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
240
+ void main().catch((error) => {
241
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
242
+ `);
243
+ process.exitCode = 1;
244
+ });
245
+ }
246
+ export {
247
+ runPostgresProductionPreflight
248
+ };
249
+ //# sourceMappingURL=postgres-production-preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/postgres-production-preflight.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { TLSSocket } from \"node:tls\";\nimport pg from \"pg\";\nimport { loadMigrationPlan, migrationRecordMatches, type AppliedMigration } from \"./migrations.js\";\n\nconst SUPPORTED_MAJORS = new Set([15, 16, 17, 18]);\nconst LEGACY_COLUMNS = new Map([\n [\"id\", \"int8\"],\n [\"source\", \"text\"],\n [\"category\", \"text\"],\n [\"content\", \"text\"],\n [\"priority\", \"text\"],\n [\"project\", \"text\"],\n [\"metadata\", \"jsonb\"],\n [\"created_at\", \"timestamptz\"],\n [\"acked_by\", \"_text\"],\n]);\n\ninterface Check {\n name: string;\n ok: boolean;\n detail: string;\n}\n\ninterface ProductionPreflightDatabase {\n query<T extends Record<string, unknown> = Record<string, unknown>>(\n sql: string,\n ): Promise<{ rows: T[]; rowCount: number | null }>;\n}\n\ninterface AuthorityRow extends Record<string, unknown> {\n serverVersionNum: string;\n inRecovery: boolean;\n canLogin: boolean;\n inherits: boolean;\n isSuperuser: boolean;\n canCreateRole: boolean;\n bypassesRls: boolean;\n canCreateDatabaseObject: boolean;\n bridgeSchema: string | null;\n migrationTable: string | null;\n legacyTable: string | null;\n ssl: boolean;\n databaseBytes: string;\n}\n\ninterface LegacyColumnRow extends Record<string, unknown> {\n columnName: string;\n typeName: string;\n}\n\nexport interface PostgresProductionPreflightReport {\n schema: \"agent-bridge-postgres-production-preflight-v1\";\n ok: boolean;\n checks: Check[];\n observations: {\n serverMajor: number;\n databaseBytes: string;\n migrationState: \"uninitialized\" | \"upgradeable\" | \"current\" | \"invalid\";\n appliedMigrationCount: number;\n requiredMigrationCount: number;\n legacyTable: boolean;\n legacyRows?: string;\n ssl: boolean;\n };\n}\n\nfunction check(name: string, ok: boolean, detail: string): Check {\n return { name, ok, detail };\n}\n\nfunction migrationPrefix(\n applied: AppliedMigration[],\n expected: ReadonlyArray<AppliedMigration>,\n): boolean {\n return applied.length <= expected.length && applied.every((entry, index) =>\n expected[index] !== undefined && migrationRecordMatches(entry, expected[index])\n );\n}\n\nasync function legacyChecks(\n db: ProductionPreflightDatabase,\n checks: Check[],\n): Promise<string | undefined> {\n const owner = await db.query<{ ownedByCaller: boolean }>(`\n SELECT relation.relowner=current_user::regrole::oid AS \"ownedByCaller\"\n FROM pg_catalog.pg_class relation\n WHERE relation.oid='public.shared_context'::regclass`);\n checks.push(check(\n \"legacy.owner\",\n owner.rows[0]?.ownedByCaller === true,\n \"the migration authority owns public.shared_context\",\n ));\n\n const columns = await db.query<LegacyColumnRow>(`\n SELECT column_name AS \"columnName\",udt_name AS \"typeName\"\n FROM information_schema.columns\n WHERE table_schema='public' AND table_name='shared_context'`);\n const actual = new Map(columns.rows.map((row) => [row.columnName, row.typeName]));\n const compatible = [...LEGACY_COLUMNS].every(([name, type]) => actual.get(name) === type);\n checks.push(check(\n \"legacy.columns\",\n compatible,\n \"public.shared_context has the columns required by migration 006\",\n ));\n\n if (!compatible) return undefined;\n const legacy = await db.query<{ rowCount: string; duplicateMessageIds: string }>(`\n WITH mapped AS (\n SELECT CASE\n WHEN metadata #>> '{message_envelope,message_id}' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'\n THEN lower(metadata #>> '{message_envelope,message_id}')\n ELSE 'legacy:' || id::text\n END AS message_id\n FROM public.shared_context\n )\n SELECT\n (SELECT count(*)::text FROM mapped) AS \"rowCount\",\n (SELECT count(*)::text FROM (\n SELECT message_id FROM mapped GROUP BY message_id HAVING count(*)>1\n ) duplicates) AS \"duplicateMessageIds\"`);\n const result = legacy.rows[0];\n const duplicates = result?.duplicateMessageIds ?? \"unknown\";\n checks.push(check(\n \"legacy.message_ids\",\n duplicates === \"0\",\n \"legacy rows map to unique immutable message IDs\",\n ));\n return result?.rowCount;\n}\n\nexport async function runPostgresProductionPreflight(\n db: ProductionPreflightDatabase,\n migrationsDirectory: string,\n options: { requireSsl?: boolean; clientTransportSsl?: boolean } = {},\n): Promise<PostgresProductionPreflightReport> {\n const expected = await loadMigrationPlan(migrationsDirectory);\n await db.query(\"BEGIN TRANSACTION READ ONLY\");\n try {\n await db.query(\"SET LOCAL statement_timeout='15s'\");\n await db.query(\"SET LOCAL lock_timeout='2s'\");\n const authority = await db.query<AuthorityRow>(`\n SELECT\n current_setting('server_version_num') AS \"serverVersionNum\",\n pg_is_in_recovery() AS \"inRecovery\",\n role.rolcanlogin AS \"canLogin\",\n role.rolinherit AS inherits,\n role.rolsuper AS \"isSuperuser\",\n role.rolcreaterole AS \"canCreateRole\",\n role.rolbypassrls AS \"bypassesRls\",\n has_database_privilege(current_user,current_database(),'CREATE') AS \"canCreateDatabaseObject\",\n to_regnamespace('agent_bridge')::text AS \"bridgeSchema\",\n to_regclass('agent_bridge.schema_migrations')::text AS \"migrationTable\",\n to_regclass('public.shared_context')::text AS \"legacyTable\",\n coalesce((SELECT ssl FROM pg_catalog.pg_stat_ssl WHERE pid=pg_backend_pid()),false) AS ssl,\n pg_database_size(current_database())::text AS \"databaseBytes\"\n FROM pg_catalog.pg_roles role\n WHERE role.rolname=current_user`);\n const row = authority.rows[0];\n if (!row) throw new Error(\"current PostgreSQL role could not be inspected\");\n\n const serverMajor = Math.floor(Number(row.serverVersionNum) / 10_000);\n const ssl = row.ssl || options.clientTransportSsl === true;\n const checks: Check[] = [\n check(\"postgres.version\", SUPPORTED_MAJORS.has(serverMajor), \"PostgreSQL major is supported\"),\n check(\"postgres.primary\", !row.inRecovery, \"connection targets a writable primary\"),\n check(\"authority.login\", row.canLogin && row.inherits, \"migration role can log in and inherits grants\"),\n check(\"authority.database_create\", row.canCreateDatabaseObject, \"migration role can create database objects\"),\n check(\"authority.roles\", row.isSuperuser || row.canCreateRole, \"migration role can administer restricted roles\"),\n check(\n \"authority.native_dr\",\n row.isSuperuser || row.bypassesRls,\n \"native DR source role can bypass row-level security\",\n ),\n ...(options.requireSsl\n ? [check(\n \"connection.ssl\",\n ssl,\n row.ssl\n ? \"PostgreSQL backend reports TLS\"\n : options.clientTransportSsl\n ? \"client transport uses TLS\"\n : \"connection does not use TLS\",\n )]\n : []),\n ];\n\n let applied: AppliedMigration[] = [];\n if (row.migrationTable) {\n const result = await db.query<{ version: number; name: string; checksum: string }>(\n \"SELECT version,name,checksum FROM agent_bridge.schema_migrations ORDER BY version\",\n );\n applied = result.rows.map((entry) => ({\n version: Number(entry.version),\n name: entry.name,\n checksum: entry.checksum,\n }));\n }\n const prefix = migrationPrefix(applied, expected);\n const unmanagedSchema = Boolean(row.bridgeSchema) && (\n !row.migrationTable || applied.length === 0\n );\n const migrationState = unmanagedSchema || !prefix || (!row.bridgeSchema && Boolean(row.migrationTable))\n ? \"invalid\"\n : applied.length === expected.length\n ? \"current\"\n : applied.length === 0\n ? \"uninitialized\"\n : \"upgradeable\";\n checks.push(check(\n \"schema.migrations\",\n migrationState !== \"invalid\",\n migrationState === \"invalid\"\n ? \"agent_bridge namespace or migration ledger conflicts with the release plan\"\n : `migration state is ${migrationState}`,\n ));\n\n if (!row.bridgeSchema) {\n const collisions = await db.query<{ count: string }>(`\n WITH names(role_name) AS (VALUES\n ('agent_bridge_runtime_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_data_owner_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_context_reader_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_event_writer_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_control_owner_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_control_operator_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_control_auditor_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_archive_operator_' || substr(md5(current_database()),1,16)),\n ('agent_bridge_backup_reader_' || substr(md5(current_database()),1,16))\n )\n SELECT count(*)::text AS count FROM names JOIN pg_catalog.pg_roles role\n ON role.rolname=names.role_name`);\n checks.push(check(\n \"schema.role_collisions\",\n collisions.rows[0]?.count === \"0\",\n \"no derived Agent Bridge roles predate the schema\",\n ));\n }\n\n let legacyRows: string | undefined;\n if (row.legacyTable && applied.length < 6) {\n legacyRows = await legacyChecks(db, checks);\n }\n\n await db.query(\"COMMIT\");\n return {\n schema: \"agent-bridge-postgres-production-preflight-v1\",\n ok: checks.every((entry) => entry.ok),\n checks,\n observations: {\n serverMajor,\n databaseBytes: row.databaseBytes,\n migrationState,\n appliedMigrationCount: applied.length,\n requiredMigrationCount: expected.length,\n legacyTable: Boolean(row.legacyTable),\n ...(legacyRows === undefined ? {} : { legacyRows }),\n ssl,\n },\n };\n } catch (error) {\n await db.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n }\n}\n\nfunction parseArguments(args: string[]): { json: boolean; help: boolean; requireSsl: boolean } {\n const options = { json: false, help: false, requireSsl: false };\n for (const argument of args) {\n if (argument === \"--json\") options.json = true;\n else if (argument === \"--require-ssl\") options.requireSsl = true;\n else if (argument === \"--help\" || argument === \"-h\") options.help = true;\n else throw new Error(`unknown argument: ${argument}`);\n }\n return options;\n}\n\nfunction safeError(error: unknown, secret: string): string {\n const message = error instanceof Error ? error.message : String(error);\n return message.split(secret).join(\"[redacted]\").replace(/postgres(?:ql)?:\\/\\/\\S+/giu, \"[redacted]\");\n}\n\nasync function main(): Promise<void> {\n const options = parseArguments(process.argv.slice(2));\n if (options.help) {\n process.stdout.write(\"Usage: npm run preflight:postgres:production -- [--json] [--require-ssl]\\n\");\n return;\n }\n const databaseUrl = process.env.AGENT_BRIDGE_DATABASE_URL;\n if (!databaseUrl) throw new Error(\"AGENT_BRIDGE_DATABASE_URL is required\");\n const client = new pg.Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: 10_000,\n application_name: \"agent-bridge-production-preflight\",\n });\n try {\n await client.connect();\n const clientTransportSsl = client.connection.stream instanceof TLSSocket &&\n client.connection.stream.encrypted;\n const migrationsDirectory = fileURLToPath(new URL(\"../sql/migrations\", import.meta.url));\n const report = await runPostgresProductionPreflight({\n query: (sql) => client.query(sql),\n }, migrationsDirectory, {\n requireSsl: options.requireSsl,\n clientTransportSsl,\n });\n if (options.json) process.stdout.write(`${JSON.stringify(report)}\\n`);\n else {\n for (const entry of report.checks) {\n process.stdout.write(`${entry.ok ? \"OK\" : \"ERROR\"} ${entry.name}: ${entry.detail}\\n`);\n }\n process.stdout.write(`${report.ok ? \"PostgreSQL production preflight passed\" : \"PostgreSQL production preflight failed\"}\\n`);\n }\n if (!report.ok) process.exitCode = 1;\n } catch (error) {\n throw new Error(safeError(error, databaseUrl));\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\nif (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {\n void main().catch((error) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n });\n}\n"],"mappings":"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","names":[]}
package/dist/server.js CHANGED
@@ -4,16 +4,16 @@ import {
4
4
  configFromEnv,
5
5
  createAgentBridgeServer,
6
6
  startServer
7
- } from "./chunk-SYGONYMM.js";
7
+ } from "./chunk-HUHG7XTB.js";
8
8
  import "./chunk-2EVLG3YA.js";
9
- import "./chunk-MCKKVNCE.js";
10
- import "./chunk-LAHNQZDN.js";
11
- import "./chunk-SV3SVBYS.js";
12
- import "./chunk-DQOZJJZ2.js";
9
+ import "./chunk-QPC6JHA3.js";
10
+ import "./chunk-Z6JP6MYD.js";
11
+ import "./chunk-SN7PIVDF.js";
12
+ import "./chunk-IVG4XWWS.js";
13
13
  import "./chunk-GTK4KOED.js";
14
14
  import "./chunk-VOQIDTCJ.js";
15
- import "./chunk-4VB44KJU.js";
16
- import "./chunk-IHZFI7TH.js";
15
+ import "./chunk-AWE3DFZV.js";
16
+ import "./chunk-E4OWHGUE.js";
17
17
  import "./chunk-BZLMF5O5.js";
18
18
  export {
19
19
  MCP_IMPLEMENTATION_VERSION,
@@ -1,5 +1,5 @@
1
1
  import { b as BridgeMessage, c as BridgePrincipal } from './bridge-domain-CBX3IVqC.js';
2
- import { d as MessageQuery, M as MessagePage } from './bridge-store-Boaj-ZbO.js';
2
+ import { d as MessageQuery, M as MessagePage } from './bridge-store-ByJ6CaJk.js';
3
3
 
4
4
  type PendingMessage = Omit<BridgeMessage, "sequence" | "createdAt">;
5
5
  interface EdgeScope {
package/dist/sqlite.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { c as BridgePrincipal, B as BridgeDelivery, g as DeliveryState, R as RetryPolicy, A as AgentPresence, b as BridgeMessage, a as BridgeDeliveryEvent } from './bridge-domain-CBX3IVqC.js';
2
- import { M as MessagePage, B as BridgeDiagnostics, I as InsertMessageResult, a as BridgeStore, d as MessageQuery, C as ClaimOptions, c as DeliveryQuery, b as DeliveryPage, D as DeliveryEventPage } from './bridge-store-Boaj-ZbO.js';
3
- import { e as EdgeMigrationGateState, S as SQLiteEdgeStore, a as EdgeDrainLease, P as PendingMessage } from './sqlite-edge-store-Bc15DvpX.js';
4
- export { E as EdgeConflictError, b as EdgeEnqueueResult, c as EdgeMigrationDrainingError, d as EdgeMigrationGate, f as EdgeMigrationLeaseError, g as EdgeMigrationRetiredError, h as EdgeOutboxRecord, i as EdgeReadOnlyInspection, j as EdgeScope, k as EdgeStats, l as edgeMessageFingerprint, m as edgeScopeKey, n as inspectEdgeScopeReadOnly, s as stableIdempotency } from './sqlite-edge-store-Bc15DvpX.js';
2
+ import { M as MessagePage, B as BridgeDiagnostics, I as InsertMessageResult, a as BridgeStore, d as MessageQuery, C as ClaimOptions, c as DeliveryQuery, b as DeliveryPage, D as DeliveryEventPage } from './bridge-store-ByJ6CaJk.js';
3
+ import { e as EdgeMigrationGateState, S as SQLiteEdgeStore, a as EdgeDrainLease, P as PendingMessage } from './sqlite-edge-store-CdMIOrho.js';
4
+ export { E as EdgeConflictError, b as EdgeEnqueueResult, c as EdgeMigrationDrainingError, d as EdgeMigrationGate, f as EdgeMigrationLeaseError, g as EdgeMigrationRetiredError, h as EdgeOutboxRecord, i as EdgeReadOnlyInspection, j as EdgeScope, k as EdgeStats, l as edgeMessageFingerprint, m as edgeScopeKey, n as inspectEdgeScopeReadOnly, s as stableIdempotency } from './sqlite-edge-store-CdMIOrho.js';
5
5
 
6
6
  interface SyncInsertResult extends InsertMessageResult {
7
7
  disposition: "committed" | "queued";
package/dist/sqlite.js CHANGED
@@ -11,10 +11,10 @@ import {
11
11
  inspectEdgeScopeReadOnly,
12
12
  isRetryableSyncError,
13
13
  stableIdempotency
14
- } from "./chunk-SV3SVBYS.js";
14
+ } from "./chunk-SN7PIVDF.js";
15
15
  import {
16
16
  SQLiteBridgeStore
17
- } from "./chunk-DQOZJJZ2.js";
17
+ } from "./chunk-IVG4XWWS.js";
18
18
  import "./chunk-GTK4KOED.js";
19
19
  import "./chunk-VOQIDTCJ.js";
20
20
  import "./chunk-BZLMF5O5.js";
@@ -40,6 +40,22 @@ receives no schema-owner authority. Its readiness check fails on an incomplete
40
40
  migration plan, unsupported PostgreSQL major, row-isolation drift, protected catalog
41
41
  drift, or an invalid owner-control membership graph.
42
42
 
43
+ Runtime bootstrap runs in one transaction. Before it changes an existing login, it
44
+ rejects every database-derived Agent Bridge authority name, unexpected role edge, and
45
+ object owner. PostgreSQL 16 and newer may add one managed-provider administrator edge
46
+ when the schema owner creates the login. The bootstrap validates that edge exactly. It
47
+ does not adopt a `NOLOGIN` shell restored by native DR.
48
+
49
+ The schema owner can be a true superuser or a managed-provider administrator with
50
+ `CREATEROLE`. That authority is sufficient for migrations. Native backup requires the
51
+ schema owner to be a true superuser or hold `BYPASSRLS`. A restore target validates the
52
+ intentionally suspended restored schema-owner shell through its superuser session. PostgreSQL 16 and newer may
53
+ split its authority over an internal role between a bootstrap administrator row and a
54
+ schema-owner inherit-and-set row. Native DR may restore the same effective authority as
55
+ one combined bootstrap-superuser row. Security checks aggregate the effective options
56
+ but also validate each allowed physical grant shape. Registered operator and archive
57
+ memberships remain single, schema-owner-granted edges without the admin option.
58
+
43
59
  The repository Compose stack publishes the gateway and PostgreSQL on loopback for
44
60
  development. Production deployments put PostgreSQL on a private network, terminate TLS
45
61
  before every non-loopback gateway, and source credentials from the platform's secret
@@ -168,9 +184,24 @@ server major, an exact-major `pg_restore`, and superuser authority. It restores
168
184
  shells as `NOLOGIN`, then schema objects, memberships, and default privileges. Claimed
169
185
  deliveries become retrying deliveries with cleared leases. The restore checks migrations,
170
186
  counts, role inventory, and every readiness attestation before success. External
171
- principals remain `NOLOGIN`. A failure after target mutation begins disables new target
172
- connections. If that offlining step fails, the command reports the target as unsafe and
173
- lists residual role shells.
187
+ principals remain `NOLOGIN`. Runtime memberships are omitted. Active control and archive
188
+ memberships are omitted and receive explicit revocation events after their prior history.
189
+ A matching schema-owner role may be reused only when it is the target session user and
190
+ a true superuser. Every other role collision fails. A failure after target mutation
191
+ begins disables new target connections. If that offlining step fails, the command
192
+ reports the target as unsafe and lists residual role shells.
193
+
194
+ Managed schema owners need read access to protected ledgers and sequences before
195
+ `pg_dump` can produce a complete native backup. A database-derived `NOLOGIN`
196
+ backup-reader role receives only that access and is granted only to the schema owner.
197
+ Native backup requires that schema owner to be a true superuser or hold `BYPASSRLS`.
198
+ During restore, the target superuser may validate the intentionally suspended restored
199
+ schema-owner shell. A restricted runtime login cannot substitute for that authority.
200
+ `CREATEROLE` alone can apply migrations but cannot produce a complete native backup.
201
+ Native-backup readiness attests the role, authority, membership, and object ACLs. The
202
+ gateway and external operator roles cannot inherit the reader. This backup gate is
203
+ separate from gateway runtime readiness, so drift in an owner-only backup object cannot
204
+ take a healthy gateway out of service.
174
205
 
175
206
  PostgreSQL source and target URLs come only from
176
207
  `AGENT_BRIDGE_DR_SOURCE_DATABASE_URL` and
@@ -208,7 +239,7 @@ A receipt records that a principal read a message.
208
239
 
209
240
  A publisher stores an immutable `deliveryPolicy` with each message. Untargeted messages default to mailbox mode. Targeted messages default to leased mode with `maxAttempts: 5`, `retryBaseDelayMs: 1000`, `retryMaxDelayMs: 60000`, and `retryJitterRatio: 0.2`. Leased policy may also set `notBefore`. Mailbox messages create no deliveries, and legacy mode rejects leased policy.
210
241
 
211
- A leased delivery records executable work for a recipient. Its state is one of `pending`, `claimed`, `acked`, `retrying`, `dead`, or `cancelled`. `attempt` is lifetime-monotonic, while `cycleAttempt` resets on publisher requeue and `requeueCount` increments. Stored policy owns retry and exhaustion. Claims order due work by urgent, high, then info priority, followed by availability, message creation time, and the stable delivery ID tie-break.
242
+ A leased delivery records executable work for a recipient. Its state is one of `pending`, `claimed`, `acked`, `retrying`, `dead`, or `cancelled`. `attempt` is lifetime-monotonic, while `cycleAttempt` resets on publisher requeue and `requeueCount` increments. Stored policy owns retry and exhaustion. Claims order due work by urgent, high, then info priority, followed by availability, message creation time, and the stable delivery ID tie-break. HTTP 2.1, MCP, and CLI may optionally identify one message. That form atomically selects only the eligible delivery for the authenticated recipient and scopes claim-time expiry and exhaustion maintenance to the same message. Omitting the filter preserves claim-next behavior. The frozen HTTP 2.0 request rejects the additive field.
212
243
 
213
244
  Consumer-side `maxAttempts` on claim and `retryPolicy` on nack are compatibility inputs for one release. The service validates them and ignores them. This keeps older clients working without letting a consumer change immutable publisher intent.
214
245
 
@@ -512,7 +543,7 @@ must already satisfy that final policy. Node identity and file-type checks run b
512
543
  and after the native policy check. Native reparse attributes
513
544
  reject symlinks, junctions, and other reparse objects before and after DACL work.
514
545
 
515
- The schema owner is the trusted offline role administrator. It holds all three control roles with the admin option and registers eligible login roles through `register_control_member`; `revoke_control_member` removes them. Those functions are not available through HTTP or MCP. Each call is idempotent by request UUID and appends the database session actor to the membership ledger. Runtime readiness compares the active registry with the full `pg_has_role` closure and the direct `pg_auth_members` edges. It rejects unregistered operator or auditor holders, every external owner holder, missing registered grants, unsafe login attributes, extra roles inherited by a registered member, and roles that inherit a registered member. Direct edges must retain their expected grantor, admin, inherit, and set options. Only the schema owner is exempt from the broad closure check because a PostgreSQL superuser reports membership across roles. A direct `GRANT` is never a valid deployment shortcut.
546
+ The schema owner is the trusted offline role administrator. It holds all three control roles with effective admin, inherit, and set authority and registers eligible login roles through `register_control_member`; `revoke_control_member` removes them. Those functions are not available through HTTP or MCP. Each call is idempotent by request UUID and appends the database session actor to the membership ledger. Runtime readiness compares the active registry with the full `pg_has_role` closure and normalized direct `pg_auth_members` edges. It also checks the raw grant origins and option shapes. It rejects unregistered operator or auditor holders, every external owner holder, missing registered grants, unsafe login attributes, extra roles inherited by a registered member, and roles that inherit a registered member. Only the schema owner is exempt from the broad closure check because a PostgreSQL superuser reports membership across roles. A direct `GRANT` is never a valid deployment shortcut.
516
547
 
517
548
  Every protected operation first locks a member-global key for `session_user`, followed by capability keys in operator-before-auditor order. It then checks the active registry, direct membership edge, safe login attributes, and complete upstream and downstream role closure. Inventory accepts either registered capability. Registration and revocation use the same global-first lock hierarchy. Opposite operator and auditor changes for one member therefore cannot deadlock. A revocation waits for an operation that already passed authorization, while every operation begun after revocation observes the new state. A session that remains in a revoked role through `SET ROLE` cannot use that stale authorization.
518
549
 
@@ -119,6 +119,26 @@ Keep these authorities separate:
119
119
  | Archive operator | Portable PostgreSQL archive operations | Never |
120
120
  | Backup and restore | Native DR backup or restore | Never |
121
121
 
122
+ The schema owner may be a true PostgreSQL superuser or a managed-provider
123
+ administrator with `CREATEROLE` and the database privileges reported by the production
124
+ preflight. `CREATEROLE` is sufficient for migrations. Native backup requires the
125
+ schema owner to be a true superuser or hold `BYPASSRLS`. A restore target validates the
126
+ intentionally suspended restored schema-owner shell through its superuser session. Migrations create internal
127
+ roles with safe defaults and reject unsafe preexisting roles instead of trying to demote
128
+ them. PostgreSQL 16 and newer may store the schema owner's effective admin, inherit,
129
+ and set authority in two physical grant rows. A native DR restore may recreate it as one
130
+ combined bootstrap-superuser grant. Agent Bridge validates both forms and their
131
+ effective authority. The restricted runtime login must never receive either schema-owner
132
+ authority or `BYPASSRLS`.
133
+
134
+ Migration 021 creates a database-derived `NOLOGIN` backup-reader role. It receives
135
+ `SELECT` on Agent Bridge tables and sequences and is granted only to the schema owner.
136
+ This lets a managed schema owner retain `BYPASSRLS` while producing a complete native
137
+ backup. Do not grant the reader to the runtime login, control members, archive members,
138
+ or another external principal. Native backup and restore require this attestation.
139
+ Gateway runtime readiness does not, so an owner-only backup fault cannot stop normal
140
+ gateway traffic.
141
+
122
142
  Use the platform's secret manager or private mounted secret files. Do not put raw
123
143
  passwords, tokens, or complete database URLs in image layers, source control, command
124
144
  arguments, deployment logs, or shared client config. Rotate each authority on its own
@@ -127,6 +147,143 @@ schedule and after any suspected exposure.
127
147
  The runtime database password must contain at least 32 characters. The bootstrap job
128
148
  fails before role creation or alteration when the mounted value is shorter.
129
149
 
150
+ ## Fly.io reference hosting
151
+
152
+ [`deploy/fly.toml`](../deploy/fly.toml) is the maintained Fly gateway contract. It has
153
+ no app name, region, or release command. The operator chooses the account, app, region,
154
+ PostgreSQL service, and public hostname. Fly terminates HTTPS and forwards to port
155
+ 8787. The service keeps at least one `shared-cpu-1x` machine with 512 MB running, checks
156
+ `GET /readyz`, and gives the Node process 30 seconds after SIGTERM to close listeners
157
+ and database pools.
158
+
159
+ Run the repository check before any Fly action:
160
+
161
+ ```bash
162
+ npm run preflight:fly -- --json
163
+ ```
164
+
165
+ The local check verifies Node, the Dockerfile, and the static Fly contract. If the app
166
+ already exists, add its name:
167
+
168
+ ```bash
169
+ npm run preflight:fly -- --app <app> --json
170
+ ```
171
+
172
+ With `--app`, the command invokes only read-only Fly commands. It observes the active
173
+ account, app status, effective config, machines, and secret names. Its JSON includes
174
+ names and check results but never environment-variable values. A passing local check
175
+ does not prove that an external resource exists or that deployment is safe.
176
+
177
+ The long-running Fly machine may receive only the restricted
178
+ `AGENT_BRIDGE_RUNTIME_DATABASE_URL` database authority and the public CA bundle in
179
+ `AGENT_BRIDGE_RUNTIME_DATABASE_CA_BASE64`. The gateway decodes the CA in memory,
180
+ removes `sslmode` and `sslrootcert` from the URL, and enables certificate and hostname
181
+ verification. It rejects the `ssl` parameter and client-certificate file parameters.
182
+ Do not give it
183
+ `AGENT_BRIDGE_DATABASE_URL`, `AGENT_BRIDGE_OPERATOR_DATABASE_URL`,
184
+ `AGENT_BRIDGE_RUNTIME_PASSWORD`, a client bearer token, or a Supabase client key. The
185
+ preflight rejects those names. Keep schema-owner, control-operator, archive, and DR
186
+ authority in separate operator jobs.
187
+
188
+ Import both runtime secrets through stdin so neither value appears in command
189
+ arguments or a temporary file:
190
+
191
+ ```bash
192
+ {
193
+ printf 'AGENT_BRIDGE_RUNTIME_DATABASE_URL=%s\n' "$(tr -d '\r\n' < runtime-url)"
194
+ printf 'AGENT_BRIDGE_RUNTIME_DATABASE_CA_BASE64=%s\n' "$(base64 < database-ca.pem | tr -d '\r\n')"
195
+ } | fly secrets import --stage --app <app>
196
+ ```
197
+
198
+ ### Operator order and gates
199
+
200
+ The following actions change external state and require an operator gate. The
201
+ repository does not perform them:
202
+
203
+ 1. Create or select the Fly app and PostgreSQL authority. Keep PostgreSQL private.
204
+ 2. Run the read-only production database preflight before any backup or migration:
205
+
206
+ ```bash
207
+ AGENT_BRIDGE_DATABASE_URL="..." npm run preflight:postgres:production -- --json --require-ssl
208
+ ```
209
+
210
+ The command opens a read-only transaction and reports only nonsecret capability
211
+ and schema facts. `authority.roles` reports migration authority. `authority.native_dr`
212
+ reports whether the source schema owner is a true superuser or has `BYPASSRLS`. A
213
+ `CREATEROLE` role can pass the migration check while failing the native DR check.
214
+ The command also checks supported server majors, migration-ledger drift, derived-role
215
+ collisions, and the legacy import shape. The PostgreSQL 15 through 18 test matrix
216
+ separately applies the full migration plan as both a superuser and a
217
+ managed-provider-style `CREATEROLE` administrator. `--require-ssl` is required for a
218
+ public database endpoint. The preflight cannot prove that later DDL will succeed. For
219
+ a session pooler, it checks the client TLS socket because PostgreSQL reports the
220
+ pooler's separate database connection.
221
+ 3. Take and verify a native DR backup when upgrading an existing authority.
222
+ 4. Run `agent-bridge migrate` once in an isolated job with
223
+ `AGENT_BRIDGE_DATABASE_URL`. Drain old gateways first when the migration notes
224
+ require it.
225
+ 5. Run [`deploy/bootstrap-runtime.sql`](../deploy/bootstrap-runtime.sql) through a
226
+ separate trusted PostgreSQL session. Supply the schema-owner connection and the
227
+ runtime password to that job only. The bootstrap is transactional and changes an
228
+ existing login only when its attributes, ownership, and membership graph already
229
+ match the recorded runtime contract. It never adopts a restored `NOLOGIN` shell.
230
+ 6. Construct the restricted runtime URL. Import it and the base64-encoded PostgreSQL CA
231
+ as the two runtime secrets shown above, then run the app-aware read-only preflight.
232
+ 7. Deploy the immutable image with `fly deploy --config deploy/fly.toml --app <app>`.
233
+ Require `/readyz` before routing clients.
234
+ 8. Run authenticated capability and status probes, then publish, pull, claim, and
235
+ settle a disposable targeted delivery.
236
+ 9. Move clients only after the gateway proof passes. Preserve every source edge store
237
+ until its outbox is empty.
238
+
239
+ App creation, database provisioning, secret writes, migration, bootstrap, deployment,
240
+ traffic changes, and client cutover are all gated external actions. The preflight is
241
+ read-only and cannot authorize any of them.
242
+
243
+ ### Fly rollback boundary
244
+
245
+ Before migration, stop a failed deployment and keep the prior image and authority. A
246
+ failed image rollout after a compatible migration can return traffic to the prior
247
+ image only if that image accepts the current schema. Never assume that an older image
248
+ can run against newer migrations.
249
+
250
+ After clients publish through the new gateway, preserve both authorities and every
251
+ edge database. Stop the cutover, drain queued outboxes, and reconcile authoritative
252
+ messages before choosing one authority. If schema or data recovery is required,
253
+ restore the verified native DR bundle into a fresh target and switch authority once.
254
+ Do not run the source and restored database as active authorities at the same time.
255
+
256
+ The manual `gateway production proof` GitHub Actions workflow runs this acceptance
257
+ proof against an existing HTTPS gateway. GitHub restricts it to `main`, and every job
258
+ uses the approval-protected `agent-bridge-production-proof` environment. Configure
259
+ `PROOF_SENDER_TOKEN`, `PROOF_RECEIVER_TOKEN`, `PROOF_HOST_SALT`, and `FLY_API_TOKEN` as
260
+ environment secrets. The sender and receiver credentials must bind the requested
261
+ workspace to the dedicated `proof-sender` and `proof-receiver` principals. The
262
+ sender credential needs `messages:write` and `messages:read`. The receiver needs
263
+ `messages:read`, `deliveries:claim`, `deliveries:settle`, and `deliveries:read`.
264
+
265
+ Run the workflow only when the Fly app has exactly one gateway machine. The cycle job
266
+ restarts that machine and queries the Fly Machines API before and after. It fails
267
+ unless the machine ID stays fixed, the runtime `instance_id` changes, the machine
268
+ returns to `started`, and `/readyz` succeeds. This changes production compute and
269
+ requires environment approval. It does not migrate the database or change secrets.
270
+
271
+ The sender first uses an unreachable loopback origin to force a leased publication
272
+ into its private SQLite outbox. It then synchronizes that row to the supplied gateway
273
+ and repeats the publication with the same idempotency key. A separate host finds the
274
+ exact message through `proof-receiver`, claims its delivery, and acknowledges it.
275
+ After the Fly machine cycle, the verifier uses a fresh instance key, edge database,
276
+ and cursor. It reads the immutable message and confirms that the prior delivery is
277
+ still `acked`.
278
+
279
+ Each phase writes an `agent-bridge-production-proof-v1` receipt. The runner rejects
280
+ unknown fields at the phase boundary and checks every receipt before use. Receipts may
281
+ record IDs, timestamps, principal and workspace labels, the gateway origin, boolean
282
+ checks, and salted SHA-256 host evidence. They never record bearer values, database
283
+ URLs, environment values, message content, lease tokens, cursor files, or edge
284
+ databases. Download all four workflow artifacts for the operator record. A passing
285
+ repository test still does not replace a completed external workflow run.
286
+
130
287
  ### Persistent data and backup
131
288
 
132
289
  A database volume or managed PostgreSQL disk keeps runtime state. It does not protect
package/llms.txt CHANGED
@@ -43,7 +43,7 @@ services can call the CLI or HTTPS API or embed the Node library without a host
43
43
  - `agent-bridge doctor`: active named checks; exits 0 ok, 2 degraded, 1 failed. Blocked outbox rows cannot be healthy.
44
44
  - agent-bridge pending
45
45
  - agent-bridge send, inbox, history, acknowledge
46
- - agent-bridge claim, extend, ack, nack, deliveries, delivery-events, cancel, requeue
46
+ - agent-bridge claim [--message-id <uuid>], extend, ack, nack, deliveries, delivery-events, cancel, requeue
47
47
  - agent-bridge sync, watch, join, presence
48
48
  - agent-bridge clients install <runtime> --identity <principal>
49
49
  - agent-bridge clients inspect <runtime> --identity <principal> --instance <stable-key> --backend-config <absolute path>
@@ -131,6 +131,7 @@ one logical authority across restore but does not fence a live clone.
131
131
 
132
132
  Compatibility: post_context, get_context, ack_context.
133
133
  Reliable messaging: send, history, claim, extend, acknowledge, negative_acknowledge, heartbeat, presence, sync.
134
+ Claim selects the next due delivery unless HTTP 2.1, MCP, or CLI supplies one messageId. The exact form atomically selects and maintains only that eligible recipient delivery. HTTP 2.0 rejects the additive field.
134
135
  Publisher delivery intent: `deliveryPolicy` mode `mailbox|leased` with `maxAttempts`, `retryBaseDelayMs`, `retryMaxDelayMs`, `retryJitterRatio`, and optional `notBefore`. MCP delivery controls: `list_deliveries`, `list_delivery_events`, `cancel_delivery`, `requeue_delivery`. Consumer `maxAttempts` and `retryPolicy` are validated but ignored for one compatibility release.
135
136
 
136
137
  Gateway MCP clients sync automatically and expose manual sync for bounded outbox replay and inbox cache refresh. Offline unacknowledged results are degraded candidates with unknown acknowledgement state, not proof that messages are unread or that no remote work exists.
@@ -4776,7 +4776,7 @@
4776
4776
  "/v2/deliveries/claim": {
4777
4777
  "post": {
4778
4778
  "operationId": "claim_delivery",
4779
- "summary": "Atomically claim the next targeted delivery.",
4779
+ "summary": "Atomically claim the next targeted delivery, or one eligible message when requested.",
4780
4780
  "security": [
4781
4781
  {
4782
4782
  "bearerAuth": []
@@ -4810,6 +4810,10 @@
4810
4810
  "leaseMs": {
4811
4811
  "type": "number"
4812
4812
  },
4813
+ "messageId": {
4814
+ "type": "string",
4815
+ "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
4816
+ },
4813
4817
  "maxAttempts": {
4814
4818
  "type": "number"
4815
4819
  }