@creatornader/agent-bridge 0.4.0 → 0.5.0
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 +73 -1
- package/README.md +73 -7
- package/ROADMAP.md +29 -0
- package/SECURITY.md +23 -0
- package/SKILL.md +5 -1
- package/dist/archive.d.ts +2 -2
- package/dist/{bridge-service-YTjGr91A.d.ts → bridge-service-CNBKdYXh.d.ts} +2 -1
- package/dist/{bridge-store-Boaj-ZbO.d.ts → bridge-store-ByJ6CaJk.d.ts} +1 -0
- package/dist/bridge.d.ts +15 -8
- package/dist/bridge.js +10 -8
- package/dist/{chunk-PDIHDBP3.js → chunk-7VJI2MXI.js} +116 -26
- package/dist/chunk-7VJI2MXI.js.map +1 -0
- package/dist/{chunk-4VB44KJU.js → chunk-AWE3DFZV.js} +3 -2
- package/dist/chunk-AWE3DFZV.js.map +1 -0
- package/dist/{chunk-IHZFI7TH.js → chunk-E4OWHGUE.js} +5 -2
- package/dist/chunk-E4OWHGUE.js.map +1 -0
- package/dist/{chunk-SYGONYMM.js → chunk-HUHG7XTB.js} +6 -5
- package/dist/chunk-HUHG7XTB.js.map +1 -0
- package/dist/{chunk-DQOZJJZ2.js → chunk-IVG4XWWS.js} +5 -5
- package/dist/{chunk-DQOZJJZ2.js.map → chunk-IVG4XWWS.js.map} +1 -1
- package/dist/{chunk-3YHVUPU2.js → chunk-P635FAN4.js} +4 -4
- package/dist/chunk-P635FAN4.js.map +1 -0
- package/dist/{chunk-6BTJE6D4.js → chunk-QNXHDOJ5.js} +56 -23
- package/dist/chunk-QNXHDOJ5.js.map +1 -0
- package/dist/{chunk-MCKKVNCE.js → chunk-QPC6JHA3.js} +5 -5
- package/dist/{chunk-SV3SVBYS.js → chunk-SN7PIVDF.js} +2 -2
- package/dist/{chunk-2DFNOLMO.js → chunk-XDNT6ZNZ.js} +9 -6
- package/dist/chunk-XDNT6ZNZ.js.map +1 -0
- package/dist/{chunk-LAHNQZDN.js → chunk-Z6JP6MYD.js} +2 -2
- package/dist/cli.js +9 -9
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +3 -3
- package/dist/client-runtime.js +6 -6
- package/dist/contracts/generate.js +1 -1
- package/dist/dr.js +3 -3
- package/dist/{gateway-BQHpV-MZ.d.ts → gateway-CW-61lvs.d.ts} +2 -2
- package/dist/gateway-main.js +5 -5
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/index.js +7 -7
- package/dist/{postgres-bridge-store-WASumj26.d.ts → postgres-bridge-store-C9KG9Z69.d.ts} +1 -1
- package/dist/postgres-production-preflight.d.ts +32 -0
- package/dist/postgres-production-preflight.js +249 -0
- package/dist/postgres-production-preflight.js.map +1 -0
- package/dist/server.js +7 -7
- package/dist/{sqlite-edge-store-Bc15DvpX.d.ts → sqlite-edge-store-CdMIOrho.d.ts} +1 -1
- package/dist/sqlite.d.ts +3 -3
- package/dist/sqlite.js +2 -2
- package/docs/architecture-v2.md +36 -5
- package/docs/deployment.md +144 -0
- package/llms.txt +2 -1
- package/openapi/agent-bridge-v2.openapi.json +5 -1
- package/package.json +5 -1
- package/schemas/agent-bridge-v2.capabilities.json +2 -1
- package/schemas/agent-bridge-v2.mcp.json +5 -1
- package/schemas/agent-bridge-v2.schema.json +4 -0
- package/sql/migrations/007_runtime_role.sql +8 -4
- package/sql/migrations/013_row_isolation.sql +71 -8
- package/sql/migrations/014_owner_control_plane.sql +124 -31
- package/sql/migrations/015_portable_archives.sql +143 -41
- package/sql/migrations/016_native_dr_fence.sql +102 -35
- package/sql/migrations/018_endpoint_migration_challenges.sql +36 -10
- package/sql/migrations/019_endpoint_migration_same_successor.sql +34 -10
- package/sql/migrations/020_managed_authority_compat.sql +698 -0
- package/sql/migrations/021_native_backup_reader.sql +421 -0
- package/sql/setup.sql +1 -3
- package/dist/chunk-2DFNOLMO.js.map +0 -1
- package/dist/chunk-3YHVUPU2.js.map +0 -1
- package/dist/chunk-4VB44KJU.js.map +0 -1
- package/dist/chunk-6BTJE6D4.js.map +0 -1
- package/dist/chunk-IHZFI7TH.js.map +0 -1
- package/dist/chunk-PDIHDBP3.js.map +0 -1
- package/dist/chunk-SYGONYMM.js.map +0 -1
- /package/dist/{chunk-MCKKVNCE.js.map → chunk-QPC6JHA3.js.map} +0 -0
- /package/dist/{chunk-SV3SVBYS.js.map → chunk-SN7PIVDF.js.map} +0 -0
- /package/dist/{chunk-LAHNQZDN.js.map → chunk-Z6JP6MYD.js.map} +0 -0
package/dist/client-runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { B as BridgeService } from './bridge-service-
|
|
2
|
-
import { a as BridgeStore } from './bridge-store-
|
|
3
|
-
import { a as EdgeDrainLease } from './sqlite-edge-store-
|
|
1
|
+
import { B as BridgeService } from './bridge-service-CNBKdYXh.js';
|
|
2
|
+
import { a as BridgeStore } from './bridge-store-ByJ6CaJk.js';
|
|
3
|
+
import { a as EdgeDrainLease } from './sqlite-edge-store-CdMIOrho.js';
|
|
4
4
|
import { ClientConfig } from './client-config.js';
|
|
5
5
|
import './bridge-domain-CBX3IVqC.js';
|
|
6
6
|
|
package/dist/client-runtime.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createClientRuntime,
|
|
4
4
|
createStore
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-QPC6JHA3.js";
|
|
6
|
+
import "./chunk-Z6JP6MYD.js";
|
|
7
|
+
import "./chunk-SN7PIVDF.js";
|
|
8
|
+
import "./chunk-IVG4XWWS.js";
|
|
9
9
|
import "./chunk-GTK4KOED.js";
|
|
10
10
|
import "./chunk-VOQIDTCJ.js";
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-AWE3DFZV.js";
|
|
12
|
+
import "./chunk-E4OWHGUE.js";
|
|
13
13
|
import "./chunk-BZLMF5O5.js";
|
|
14
14
|
export {
|
|
15
15
|
createClientRuntime,
|
package/dist/dr.js
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
verifyNativeDrBundle,
|
|
40
40
|
verifyPostgresNativeDrArtifacts,
|
|
41
41
|
writeNativeDrBundle
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-7VJI2MXI.js";
|
|
43
43
|
import {
|
|
44
44
|
EDGE_SQLITE_APPLICATION_ID,
|
|
45
45
|
EDGE_SQLITE_SCHEMA_CONTRACTS,
|
|
@@ -59,9 +59,9 @@ import {
|
|
|
59
59
|
isSupportedLocalSqliteSchemaContract,
|
|
60
60
|
sqliteSchemaContractHash,
|
|
61
61
|
verifySqliteHealth
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-IVG4XWWS.js";
|
|
63
63
|
import "./chunk-GTK4KOED.js";
|
|
64
|
-
import "./chunk-
|
|
64
|
+
import "./chunk-QNXHDOJ5.js";
|
|
65
65
|
import "./chunk-VOQIDTCJ.js";
|
|
66
66
|
import "./chunk-BZLMF5O5.js";
|
|
67
67
|
export {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as node_http from 'node:http';
|
|
2
2
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
-
import { a as BridgeStore } from './bridge-store-
|
|
3
|
+
import { a as BridgeStore } from './bridge-store-ByJ6CaJk.js';
|
|
4
4
|
import { c as BridgePrincipal } from './bridge-domain-CBX3IVqC.js';
|
|
5
|
-
import { P as PgQueryable, b as PostgresBridgeStore } from './postgres-bridge-store-
|
|
5
|
+
import { P as PgQueryable, b as PostgresBridgeStore } from './postgres-bridge-store-C9KG9Z69.js';
|
|
6
6
|
|
|
7
7
|
declare const AUTHORIZATION_SCOPES: readonly ["deliveries:claim", "deliveries:manage", "deliveries:read", "deliveries:settle", "gateway:metrics", "messages:read", "messages:write", "presence:read", "presence:write", "receipts:write", "status:read"];
|
|
8
8
|
type AuthorizationScope = typeof AUTHORIZATION_SCOPES[number];
|
package/dist/gateway-main.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PostgresBridgeStore
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XDNT6ZNZ.js";
|
|
5
5
|
import {
|
|
6
6
|
loadMigrationPlan,
|
|
7
7
|
migrationsReady,
|
|
8
8
|
runMigrations,
|
|
9
9
|
runtimeSchemaReady
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QNXHDOJ5.js";
|
|
11
11
|
import "./chunk-VOQIDTCJ.js";
|
|
12
12
|
import {
|
|
13
13
|
PostgresCredentialResolver,
|
|
14
14
|
createGateway
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-P635FAN4.js";
|
|
16
|
+
import "./chunk-AWE3DFZV.js";
|
|
17
|
+
import "./chunk-E4OWHGUE.js";
|
|
18
18
|
import "./chunk-BZLMF5O5.js";
|
|
19
19
|
|
|
20
20
|
// src/gateway-main.ts
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'node:http';
|
|
2
|
-
import './bridge-store-
|
|
3
|
-
export { G as GatewayOptions, c as createGateway } from './gateway-
|
|
2
|
+
import './bridge-store-ByJ6CaJk.js';
|
|
3
|
+
export { G as GatewayOptions, c as createGateway } from './gateway-CW-61lvs.js';
|
|
4
4
|
import './bridge-domain-CBX3IVqC.js';
|
|
5
|
-
import './postgres-bridge-store-
|
|
5
|
+
import './postgres-bridge-store-C9KG9Z69.js';
|
package/dist/gateway.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createGateway
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-P635FAN4.js";
|
|
5
|
+
import "./chunk-AWE3DFZV.js";
|
|
6
|
+
import "./chunk-E4OWHGUE.js";
|
|
7
7
|
import "./chunk-BZLMF5O5.js";
|
|
8
8
|
export {
|
|
9
9
|
createGateway
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HUHG7XTB.js";
|
|
5
5
|
import "./chunk-2EVLG3YA.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-QPC6JHA3.js";
|
|
7
|
+
import "./chunk-Z6JP6MYD.js";
|
|
8
|
+
import "./chunk-SN7PIVDF.js";
|
|
9
|
+
import "./chunk-IVG4XWWS.js";
|
|
10
10
|
import "./chunk-GTK4KOED.js";
|
|
11
11
|
import "./chunk-VOQIDTCJ.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-AWE3DFZV.js";
|
|
13
|
+
import "./chunk-E4OWHGUE.js";
|
|
14
14
|
import "./chunk-BZLMF5O5.js";
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b as BridgeMessage, c as BridgePrincipal, B as BridgeDelivery, R as RetryPolicy, a as BridgeDeliveryEvent, A as AgentPresence } from './bridge-domain-CBX3IVqC.js';
|
|
2
|
-
import { a as BridgeStore, I as InsertMessageResult, d as MessageQuery, M as MessagePage, C as ClaimOptions, c as DeliveryQuery, B as BridgeDiagnostics } from './bridge-store-
|
|
2
|
+
import { a as BridgeStore, I as InsertMessageResult, d as MessageQuery, M as MessagePage, C as ClaimOptions, c as DeliveryQuery, B as BridgeDiagnostics } from './bridge-store-ByJ6CaJk.js';
|
|
3
3
|
|
|
4
4
|
interface PgQueryable {
|
|
5
5
|
query<T extends Record<string, unknown> = Record<string, unknown>>(sql: string, values?: unknown[]): Promise<{
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface Check {
|
|
2
|
+
name: string;
|
|
3
|
+
ok: boolean;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
interface ProductionPreflightDatabase {
|
|
7
|
+
query<T extends Record<string, unknown> = Record<string, unknown>>(sql: string): Promise<{
|
|
8
|
+
rows: T[];
|
|
9
|
+
rowCount: number | null;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface PostgresProductionPreflightReport {
|
|
13
|
+
schema: "agent-bridge-postgres-production-preflight-v1";
|
|
14
|
+
ok: boolean;
|
|
15
|
+
checks: Check[];
|
|
16
|
+
observations: {
|
|
17
|
+
serverMajor: number;
|
|
18
|
+
databaseBytes: string;
|
|
19
|
+
migrationState: "uninitialized" | "upgradeable" | "current" | "invalid";
|
|
20
|
+
appliedMigrationCount: number;
|
|
21
|
+
requiredMigrationCount: number;
|
|
22
|
+
legacyTable: boolean;
|
|
23
|
+
legacyRows?: string;
|
|
24
|
+
ssl: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
declare function runPostgresProductionPreflight(db: ProductionPreflightDatabase, migrationsDirectory: string, options?: {
|
|
28
|
+
requireSsl?: boolean;
|
|
29
|
+
clientTransportSsl?: boolean;
|
|
30
|
+
}): Promise<PostgresProductionPreflightReport>;
|
|
31
|
+
|
|
32
|
+
export { type PostgresProductionPreflightReport, runPostgresProductionPreflight };
|
|
@@ -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-
|
|
7
|
+
} from "./chunk-HUHG7XTB.js";
|
|
8
8
|
import "./chunk-2EVLG3YA.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
12
|
-
import "./chunk-
|
|
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-
|
|
16
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
3
|
-
import { e as EdgeMigrationGateState, S as SQLiteEdgeStore, a as EdgeDrainLease, P as PendingMessage } from './sqlite-edge-store-
|
|
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-
|
|
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-
|
|
14
|
+
} from "./chunk-SN7PIVDF.js";
|
|
15
15
|
import {
|
|
16
16
|
SQLiteBridgeStore
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-IVG4XWWS.js";
|
|
18
18
|
import "./chunk-GTK4KOED.js";
|
|
19
19
|
import "./chunk-VOQIDTCJ.js";
|
|
20
20
|
import "./chunk-BZLMF5O5.js";
|
package/docs/architecture-v2.md
CHANGED
|
@@ -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`.
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
|