From e94759d8804e98c75e5e4349c00b94b7f65c72e0 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:17:45 +0800 Subject: [PATCH 1/5] feat(postgresql): add service admission and identity rotation Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../postgresql_authority_service.ts | 212 +++++++++++++ .../postgresql_authority_store.ts | 133 +++++++- .../postgresql_authority_service.test.ts | 285 ++++++++++++++++++ .../postgresql_authority_service_fixture.ts | 32 ++ .../postgresql_authority_service_v0.json | 42 +++ 5 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 loopx/control_plane/coordination/postgresql_authority_service.ts create mode 100644 tests/control_plane_ts/postgresql_authority_service.test.ts create mode 100644 tests/control_plane_ts/postgresql_authority_service_fixture.ts create mode 100644 tests/fixtures/control_plane/postgresql_authority_service_v0.json diff --git a/loopx/control_plane/coordination/postgresql_authority_service.ts b/loopx/control_plane/coordination/postgresql_authority_service.ts new file mode 100644 index 0000000000..43305b8cb5 --- /dev/null +++ b/loopx/control_plane/coordination/postgresql_authority_service.ts @@ -0,0 +1,212 @@ +import type { AuthorityStore } from "./authority_store.ts"; +import { + POSTGRESQL_STORE_IDENTITY_PATTERN, + PostgreSqlAuthorityStore, + type PostgreSqlAuthorityDatabase, + type PostgreSqlAuthorityStoreOptions, +} from "./postgresql_authority_store.ts"; +import { requireAuthorityStoreId } from "./authority_store_codec.ts"; + +/** A principal already verified by the service's authentication layer. */ +export interface PostgreSqlAuthenticatedPrincipal { + principal_id: string; +} + +export type PostgreSqlPrincipalAuthenticationResult = + | { status: "authenticated"; principal: PostgreSqlAuthenticatedPrincipal } + | { status: "rejected"; reason_code: string; reason: string }; + +export type PostgreSqlTenantAuthorizationResult = + | { status: "allowed" } + | { status: "denied"; reason_code: string; reason: string }; + +/** + * The service owns authentication and authorization. The provider only sees + * an already verified principal id and the authorized tenant binding. + */ +export interface PostgreSqlAuthorityServiceDependencies { + database: PostgreSqlAuthorityDatabase; + authenticatePrincipal( + credential: unknown, + ): Promise | PostgreSqlPrincipalAuthenticationResult; + authorizeTenant( + principalId: string, + tenantId: string, + ): Promise | PostgreSqlTenantAuthorizationResult; + max_commit_bytes?: PostgreSqlAuthorityStoreOptions["max_commit_bytes"]; +} + +export interface PostgreSqlAuthorityServiceOpenRequest { + /** Opaque transport credential; never persisted or passed to PostgreSQL. */ + credential: unknown; + tenant_id: string; + goal_id: string; + store_identity: string; +} + +export type PostgreSqlAuthorityServiceOpenResult = + | { + status: "opened"; + provider: "postgresql"; + principal_id: string; + tenant_id: string; + goal_id: string; + store_identity: string; + store: AuthorityStore; + } + | { + status: "rejected"; + reason_code: + | "invalid_service_request" + | "principal_unauthenticated" + | "principal_verification_unavailable" + | "tenant_unauthorized" + | "tenant_authorization_unavailable" + | "store_identity_unavailable" + | "store_identity_mismatch"; + reason: string; + principal_id?: string; + tenant_id?: string; + goal_id?: string; + }; + +type PostgreSqlAuthorityServiceRejected = Extract< + PostgreSqlAuthorityServiceOpenResult, + {status: "rejected"} +>; + +function rejected( + reasonCode: PostgreSqlAuthorityServiceRejected["reason_code"], + reason: string, + facts: Partial = {}, +): PostgreSqlAuthorityServiceRejected { + return {status: "rejected", reason_code: reasonCode, reason, ...facts}; +} + +function validIdentity(value: string): boolean { + return POSTGRESQL_STORE_IDENTITY_PATTERN.test(value); +} + +/** + * Service-owned PostgreSQL admission. This is intentionally an in-process + * boundary: transport authentication, pool lifecycle, and credentials stay + * outside the provider-neutral AuthorityStore contract. + */ +export class PostgreSqlAuthorityService { + readonly #dependencies: PostgreSqlAuthorityServiceDependencies; + + constructor(dependencies: PostgreSqlAuthorityServiceDependencies) { + this.#dependencies = dependencies; + } + + async openStore( + request: PostgreSqlAuthorityServiceOpenRequest, + ): Promise { + let tenantId: string; + let goalId: string; + if ( + typeof request !== "object" || request === null || + typeof request.tenant_id !== "string" || + typeof request.goal_id !== "string" || + typeof request.store_identity !== "string" + ) { + return rejected("invalid_service_request", "PostgreSQL service request is invalid"); + } + try { + tenantId = requireAuthorityStoreId(request.tenant_id, "tenant id"); + goalId = requireAuthorityStoreId(request.goal_id, "goal id"); + } catch (error) { + return rejected( + "invalid_service_request", + error instanceof Error ? error.message : "PostgreSQL service request is invalid", + ); + } + if (!validIdentity(request.store_identity)) { + return rejected( + "invalid_service_request", + "PostgreSQL store identity must match postgresql:<32 lowercase hex>", + {tenant_id: tenantId, goal_id: goalId}, + ); + } + + let authentication: PostgreSqlPrincipalAuthenticationResult; + try { + authentication = await this.#dependencies.authenticatePrincipal(request.credential); + } catch { + return rejected( + "principal_verification_unavailable", + "PostgreSQL service could not verify the principal", + {tenant_id: tenantId, goal_id: goalId}, + ); + } + if (authentication.status !== "authenticated") { + return rejected( + "principal_unauthenticated", + "PostgreSQL service principal authentication was rejected", + {tenant_id: tenantId, goal_id: goalId}, + ); + } + + let principalId: string; + try { + principalId = requireAuthorityStoreId( + authentication.principal.principal_id, + "principal id", + ); + } catch { + return rejected( + "principal_unauthenticated", + "PostgreSQL service returned an invalid authenticated principal", + {tenant_id: tenantId, goal_id: goalId}, + ); + } + + let tenantDecision: PostgreSqlTenantAuthorizationResult; + try { + tenantDecision = await this.#dependencies.authorizeTenant(principalId, tenantId); + } catch { + return rejected( + "tenant_authorization_unavailable", + "PostgreSQL service could not authorize the tenant", + {principal_id: principalId, tenant_id: tenantId, goal_id: goalId}, + ); + } + if (tenantDecision.status !== "allowed") { + return rejected( + "tenant_unauthorized", + "PostgreSQL principal is not authorized for the requested tenant", + {principal_id: principalId, tenant_id: tenantId, goal_id: goalId}, + ); + } + + const store = new PostgreSqlAuthorityStore(this.#dependencies.database, { + tenant_id: tenantId, + goal_id: goalId, + max_commit_bytes: this.#dependencies.max_commit_bytes, + }); + const identity = await store.storeIdentity(); + if (identity.status !== "available") { + return rejected( + "store_identity_unavailable", + "PostgreSQL service could not verify the database incarnation", + {principal_id: principalId, tenant_id: tenantId, goal_id: goalId}, + ); + } + if (identity.store_identity !== request.store_identity) { + return rejected( + "store_identity_mismatch", + "PostgreSQL database incarnation does not match the requested binding", + {principal_id: principalId, tenant_id: tenantId, goal_id: goalId}, + ); + } + return { + status: "opened", + provider: "postgresql", + principal_id: principalId, + tenant_id: tenantId, + goal_id: goalId, + store_identity: identity.store_identity, + store, + }; + } +} diff --git a/loopx/control_plane/coordination/postgresql_authority_store.ts b/loopx/control_plane/coordination/postgresql_authority_store.ts index a14722a258..8b9ee6fa04 100644 --- a/loopx/control_plane/coordination/postgresql_authority_store.ts +++ b/loopx/control_plane/coordination/postgresql_authority_store.ts @@ -20,9 +20,9 @@ import { requireAuthorityStoreId, } from "./authority_store_codec.ts"; -const POSTGRESQL_STORE_IDENTITY_PATTERN = /^postgresql:[0-9a-f]{32}$/; +export const POSTGRESQL_STORE_IDENTITY_PATTERN = /^postgresql:[0-9a-f]{32}$/; const POSTGRESQL_PROVIDER_REVISION_PATTERN = /^postgresql:([0-9a-f]{32}):([1-9]\d*)$/; -const POSTGRESQL_SCHEMA_VERSION = "loopx_postgresql_authority_store_v0"; +export const POSTGRESQL_SCHEMA_VERSION = "loopx_postgresql_authority_store_v0"; export const DEFAULT_POSTGRESQL_MAX_COMMIT_BYTES = 16 * 1024 * 1024; /** @@ -410,6 +410,135 @@ export async function installPostgreSqlAuthorityStoreSchema( } } +export type PostgreSqlAuthorityIdentityRotationResult = + | { + status: "rotated"; + previous_store_identity: string; + store_identity: string; + } + | { + status: "ambiguous"; + reason_code: "store_identity_rotation_outcome_unknown"; + reason: string; + } + | { + status: "failed"; + reason_code: + | "invalid_store_identity" + | "store_identity_unchanged" + | "provider_connection_unavailable" + | "store_identity_mismatch" + | "provider_protocol_violation" + | "provider_transaction_failed"; + reason: string; + }; + +class PostgreSqlAuthorityIdentityRotationRejected extends Error { + readonly reasonCode: "store_identity_mismatch"; + + constructor(reason: string) { + super(reason); + this.name = "PostgreSqlAuthorityIdentityRotationRejected"; + this.reasonCode = "store_identity_mismatch"; + } +} + +/** + * Rotate the service-managed database incarnation after a restore. Revision + * tokens minted before the rotation become unusable because their opaque + * identity no longer matches; no Goal or operation state is rewritten. + */ +export async function rotatePostgreSqlAuthorityStoreIdentity( + database: PostgreSqlAuthorityDatabase, + expectedStoreIdentity: string, + nextStoreIdentity: string, +): Promise { + if (!POSTGRESQL_STORE_IDENTITY_PATTERN.test(expectedStoreIdentity) || + !POSTGRESQL_STORE_IDENTITY_PATTERN.test(nextStoreIdentity)) { + return { + status: "failed", + reason_code: "invalid_store_identity", + reason: "PostgreSQL store identities must match postgresql:<32 lowercase hex>", + }; + } + if (expectedStoreIdentity === nextStoreIdentity) { + return { + status: "failed", + reason_code: "store_identity_unchanged", + reason: "PostgreSQL store identity rotation requires a new incarnation", + }; + } + + let connection: PostgreSqlAuthorityConnection; + try { + connection = await database.connect(); + } catch { + return { + status: "failed", + reason_code: "provider_connection_unavailable", + reason: "PostgreSQL connection was unavailable before identity rotation", + }; + } + + let commitStarted = false; + let releaseError: Error | undefined; + try { + await connection.query("BEGIN"); + const metadata = oneRow(await connection.query( + `SELECT schema_version, store_identity + FROM loopx_control_plane.authority_store_metadata + WHERE singleton = TRUE + FOR UPDATE`, + ), "PostgreSQL store metadata"); + if ( + metadata === null || metadata.schema_version !== POSTGRESQL_SCHEMA_VERSION || + !POSTGRESQL_STORE_IDENTITY_PATTERN.test(String(metadata.store_identity)) || + metadata.store_identity !== expectedStoreIdentity + ) { + throw new PostgreSqlAuthorityIdentityRotationRejected( + "PostgreSQL store identity does not match the expected database incarnation", + ); + } + await connection.query( + `UPDATE loopx_control_plane.authority_store_metadata + SET store_identity = $1 + WHERE singleton = TRUE`, + [nextStoreIdentity], + ); + commitStarted = true; + await connection.query("COMMIT"); + return { + status: "rotated", + previous_store_identity: expectedStoreIdentity, + store_identity: nextStoreIdentity, + }; + } catch (error) { + if (commitStarted) { + releaseError = asError(error); + return { + status: "ambiguous", + reason_code: "store_identity_rotation_outcome_unknown", + reason: "PostgreSQL identity rotation outcome is unknown; read store metadata before retrying", + }; + } + releaseError = (await rollback(connection)) ?? undefined; + if (error instanceof PostgreSqlAuthorityIdentityRotationRejected) { + return {status: "failed", reason_code: error.reasonCode, reason: error.message}; + } + return { + status: "failed", + reason_code: error instanceof AuthorityStoreProtocolError + ? "provider_protocol_violation" + : "provider_transaction_failed", + reason: error instanceof AuthorityStoreProtocolError + ? error.message + : "PostgreSQL identity rotation failed before COMMIT", + }; + } finally { + await connection.release(releaseError); + } +} + /** PostgreSQL Stage 2B store; domain decisions remain in LoopX authority. */ export class PostgreSqlAuthorityStore implements AuthorityStore { readonly providerKind = "postgresql" as const; diff --git a/tests/control_plane_ts/postgresql_authority_service.test.ts b/tests/control_plane_ts/postgresql_authority_service.test.ts new file mode 100644 index 0000000000..232f85ed97 --- /dev/null +++ b/tests/control_plane_ts/postgresql_authority_service.test.ts @@ -0,0 +1,285 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + POSTGRESQL_SCHEMA_VERSION, + POSTGRESQL_STORE_IDENTITY_PATTERN, + rotatePostgreSqlAuthorityStoreIdentity, + type PostgreSqlAuthorityConnection, + type PostgreSqlAuthorityDatabase, +} from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; +import { + PostgreSqlAuthorityService, + type PostgreSqlPrincipalAuthenticationResult, + type PostgreSqlTenantAuthorizationResult, +} from "../../loopx/control_plane/coordination/postgresql_authority_service.ts"; +import { + POSTGRESQL_AUTHORITY_SERVICE_FIXTURE_SCHEMA, + postgresqlAuthorityServiceFixture, +} from "./postgresql_authority_service_fixture.ts"; + +const STORE_IDENTITY = `postgresql:${"a".repeat(32)}`; +const OTHER_STORE_IDENTITY = `postgresql:${"b".repeat(32)}`; +const TENANT_ID = "tenant-service-fixture"; +const GOAL_ID = "goal-service-fixture"; + +function metadataDatabase( + storeIdentity: string, + options: {onConnect?: () => void; onQuery?: (text: string) => void} = {}, +): PostgreSqlAuthorityDatabase { + return { + connect: async () => { + options.onConnect?.(); + const connection: PostgreSqlAuthorityConnection = { + query: async (text) => { + options.onQuery?.(text); + if (text.includes("authority_store_metadata")) { + return { + rows: [{ + schema_version: POSTGRESQL_SCHEMA_VERSION, + store_identity: storeIdentity, + }], + rowCount: 1, + }; + } + throw new Error(`unexpected PostgreSQL service query: ${text}`); + }, + release: () => {}, + }; + return connection; + }, + }; +} + +function principalResult( + principalId = "principal-service-fixture", +): PostgreSqlPrincipalAuthenticationResult { + return {status: "authenticated", principal: {principal_id: principalId}}; +} + +function allowedTenant(): PostgreSqlTenantAuthorizationResult { + return {status: "allowed"}; +} + +test("PostgreSQL service fixture is public-safe and versioned", () => { + assert.equal( + postgresqlAuthorityServiceFixture.schema_version, + POSTGRESQL_AUTHORITY_SERVICE_FIXTURE_SCHEMA, + ); + assert.equal(postgresqlAuthorityServiceFixture.source_authority, "postgresql_v0"); + assert.equal(postgresqlAuthorityServiceFixture.credential_persistence, "forbidden"); + assert.equal(postgresqlAuthorityServiceFixture.agent_database_access, "forbidden"); + assert.deepEqual( + postgresqlAuthorityServiceFixture.principal_cases.map(item => item.id), + ["authenticated-tenant", "unauthenticated", "tenant-denied", "identity-drift"], + ); +}); + +test("PostgreSQL service rejects malformed requests before authentication", async () => { + let authenticationCalls = 0; + const service = new PostgreSqlAuthorityService({ + database: metadataDatabase(STORE_IDENTITY), + authenticatePrincipal: () => { + authenticationCalls += 1; + return principalResult(); + }, + authorizeTenant: allowedTenant, + }); + + const result = await service.openStore({ + credential: {secret: "opaque"}, + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: "postgresql:not-an-incarnation", + }); + assert.equal(result.status, "rejected"); + if (result.status === "rejected") assert.equal(result.reason_code, "invalid_service_request"); + assert.equal(authenticationCalls, 0); +}); + +test("PostgreSQL service authenticates and authorizes before opening the provider", async () => { + let connectionAttempts = 0; + let seenCredential: unknown; + let authorizedPrincipal: string | null = null; + let authorizedTenant: string | null = null; + const credential = {kind: "opaque", token: "never-persist"}; + const service = new PostgreSqlAuthorityService({ + database: metadataDatabase(STORE_IDENTITY, {onConnect: () => connectionAttempts += 1}), + authenticatePrincipal: received => { + seenCredential = received; + return principalResult(); + }, + authorizeTenant: (principalId, tenantId) => { + authorizedPrincipal = principalId; + authorizedTenant = tenantId; + return allowedTenant(); + }, + }); + + const result = await service.openStore({ + credential, + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: STORE_IDENTITY, + }); + assert.equal(result.status, "opened"); + if (result.status !== "opened") return; + assert.equal(result.provider, "postgresql"); + assert.equal(result.principal_id, "principal-service-fixture"); + assert.equal(result.store_identity, STORE_IDENTITY); + assert.strictEqual(seenCredential, credential); + assert.equal(authorizedPrincipal, "principal-service-fixture"); + assert.equal(authorizedTenant, TENANT_ID); + assert.equal(connectionAttempts, 1); +}); + +test("PostgreSQL service fails closed for authentication, tenant, and identity drift", async () => { + let connectionAttempts = 0; + const database = metadataDatabase(STORE_IDENTITY, {onConnect: () => connectionAttempts += 1}); + + const unauthenticated = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => ({ + status: "rejected", + reason_code: "credential_invalid", + reason: "synthetic credential is invalid", + }), + authorizeTenant: allowedTenant, + }); + const rejectedPrincipal = await unauthenticated.openStore({ + credential: "invalid", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: STORE_IDENTITY, + }); + assert.deepEqual( + rejectedPrincipal, + { + status: "rejected", + reason_code: "principal_unauthenticated", + reason: "PostgreSQL service principal authentication was rejected", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + }, + ); + assert.equal(connectionAttempts, 0); + + const denied = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => principalResult(), + authorizeTenant: () => ({ + status: "denied", + reason_code: "tenant_not_granted", + reason: "synthetic tenant policy denied the request", + }), + }); + const rejectedTenant = await denied.openStore({ + credential: "valid", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: STORE_IDENTITY, + }); + assert.equal(rejectedTenant.status, "rejected"); + if (rejectedTenant.status === "rejected") { + assert.equal(rejectedTenant.reason_code, "tenant_unauthorized"); + assert.equal(rejectedTenant.principal_id, "principal-service-fixture"); + } + assert.equal(connectionAttempts, 0); + + const drifted = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => principalResult(), + authorizeTenant: allowedTenant, + }); + const rejectedIdentity = await drifted.openStore({ + credential: "valid", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: OTHER_STORE_IDENTITY, + }); + assert.equal(rejectedIdentity.status, "rejected"); + if (rejectedIdentity.status === "rejected") { + assert.equal(rejectedIdentity.reason_code, "store_identity_mismatch"); + assert.equal(rejectedIdentity.principal_id, "principal-service-fixture"); + } + assert.equal(connectionAttempts, 1); +}); + +test("PostgreSQL service treats provider metadata failures as unavailable", async () => { + const service = new PostgreSqlAuthorityService({ + database: { + connect: async () => { + throw new Error("synthetic unavailable provider"); + }, + }, + authenticatePrincipal: () => principalResult(), + authorizeTenant: allowedTenant, + }); + const result = await service.openStore({ + credential: "valid", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: STORE_IDENTITY, + }); + assert.equal(result.status, "rejected"); + if (result.status === "rejected") assert.equal(result.reason_code, "store_identity_unavailable"); +}); + +test("PostgreSQL identity rotation reports a lost COMMIT response as ambiguous", async () => { + let releasedWith: Error | undefined; + const database: PostgreSqlAuthorityDatabase = { + connect: async () => { + const connection: PostgreSqlAuthorityConnection = { + query: async text => { + if (text === "BEGIN" || text.includes("UPDATE loopx_control_plane.authority_store_metadata")) { + return {rows: [], rowCount: 1}; + } + if (text.includes("authority_store_metadata")) { + return { + rows: [{ + schema_version: POSTGRESQL_SCHEMA_VERSION, + store_identity: STORE_IDENTITY, + }], + rowCount: 1, + }; + } + if (text === "COMMIT") throw new Error("synthetic response loss after COMMIT"); + throw new Error(`unexpected PostgreSQL rotation query: ${text}`); + }, + release: error => { + releasedWith = error; + }, + }; + return connection; + }, + }; + + const result = await rotatePostgreSqlAuthorityStoreIdentity( + database, + STORE_IDENTITY, + OTHER_STORE_IDENTITY, + ); + assert.deepEqual(result, { + status: "ambiguous", + reason_code: "store_identity_rotation_outcome_unknown", + reason: "PostgreSQL identity rotation outcome is unknown; read store metadata before retrying", + }); + assert.match(releasedWith?.message ?? "", /response loss/); +}); + +test("PostgreSQL service identity format stays provider-specific", () => { + assert.equal(POSTGRESQL_STORE_IDENTITY_PATTERN.test(STORE_IDENTITY), true); + assert.equal(POSTGRESQL_STORE_IDENTITY_PATTERN.test(OTHER_STORE_IDENTITY), true); + assert.equal(POSTGRESQL_STORE_IDENTITY_PATTERN.test("sqlite:abc"), false); +}); + +if (process.env.LOOPX_TEST_POSTGRES_URL) { + test("PostgreSQL service real-path integration (set LOOPX_TEST_POSTGRES_URL)", async () => { + // The disposable PostgreSQL integration is intentionally kept in the + // provider integration suite; this test documents the required real-path + // command without sharing mutable singleton metadata with that suite. + assert.match(process.env.LOOPX_TEST_POSTGRES_URL, /^postgres(?:ql)?:\/\//); + }); +} else { + test("PostgreSQL service real-path integration (set LOOPX_TEST_POSTGRES_URL)", {skip: true}, () => {}); +} diff --git a/tests/control_plane_ts/postgresql_authority_service_fixture.ts b/tests/control_plane_ts/postgresql_authority_service_fixture.ts new file mode 100644 index 0000000000..3353b0b6a4 --- /dev/null +++ b/tests/control_plane_ts/postgresql_authority_service_fixture.ts @@ -0,0 +1,32 @@ +import {readFileSync} from "node:fs"; + +const envelope = JSON.parse(readFileSync(new URL( + "../fixtures/control_plane/postgresql_authority_service_v0.json", + import.meta.url, +), "utf8")) as PostgreSqlAuthorityServiceFixture; + +export const POSTGRESQL_AUTHORITY_SERVICE_FIXTURE_SCHEMA = + "loopx_postgresql_authority_service_fixture_v0"; + +export interface PostgreSqlAuthorityServiceFixtureCase { + readonly id: string; + readonly credential_kind?: "opaque"; + readonly expected: "opened" | "rejected" | "rotated" | "failed"; + readonly reason_code?: string; +} + +export interface PostgreSqlAuthorityServiceFixture { + readonly schema_version: string; + readonly source_authority: "postgresql_v0"; + readonly principal_cases: readonly PostgreSqlAuthorityServiceFixtureCase[]; + readonly rotation_cases: readonly PostgreSqlAuthorityServiceFixtureCase[]; + readonly credential_persistence: "forbidden"; + readonly agent_database_access: "forbidden"; +} + +if (envelope.schema_version !== POSTGRESQL_AUTHORITY_SERVICE_FIXTURE_SCHEMA) { + throw new Error("PostgreSQL authority service fixture schema version is invalid"); +} + +export const postgresqlAuthorityServiceFixture: PostgreSqlAuthorityServiceFixture = + structuredClone(envelope); diff --git a/tests/fixtures/control_plane/postgresql_authority_service_v0.json b/tests/fixtures/control_plane/postgresql_authority_service_v0.json new file mode 100644 index 0000000000..b55473dd68 --- /dev/null +++ b/tests/fixtures/control_plane/postgresql_authority_service_v0.json @@ -0,0 +1,42 @@ +{ + "schema_version": "loopx_postgresql_authority_service_fixture_v0", + "source_authority": "postgresql_v0", + "principal_cases": [ + { + "id": "authenticated-tenant", + "credential_kind": "opaque", + "expected": "opened" + }, + { + "id": "unauthenticated", + "credential_kind": "opaque", + "expected": "rejected", + "reason_code": "principal_unauthenticated" + }, + { + "id": "tenant-denied", + "credential_kind": "opaque", + "expected": "rejected", + "reason_code": "tenant_unauthorized" + }, + { + "id": "identity-drift", + "credential_kind": "opaque", + "expected": "rejected", + "reason_code": "store_identity_mismatch" + } + ], + "rotation_cases": [ + { + "id": "new-incarnation", + "expected": "rotated" + }, + { + "id": "wrong-incarnation", + "expected": "failed", + "reason_code": "store_identity_mismatch" + } + ], + "credential_persistence": "forbidden", + "agent_database_access": "forbidden" +} From 18658fac7c5ee715fb7dbbaf66686345d208ed8c Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:17:49 +0800 Subject: [PATCH 2/5] test(postgresql): exercise service against disposable server Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- package.json | 1 + ...esql_authority_service.integration.test.ts | 155 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tests/control_plane_ts/postgresql_authority_service.integration.test.ts diff --git a/package.json b/package.json index eab9485a97..392ce67e99 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "scripts": { "test:control-plane": "node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/*.test.ts", + "test:postgresql-authority-service": "node --no-warnings --experimental-strip-types --test tests/control_plane_ts/postgresql_authority_service.test.ts tests/control_plane_ts/postgresql_authority_service.integration.test.ts", "test:control-plane:coverage": "c8 --all --include=loopx/control_plane/**/*.ts --exclude=loopx/control_plane/**/*.generated.ts --reporter=lcov --reporter=text --reports-dir=coverage/control-plane node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/*.test.ts", "test:dashboard:coverage": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --sourceMap --inlineSources --outDir apps/presentation/dashboard/node_modules/.cache/loopx-agent-family apps/presentation/dashboard/src/features/personal-workspace/agent-family.ts && c8 --exclude-node-modules=false --include=apps/presentation/dashboard/node_modules/.cache/loopx-agent-family/agent-family.js --reporter=lcov --reporter=text --reports-dir=coverage/dashboard node apps/presentation/dashboard/src/features/personal-workspace/agent-family.test.mjs", "test:postgresql-authority-store": "node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/control_plane_ts/postgresql_authority_store.integration.test.ts", diff --git a/tests/control_plane_ts/postgresql_authority_service.integration.test.ts b/tests/control_plane_ts/postgresql_authority_service.integration.test.ts new file mode 100644 index 0000000000..9534c0fdc5 --- /dev/null +++ b/tests/control_plane_ts/postgresql_authority_service.integration.test.ts @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import {randomUUID} from "node:crypto"; +import test from "node:test"; +import {Pool, type PoolClient} from "pg"; + +import { + installPostgreSqlAuthorityStoreSchema, + type PostgreSqlAuthorityConnection, + type PostgreSqlAuthorityDatabase, + PostgreSqlAuthorityStore, + rotatePostgreSqlAuthorityStoreIdentity, +} from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; +import { + PostgreSqlAuthorityService, +} from "../../loopx/control_plane/coordination/postgresql_authority_service.ts"; +import {authorityStoreCommitFixture as commit} from "./authority_store_conformance.ts"; + +const connectionString = process.env.LOOPX_TEST_POSTGRES_SERVICE_URL; +const pool = connectionString ? new Pool({connectionString, max: 4}) : null; +const STORE_IDENTITY = `postgresql:${"c".repeat(32)}`; +const NEXT_STORE_IDENTITY = `postgresql:${"d".repeat(32)}`; + +function databaseFromPool(value: Pool): PostgreSqlAuthorityDatabase { + return { + connect: async () => { + const client: PoolClient = await value.connect(); + const connection: PostgreSqlAuthorityConnection = { + query: async (text, values) => + await client.query(text, values ? [...values] : undefined), + release: (error) => client.release(error), + }; + return connection; + }, + }; +} + +async function cleanScope(tenantId: string, goalId: string): Promise { + if (!pool) return; + await pool.query( + `DELETE FROM loopx_control_plane.authority_heads + WHERE tenant_id = $1 AND goal_id = $2`, + [tenantId, goalId], + ); +} + +if (pool) { + const database = databaseFromPool(pool); + const installed = installPostgreSqlAuthorityStoreSchema(database, STORE_IDENTITY); + + test("PostgreSQL service admits an authorized tenant and rotates a restored incarnation", async t => { + await installed; + const tenantId = `tenant-${randomUUID()}`; + const goalId = `goal-${randomUUID()}`; + t.after(() => cleanScope(tenantId, goalId)); + + const service = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: credential => + credential === "fixture-credential" + ? {status: "authenticated", principal: {principal_id: "principal-fixture"}} + : { + status: "rejected", + reason_code: "credential_invalid", + reason: "fixture credential rejected", + }, + authorizeTenant: (principalId, requestedTenant) => + principalId === "principal-fixture" && requestedTenant === tenantId + ? {status: "allowed"} + : { + status: "denied", + reason_code: "tenant_not_granted", + reason: "fixture tenant policy rejected", + }, + }); + + const opened = await service.openStore({ + credential: "fixture-credential", + tenant_id: tenantId, + goal_id: goalId, + store_identity: STORE_IDENTITY, + }); + assert.equal(opened.status, "opened", JSON.stringify(opened)); + if (opened.status !== "opened") return; + const first = await opened.store.commitAuthority(commit(null, "service-operation", 1, 1)); + assert.equal(first.status, "applied", JSON.stringify(first)); + if (first.status !== "applied") return; + + const rotated = await rotatePostgreSqlAuthorityStoreIdentity( + database, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + assert.deepEqual(rotated, { + status: "rotated", + previous_store_identity: STORE_IDENTITY, + store_identity: NEXT_STORE_IDENTITY, + }); + + const stale = await opened.store.commitAuthority( + commit(first.provider_revision, "stale-after-restore", 2, 2), + ); + assert.equal(stale.status, "conflict", JSON.stringify(stale)); + if (stale.status === "conflict") { + assert.equal(stale.conflict_kind, "provider_revision_mismatch"); + assert.equal(stale.current_provider_revision, `${NEXT_STORE_IDENTITY}:1`); + } + assert.equal((await opened.store.readReceipt("service-operation")).status, "found"); + + const oldBinding = await service.openStore({ + credential: "fixture-credential", + tenant_id: tenantId, + goal_id: goalId, + store_identity: STORE_IDENTITY, + }); + assert.equal(oldBinding.status, "rejected"); + if (oldBinding.status === "rejected") { + assert.equal(oldBinding.reason_code, "store_identity_mismatch"); + } + + const newBinding = await service.openStore({ + credential: "fixture-credential", + tenant_id: tenantId, + goal_id: goalId, + store_identity: NEXT_STORE_IDENTITY, + }); + assert.equal(newBinding.status, "opened", JSON.stringify(newBinding)); + if (newBinding.status === "opened") { + const loaded = await newBinding.store.loadAuthority(); + assert.equal(loaded.status, "loaded", JSON.stringify(loaded)); + if (loaded.status === "loaded") { + assert.equal(loaded.provider_revision, `${NEXT_STORE_IDENTITY}:1`); + } + } + + const wrongExpected = await rotatePostgreSqlAuthorityStoreIdentity( + database, + STORE_IDENTITY, + `postgresql:${"e".repeat(32)}`, + ); + assert.equal(wrongExpected.status, "failed"); + if (wrongExpected.status === "failed") { + assert.equal(wrongExpected.reason_code, "store_identity_mismatch"); + } + }); +} else { + test( + "PostgreSQL service real-path integration (set LOOPX_TEST_POSTGRES_SERVICE_URL)", + {skip: true}, + () => {}, + ); +} + +test.after(async () => { + await pool?.end(); +}); From 98e2ede0313ce49f2b1ea2d514436faa794a7f25 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:17:54 +0800 Subject: [PATCH 3/5] docs(postgresql): describe service admission contract Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- docs/reference/README.md | 2 + .../postgresql-authority-service-v0.md | 67 +++++++++++++++++++ .../postgresql-authority-service-v0.zh-CN.md | 61 +++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 docs/reference/postgresql-authority-service-v0.md create mode 100644 docs/reference/postgresql-authority-service-v0.zh-CN.md diff --git a/docs/reference/README.md b/docs/reference/README.md index 97d0482e25..25f9284c68 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -11,6 +11,8 @@ Current groups: registration, extension packaging, readiness, and lifecycle boundaries. - [Project skill delivery](../../loopx/capabilities/project_skill_delivery/README.md): release-owned, project-local skill discovery and managed-copy lifecycle. +- [PostgreSQL authority service admission v0](postgresql-authority-service-v0.md): opt-in + authentication, tenant authorization, and restore-incarnation rotation. High-traffic read paths: diff --git a/docs/reference/postgresql-authority-service-v0.md b/docs/reference/postgresql-authority-service-v0.md new file mode 100644 index 0000000000..4c3af0c35c --- /dev/null +++ b/docs/reference/postgresql-authority-service-v0.md @@ -0,0 +1,67 @@ +# PostgreSQL authority service admission v0 + +This document defines the service-owned admission boundary for the switchable +PostgreSQL authority provider. It is an opt-in Stage 2B seam: it does not make +PostgreSQL the default, change file/SQLite selection, add a runtime caller, or +claim promotion readiness. + +## Boundary + +`PostgreSqlAuthorityService` receives an opaque transport credential, a tenant, +a goal, and the expected database-incarnation identity. It calls injected +authentication and tenant-authorization functions before opening the provider. +The credential is never persisted, passed to PostgreSQL, or exposed through the +provider-neutral `AuthorityStore` contract. A denied or unverifiable principal +fails closed before a database connection is opened. + +The service returns one of these typed outcomes: + +| Outcome | Meaning | +| --- | --- | +| `opened` | The principal is authenticated, authorized for the tenant, and the database identity matches the requested binding. | +| `principal_unauthenticated` | Authentication rejected the opaque credential or returned an invalid principal. | +| `principal_verification_unavailable` | Authentication could not be completed. | +| `tenant_unauthorized` | The authenticated principal is not allowed to use the tenant. | +| `tenant_authorization_unavailable` | Tenant policy could not be evaluated. | +| `store_identity_unavailable` | Provider metadata could not be read. | +| `store_identity_mismatch` | The requested incarnation is not the database's current incarnation. | + +The service is intentionally in-process. A deployment supplies its own +transport, credential verifier, tenant policy, connection pool, and secret +handling. This module does not grant network access, actor ownership, lease +ownership, cross-host synchronization, or promotion authority. + +## Restore-incarnation rotation + +`rotatePostgreSqlAuthorityStoreIdentity` is an administrative operation owned by +the authenticated service deployment. It locks the singleton metadata row, +verifies the expected identity, and atomically writes a newly minted +`postgresql:<32 lowercase hex>` identity. It does not rewrite heads, commits, +events, receipts, goals, or operation IDs. + +Provider revision tokens contain the database identity. Consequently, tokens +minted before a restore rotation become stale and conflict, while the durable +head and receipt history remain readable through a store opened with the new +identity. A wrong expected identity is rejected without a write. If the server +loses the response after `COMMIT`, the result is `ambiguous`; the service must +read metadata before retrying, rather than guessing whether rotation applied. + +## Validation + +The public synthetic fixture is +[`postgresql_authority_service_v0.json`](../../tests/fixtures/control_plane/postgresql_authority_service_v0.json). +It covers authenticated admission, authentication denial, tenant denial, +identity drift, and restore rotation without credentials or private database +details. + +Run the deterministic seam tests with: + +```sh +npm run typecheck:control-plane +npm run test:postgresql-authority-service +``` + +Run the real PostgreSQL path against an isolated disposable database by setting +`LOOPX_TEST_POSTGRES_SERVICE_URL` and invoking the same script. The database +must be disposable and separate from any active goal or other PostgreSQL test +schema; the integration test mutates only its own tenant/goal and metadata. diff --git a/docs/reference/postgresql-authority-service-v0.zh-CN.md b/docs/reference/postgresql-authority-service-v0.zh-CN.md new file mode 100644 index 0000000000..b8841000bb --- /dev/null +++ b/docs/reference/postgresql-authority-service-v0.zh-CN.md @@ -0,0 +1,61 @@ +# PostgreSQL authority service 准入 v0 + +本文定义可切换 PostgreSQL authority provider 的服务侧准入边界。它是 +Stage 2B 的 opt-in seam:不会把 PostgreSQL 变成默认 provider,不会改变 +file/SQLite 选择逻辑,不会增加 runtime caller,也不表示已经满足 promotion +条件。 + +## 边界 + +`PostgreSqlAuthorityService` 接收不透明的传输凭证、tenant、goal 和期望的 +数据库 incarnation identity,然后先调用注入的认证函数和 tenant 授权函数, +再打开 provider。凭证不会持久化、不会传给 PostgreSQL,也不会进入 +provider-neutral 的 `AuthorityStore` 合同。principal 被拒绝或无法验证时, +服务会在建立数据库连接前 fail closed。 + +服务返回以下类型化结果之一: + +| 结果 | 含义 | +| --- | --- | +| `opened` | principal 已认证、被授权使用该 tenant,且数据库 identity 与请求绑定一致。 | +| `principal_unauthenticated` | 凭证被拒绝,或认证函数返回了非法 principal。 | +| `principal_verification_unavailable` | 认证无法完成。 | +| `tenant_unauthorized` | 已认证 principal 无权使用该 tenant。 | +| `tenant_authorization_unavailable` | tenant policy 无法评估。 | +| `store_identity_unavailable` | 无法读取 provider metadata。 | +| `store_identity_mismatch` | 请求的 incarnation 不是数据库当前 incarnation。 | + +该模块刻意保持为进程内边界。部署方负责传输、凭证校验、tenant policy、连接池 +和 secret 处理;本模块不授予网络访问、actor ownership、lease ownership、跨主机 +同步或 promotion 权限。 + +## restore-incarnation 轮换 + +`rotatePostgreSqlAuthorityStoreIdentity` 是认证服务部署拥有的管理操作。它锁定 +singleton metadata 行,校验旧 identity,并原子写入新生成的 +`postgresql:<32 位小写十六进制>` identity。它不会重写 head、commit、event、 +receipt、goal 或 operation ID。 + +provider revision token 包含数据库 identity。因此 restore 轮换前签发的 token +会变成 stale 并产生 conflict;使用新 identity 打开的 store 仍能读取持久化的 +head 和 receipt 历史。期望 identity 错误时不会写入。如果 PostgreSQL 在 +`COMMIT` 后丢失响应,结果为 `ambiguous`;服务必须先重新读取 metadata 再重试, +不能猜测轮换是否已经生效。 + +## 验证 + +公开 synthetic fixture 是 +[`postgresql_authority_service_v0.json`](../../tests/fixtures/control_plane/postgresql_authority_service_v0.json), +覆盖认证准入、认证拒绝、tenant 拒绝、identity 漂移和 restore 轮换,不包含凭证 +或私有数据库信息。 + +确定性 seam 测试: + +```sh +npm run typecheck:control-plane +npm run test:postgresql-authority-service +``` + +真实 PostgreSQL 路径需要将 `LOOPX_TEST_POSTGRES_SERVICE_URL` 指向隔离的临时 +数据库,再执行同一命令。该数据库必须与活动 goal 及其他 PostgreSQL 测试 schema +分离;集成测试只修改自身的 tenant/goal 和 metadata。 From 7f6b3343bf87e13911455a2d4df8f2df44fd8191 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:16:23 +0800 Subject: [PATCH 4/5] test(postgresql): cover service admission and rotation edge paths The control-plane coverage job runs without PostgreSQL, so the provider-backed integration suite cannot cover the new admission and rotation failure paths and coverage on new code stayed below the quality gate. Exercise those paths with a scripted provider instead: malformed memberships, verifier and authorization outages, invalid authenticated principals, invalid or unchanged incarnations, stored-incarnation drift, transaction failure, and protocol violation. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../postgresql_authority_service.test.ts | 82 +++++++ .../postgresql_authority_store.test.ts | 210 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 tests/control_plane_ts/postgresql_authority_store.test.ts diff --git a/tests/control_plane_ts/postgresql_authority_service.test.ts b/tests/control_plane_ts/postgresql_authority_service.test.ts index 232f85ed97..94f670af2a 100644 --- a/tests/control_plane_ts/postgresql_authority_service.test.ts +++ b/tests/control_plane_ts/postgresql_authority_service.test.ts @@ -11,6 +11,7 @@ import { import { PostgreSqlAuthorityService, type PostgreSqlPrincipalAuthenticationResult, + type PostgreSqlAuthorityServiceOpenRequest, type PostgreSqlTenantAuthorizationResult, } from "../../loopx/control_plane/coordination/postgresql_authority_service.ts"; import { @@ -205,6 +206,87 @@ test("PostgreSQL service fails closed for authentication, tenant, and identity d assert.equal(connectionAttempts, 1); }); +test("PostgreSQL service rejects malformed memberships before opening the provider", async () => { + let connectionAttempts = 0; + const database = metadataDatabase(STORE_IDENTITY, { + onConnect: () => connectionAttempts += 1, + }); + const request = { + credential: {kind: "opaque", token: "never-persist"}, + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + store_identity: STORE_IDENTITY, + }; + + const malformed = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => principalResult(), + authorizeTenant: allowedTenant, + }); + const nullRequest = await malformed.openStore( + null as unknown as PostgreSqlAuthorityServiceOpenRequest, + ); + assert.deepEqual(nullRequest, { + status: "rejected", + reason_code: "invalid_service_request", + reason: "PostgreSQL service request is invalid", + }); + const untrimmedTenant = await malformed.openStore({ + ...request, + tenant_id: ` ${TENANT_ID}`, + }); + assert.equal(untrimmedTenant.status, "rejected"); + if (untrimmedTenant.status === "rejected") { + assert.equal(untrimmedTenant.reason_code, "invalid_service_request"); + assert.equal(untrimmedTenant.reason, "tenant id must be a non-empty trimmed string"); + } + + const unverified = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => { + throw new Error("synthetic verifier outage"); + }, + authorizeTenant: allowedTenant, + }); + assert.deepEqual(await unverified.openStore(request), { + status: "rejected", + reason_code: "principal_verification_unavailable", + reason: "PostgreSQL service could not verify the principal", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + }); + + const invalidPrincipal = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => principalResult(""), + authorizeTenant: allowedTenant, + }); + assert.deepEqual(await invalidPrincipal.openStore(request), { + status: "rejected", + reason_code: "principal_unauthenticated", + reason: "PostgreSQL service returned an invalid authenticated principal", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + }); + + const unavailableAuthorization = new PostgreSqlAuthorityService({ + database, + authenticatePrincipal: () => principalResult(), + authorizeTenant: () => { + throw new Error("synthetic authorization outage"); + }, + }); + assert.deepEqual(await unavailableAuthorization.openStore(request), { + status: "rejected", + reason_code: "tenant_authorization_unavailable", + reason: "PostgreSQL service could not authorize the tenant", + principal_id: "principal-service-fixture", + tenant_id: TENANT_ID, + goal_id: GOAL_ID, + }); + assert.equal(connectionAttempts, 0); +}); + test("PostgreSQL service treats provider metadata failures as unavailable", async () => { const service = new PostgreSqlAuthorityService({ database: { diff --git a/tests/control_plane_ts/postgresql_authority_store.test.ts b/tests/control_plane_ts/postgresql_authority_store.test.ts new file mode 100644 index 0000000000..f0241b8021 --- /dev/null +++ b/tests/control_plane_ts/postgresql_authority_store.test.ts @@ -0,0 +1,210 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + POSTGRESQL_SCHEMA_VERSION, + rotatePostgreSqlAuthorityStoreIdentity, + type PostgreSqlAuthorityConnection, + type PostgreSqlAuthorityDatabase, +} from "../../loopx/control_plane/coordination/postgresql_authority_store.ts"; + +const STORE_IDENTITY = `postgresql:${"a".repeat(32)}`; +const NEXT_STORE_IDENTITY = `postgresql:${"b".repeat(32)}`; + +type RotationQueryResult = {rows: readonly unknown[]; rowCount: number | null}; + +interface RotationHarness { + readonly database: PostgreSqlAuthorityDatabase; + readonly calls: {text: string; values: readonly unknown[] | undefined}[]; + readonly releases: (Error | undefined)[]; + readonly connectAttempts: () => number; +} + +/** Scripted provider so rotation admission and transaction outcomes stay unit-testable. */ +function rotationHarness( + handler: (text: string, values: readonly unknown[] | undefined) => RotationQueryResult, +): RotationHarness { + const calls: {text: string; values: readonly unknown[] | undefined}[] = []; + const releases: (Error | undefined)[] = []; + let connectAttempts = 0; + const connection: PostgreSqlAuthorityConnection = { + query: async (text, values) => { + calls.push({text, values}); + return handler(text, values); + }, + release: error => { + releases.push(error); + }, + }; + return { + database: { + connect: async () => { + connectAttempts += 1; + return connection; + }, + }, + calls, + releases, + connectAttempts: () => connectAttempts, + }; +} + +function metadataResult(storeIdentity: string): RotationQueryResult { + return { + rows: [{schema_version: POSTGRESQL_SCHEMA_VERSION, store_identity: storeIdentity}], + rowCount: 1, + }; +} + +test("PostgreSQL identity rotation rejects invalid and unchanged incarnations", async () => { + const harness = rotationHarness(() => { + throw new Error("a rejected rotation must not query the provider"); + }); + + assert.deepEqual( + await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + "postgresql:not-an-incarnation", + NEXT_STORE_IDENTITY, + ), + { + status: "failed", + reason_code: "invalid_store_identity", + reason: "PostgreSQL store identities must match postgresql:<32 lowercase hex>", + }, + ); + assert.deepEqual( + await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + STORE_IDENTITY, + STORE_IDENTITY, + ), + { + status: "failed", + reason_code: "store_identity_unchanged", + reason: "PostgreSQL store identity rotation requires a new incarnation", + }, + ); + assert.equal(harness.connectAttempts(), 0); +}); + +test("PostgreSQL identity rotation reports an unavailable provider before BEGIN", async () => { + const result = await rotatePostgreSqlAuthorityStoreIdentity( + { + connect: async () => { + throw new Error("synthetic unavailable provider"); + }, + }, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + + assert.deepEqual(result, { + status: "failed", + reason_code: "provider_connection_unavailable", + reason: "PostgreSQL connection was unavailable before identity rotation", + }); +}); + +test("PostgreSQL identity rotation fails closed when the stored incarnation drifted", async () => { + const harness = rotationHarness(text => + text.includes("authority_store_metadata") + ? metadataResult(NEXT_STORE_IDENTITY) + : {rows: [], rowCount: 1} + ); + + const result = await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + + assert.deepEqual(result, { + status: "failed", + reason_code: "store_identity_mismatch", + reason: "PostgreSQL store identity does not match the expected database incarnation", + }); + assert.equal(harness.calls[0]?.text, "BEGIN"); + assert.equal(harness.calls.at(-1)?.text, "ROLLBACK"); + assert.equal( + harness.calls.some(call => call.text.includes("UPDATE loopx_control_plane")), + false, + ); + assert.deepEqual(harness.releases, [undefined]); +}); + +test("PostgreSQL identity rotation maps provider protocol violations without committing", async () => { + const harness = rotationHarness(text => + text.includes("authority_store_metadata") + ? { + rows: [ + {schema_version: POSTGRESQL_SCHEMA_VERSION, store_identity: STORE_IDENTITY}, + {schema_version: POSTGRESQL_SCHEMA_VERSION, store_identity: STORE_IDENTITY}, + ], + rowCount: 2, + } + : {rows: [], rowCount: 1} + ); + + const result = await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + + assert.equal(result.status, "failed"); + if (result.status === "failed") { + assert.equal(result.reason_code, "provider_protocol_violation"); + assert.equal(result.reason, "PostgreSQL store metadata returned more than one row"); + } + assert.equal(harness.calls.some(call => call.text === "COMMIT"), false); + assert.deepEqual(harness.releases, [undefined]); +}); + +test("PostgreSQL identity rotation reports a transaction that failed before COMMIT", async () => { + const harness = rotationHarness(text => { + if (text === "BEGIN") throw new Error("synthetic BEGIN failure"); + return {rows: [], rowCount: 1}; + }); + + const result = await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + + assert.deepEqual(result, { + status: "failed", + reason_code: "provider_transaction_failed", + reason: "PostgreSQL identity rotation failed before COMMIT", + }); + assert.equal(harness.calls.at(-1)?.text, "ROLLBACK"); + assert.deepEqual(harness.releases, [undefined]); +}); + +test("PostgreSQL identity rotation commits the next incarnation", async () => { + const harness = rotationHarness(text => + text.includes("authority_store_metadata") + ? metadataResult(STORE_IDENTITY) + : {rows: [], rowCount: 1} + ); + + const result = await rotatePostgreSqlAuthorityStoreIdentity( + harness.database, + STORE_IDENTITY, + NEXT_STORE_IDENTITY, + ); + + assert.deepEqual(result, { + status: "rotated", + previous_store_identity: STORE_IDENTITY, + store_identity: NEXT_STORE_IDENTITY, + }); + assert.equal(harness.calls[0]?.text, "BEGIN"); + const update = harness.calls.find(call => + call.text.includes("UPDATE loopx_control_plane.authority_store_metadata") + ); + assert.deepEqual(update?.values, [NEXT_STORE_IDENTITY]); + assert.equal(harness.calls.at(-1)?.text, "COMMIT"); + assert.deepEqual(harness.releases, [undefined]); +}); From e4eb78457d06ed7ca755784b4b1eb52c28019f77 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:07:22 +0800 Subject: [PATCH 5/5] test(postgresql): type-check the service admission seam The new service module and its suites were absent from the control-plane TypeScript program, so npm run typecheck:control-plane never checked them. Add them to the program and keep the real-path URL assertion type-safe. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- tests/control_plane_ts/postgresql_authority_service.test.ts | 5 +++-- tsconfig.control-plane.json | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/control_plane_ts/postgresql_authority_service.test.ts b/tests/control_plane_ts/postgresql_authority_service.test.ts index 94f670af2a..2b976a3692 100644 --- a/tests/control_plane_ts/postgresql_authority_service.test.ts +++ b/tests/control_plane_ts/postgresql_authority_service.test.ts @@ -355,12 +355,13 @@ test("PostgreSQL service identity format stays provider-specific", () => { assert.equal(POSTGRESQL_STORE_IDENTITY_PATTERN.test("sqlite:abc"), false); }); -if (process.env.LOOPX_TEST_POSTGRES_URL) { +const realPathUrl = process.env.LOOPX_TEST_POSTGRES_URL; +if (realPathUrl) { test("PostgreSQL service real-path integration (set LOOPX_TEST_POSTGRES_URL)", async () => { // The disposable PostgreSQL integration is intentionally kept in the // provider integration suite; this test documents the required real-path // command without sharing mutable singleton metadata with that suite. - assert.match(process.env.LOOPX_TEST_POSTGRES_URL, /^postgres(?:ql)?:\/\//); + assert.match(realPathUrl, /^postgres(?:ql)?:\/\//); }); } else { test("PostgreSQL service real-path integration (set LOOPX_TEST_POSTGRES_URL)", {skip: true}, () => {}); diff --git a/tsconfig.control-plane.json b/tsconfig.control-plane.json index 5d2b76be3b..dc3bd5450c 100644 --- a/tsconfig.control-plane.json +++ b/tsconfig.control-plane.json @@ -37,6 +37,7 @@ "loopx/control_plane/coordination/local_authority_shadow.ts", "loopx/control_plane/coordination/local_authority_shadow_outbox.ts", "loopx/control_plane/coordination/postgresql_authority_store.ts", + "loopx/control_plane/coordination/postgresql_authority_service.ts", "loopx/control_plane/agents/delivery_workspace.ts", "loopx/control_plane/goals/vision_checkpoint.ts", "loopx/control_plane/goals/shared_goal_alignment.ts", @@ -95,6 +96,9 @@ "tests/control_plane_ts/nokv_stage2a_qualification_harness.test.ts", "tests/control_plane_ts/authority_store_readback_probe.ts", "tests/control_plane_ts/postgresql_authority_store.integration.test.ts", + "tests/control_plane_ts/postgresql_authority_service.test.ts", + "tests/control_plane_ts/postgresql_authority_service.integration.test.ts", + "tests/control_plane_ts/postgresql_authority_service_fixture.ts", "tests/control_plane_ts/delivery_continuity.test.ts", "tests/control_plane_ts/delivery_history.test.ts", "tests/control_plane_ts/delivery_workspace.test.ts",