From de3b9196b8941efb2426edf46abe062047783171 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:15:34 +0900 Subject: [PATCH 01/28] test(plugin): define durable installation store contract --- .../plugin-installation-repository.test.ts | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 apps/integration-service/src/plugin-installation-repository.test.ts diff --git a/apps/integration-service/src/plugin-installation-repository.test.ts b/apps/integration-service/src/plugin-installation-repository.test.ts new file mode 100644 index 00000000..49473392 --- /dev/null +++ b/apps/integration-service/src/plugin-installation-repository.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from 'vitest'; +import type { PluginInstallationRecord } from './plugin-installation'; + +const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; +const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; +const USER_ID = '33333333-3333-4333-8333-333333333333'; +const INSTALLED_AT = '2026-08-10T02:00:00.000Z'; +const REVOKED_AT = '2026-08-10T03:00:00.000Z'; + +interface SqlCall { + readonly text: string; + readonly values: readonly unknown[]; +} + +class RecordingSqlClient { + readonly calls: SqlCall[] = []; + + constructor(private readonly rowsByCall: readonly (readonly unknown[])[]) {} + + async query( + text: string, + values: readonly unknown[] = [], + ): Promise<{ readonly rows: readonly Row[]; readonly rowCount: number | null }> { + this.calls.push({ text, values }); + const rows = this.rowsByCall[this.calls.length - 1] ?? []; + return { rows: rows as readonly Row[], rowCount: rows.length }; + } +} + +function activeRow(overrides: Readonly> = {}) { + return { + installation_id: INSTALLATION_ID, + workspace_id: WORKSPACE_ID, + installed_by_user_id: USER_ID, + plugin_id: 'example.plugin', + plugin_contract_version: '1.0.0', + manifest_sha256: 'a'.repeat(64), + granted_capabilities: ['task.completed'], + installation_status: 'active', + installed_at: new Date(INSTALLED_AT), + revoked_at: null, + ...overrides, + }; +} + +function candidate(): PluginInstallationRecord { + return { + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_ID, + pluginId: 'example.plugin', + pluginContractVersion: '1.0.0', + manifestSha256: 'a'.repeat(64), + grantedCapabilities: ['task.completed'], + status: 'active', + installedAt: INSTALLED_AT, + revokedAt: null, + }; +} + +async function repositoryModule(): Promise>> { + return import('./plugin-installation-repository').catch(() => ({})); +} + +describe('PostgresPluginInstallationStore', () => { + it('creates one exact workspace-owned installation with parameterized SQL and no secret columns', async () => { + const module = await repositoryModule(); + const Store = module.PostgresPluginInstallationStore as new ( + client: RecordingSqlClient, + ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; + expect(typeof Store).toBe('function'); + const client = new RecordingSqlClient([[activeRow()]]); + const store = new Store(client); + + await expect(store.createIfAbsent(candidate())).resolves.toEqual(candidate()); + expect(client.calls).toHaveLength(1); + expect(client.calls[0]?.text).toContain( + 'INSERT INTO plugin_integration.plugin_installation_record', + ); + expect(client.calls[0]?.text).toContain('ON CONFLICT (installation_id) DO NOTHING'); + expect(client.calls[0]?.text).not.toMatch(/secret|token|credential/iu); + expect(client.calls[0]?.values).toEqual([ + INSTALLATION_ID, + WORKSPACE_ID, + USER_ID, + 'example.plugin', + '1.0.0', + 'a'.repeat(64), + ['task.completed'], + INSTALLED_AT, + ]); + }); + + it('returns the durable winner after an installation-id conflict without widening tenant authority', async () => { + const module = await repositoryModule(); + const Store = module.PostgresPluginInstallationStore as new ( + client: RecordingSqlClient, + ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; + const client = new RecordingSqlClient([[], [activeRow()]]); + const store = new Store(client); + + await expect(store.createIfAbsent(candidate())).resolves.toEqual(candidate()); + expect(client.calls).toHaveLength(2); + expect(client.calls[1]?.text).toContain('WHERE installation_id = $1::uuid'); + expect(client.calls[1]?.text).toContain('workspace_id = $2::uuid'); + }); + + it('looks up only exact workspace-owned evidence and atomically revokes an active installation', async () => { + const module = await repositoryModule(); + const Store = module.PostgresPluginInstallationStore as new ( + client: RecordingSqlClient, + ) => { + findById(installationId: string): Promise; + revokeActive(input: { + installationId: string; + workspaceId: string; + revokedAt: string; + }): Promise; + }; + const client = new RecordingSqlClient([ + [activeRow()], + [ + activeRow({ + installation_status: 'revoked', + revoked_at: new Date(REVOKED_AT), + }), + ], + ]); + const store = new Store(client); + + await expect(store.findById(INSTALLATION_ID)).resolves.toEqual(candidate()); + await expect( + store.revokeActive({ + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + revokedAt: REVOKED_AT, + }), + ).resolves.toEqual({ ...candidate(), status: 'revoked', revokedAt: REVOKED_AT }); + expect(client.calls[1]?.text).toContain("installation_status = 'active'"); + expect(client.calls[1]?.text).toContain('workspace_id = $2::uuid'); + }); + + it('fails closed before SQL on malformed authority and rejects duplicate or corrupt durable evidence', async () => { + const module = await repositoryModule(); + const Store = module.PostgresPluginInstallationStore as new ( + client: RecordingSqlClient, + ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; + const ValidationError = module.PluginInstallationPersistenceValidationError as new () => Error; + const PersistenceError = module.PluginInstallationPersistenceEvidenceError as new () => Error; + expect(typeof ValidationError).toBe('function'); + expect(typeof PersistenceError).toBe('function'); + + const malformedClient = new RecordingSqlClient([]); + const malformedStore = new Store(malformedClient); + await expect( + malformedStore.createIfAbsent({ ...candidate(), workspaceId: 'not-a-uuid' }), + ).rejects.toBeInstanceOf(ValidationError); + expect(malformedClient.calls).toHaveLength(0); + + for (const rows of [ + [activeRow(), activeRow()], + [activeRow({ workspace_id: '44444444-4444-4444-8444-444444444444' })], + [activeRow({ manifest_sha256: 'not-a-digest' })], + ]) { + const client = new RecordingSqlClient([rows]); + const store = new Store(client); + await expect(store.createIfAbsent(candidate())).rejects.toBeInstanceOf( + PersistenceError, + ); + } + }); +}); From ef6a0330ca02cbc8d92dbe667c7aa2d21bd4465b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:15:44 +0900 Subject: [PATCH 02/28] test(plugin): define installation persistence migration --- .../src/plugin-installation-migration.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 apps/integration-service/src/plugin-installation-migration.test.ts diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts new file mode 100644 index 00000000..7b26490f --- /dev/null +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -0,0 +1,41 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const MIGRATION_PATH = join( + process.cwd(), + 'migrations', + '0001_plugin_installation_record.sql', +); + +describe('plugin installation migration', () => { + it('uses descriptive multiword database names and stores authority evidence without secret material', () => { + const sql = readFileSync(MIGRATION_PATH, 'utf8'); + + expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS plugin_integration'); + expect(sql).toContain( + 'CREATE TABLE plugin_integration.plugin_installation_record', + ); + for (const column of [ + 'installation_id uuid PRIMARY KEY', + 'workspace_id uuid NOT NULL', + 'installed_by_user_id uuid NOT NULL', + 'plugin_id text NOT NULL', + 'plugin_contract_version text NOT NULL', + 'manifest_sha256 text NOT NULL', + 'granted_capabilities text[] NOT NULL', + "installation_status text NOT NULL DEFAULT 'active'", + 'installed_at timestamptz NOT NULL', + 'revoked_at timestamptz', + ]) { + expect(sql).toContain(column); + } + expect(sql).toContain("installation_status IN ('active', 'revoked')"); + expect(sql).toContain('cardinality(granted_capabilities) BETWEEN 0 AND 32'); + expect(sql).toContain('char_length(manifest_sha256) = 64'); + expect(sql).toContain("manifest_sha256 ~ '^[0-9a-f]{64}$'"); + expect(sql).toContain("installation_status = 'active' AND revoked_at IS NULL"); + expect(sql).toContain("installation_status = 'revoked' AND revoked_at IS NOT NULL"); + expect(sql).not.toMatch(/\b(secret|token|credential|password)_/iu); + }); +}); From 8426598f83ae0ab3df9c2a898d0227f2008ffc7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:15:55 +0900 Subject: [PATCH 03/28] feat(plugin): add installation authority migration --- .../0001_plugin_installation_record.sql | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 apps/integration-service/migrations/0001_plugin_installation_record.sql diff --git a/apps/integration-service/migrations/0001_plugin_installation_record.sql b/apps/integration-service/migrations/0001_plugin_installation_record.sql new file mode 100644 index 00000000..be3cd2bb --- /dev/null +++ b/apps/integration-service/migrations/0001_plugin_installation_record.sql @@ -0,0 +1,28 @@ +CREATE SCHEMA IF NOT EXISTS plugin_integration; + +CREATE TABLE plugin_integration.plugin_installation_record ( + installation_id uuid PRIMARY KEY, + workspace_id uuid NOT NULL, + installed_by_user_id uuid NOT NULL, + plugin_id text NOT NULL, + plugin_contract_version text NOT NULL, + manifest_sha256 text NOT NULL, + granted_capabilities text[] NOT NULL, + installation_status text NOT NULL DEFAULT 'active', + installed_at timestamptz NOT NULL, + revoked_at timestamptz, + CHECK (char_length(plugin_id) BETWEEN 1 AND 256), + CHECK (char_length(plugin_contract_version) BETWEEN 1 AND 128), + CHECK (char_length(manifest_sha256) = 64), + CHECK (manifest_sha256 ~ '^[0-9a-f]{64}$'), + CHECK (cardinality(granted_capabilities) BETWEEN 0 AND 32), + CHECK (installation_status IN ('active', 'revoked')), + CHECK (revoked_at IS NULL OR revoked_at >= installed_at), + CHECK ( + (installation_status = 'active' AND revoked_at IS NULL) + OR (installation_status = 'revoked' AND revoked_at IS NOT NULL) + ) +); + +CREATE INDEX plugin_installation_workspace_index + ON plugin_integration.plugin_installation_record (workspace_id, installation_status); From 3484b9662e977b05780672492d21b4e843d01eeb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:16:46 +0900 Subject: [PATCH 04/28] feat(plugin): persist installation authority atomically --- .../src/plugin-installation-repository.ts | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 apps/integration-service/src/plugin-installation-repository.ts diff --git a/apps/integration-service/src/plugin-installation-repository.ts b/apps/integration-service/src/plugin-installation-repository.ts new file mode 100644 index 00000000..b63df65a --- /dev/null +++ b/apps/integration-service/src/plugin-installation-repository.ts @@ -0,0 +1,381 @@ +import type { + PluginInstallationRecord, + PluginInstallationStore, + RevokePluginInstallation, +} from './plugin-installation'; + +const UUID_V4_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +const ISO_INSTANT_PATTERN = + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u; +const SHA256_PATTERN = /^[0-9a-f]{64}$/u; +const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u; +const MAXIMUM_PLUGIN_ID_LENGTH = 256; +const MAXIMUM_CONTRACT_VERSION_LENGTH = 128; +const MAXIMUM_CAPABILITY_COUNT = 32; +const MAXIMUM_CAPABILITY_LENGTH = 256; + +/** Result returned by the bounded installation SQL client. */ +export interface PluginInstallationSqlResult { + readonly rows: readonly Row[]; + readonly rowCount: number | null; +} + +/** Minimal fixed-query SQL authority used by the PostgreSQL installation store. */ +export interface PluginInstallationSqlClient { + query( + text: string, + values?: readonly unknown[], + ): Promise>; +} + +/** Rejects malformed input before it reaches PostgreSQL. */ +export class PluginInstallationPersistenceValidationError extends Error { + /** Creates a fixed credential-free validation failure. */ + constructor() { + super('Plugin installation persistence input is invalid'); + this.name = 'PluginInstallationPersistenceValidationError'; + } +} + +/** Rejects impossible, ambiguous, or corrupted persisted installation evidence. */ +export class PluginInstallationPersistenceEvidenceError extends Error { + /** Creates a fixed error without reflecting untrusted database values. */ + constructor() { + super('Persisted plugin installation evidence is invalid'); + this.name = 'PluginInstallationPersistenceEvidenceError'; + } +} + +interface PluginInstallationRow { + installation_id: unknown; + workspace_id: unknown; + installed_by_user_id: unknown; + plugin_id: unknown; + plugin_contract_version: unknown; + manifest_sha256: unknown; + granted_capabilities: unknown; + installation_status: unknown; + installed_at: unknown; + revoked_at: unknown; +} + +function invalidInput(): never { + throw new PluginInstallationPersistenceValidationError(); +} + +function invalidEvidence(): never { + throw new PluginInstallationPersistenceEvidenceError(); +} + +function inputUuid(value: unknown): string { + if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { + return invalidInput(); + } + return value.toLowerCase(); +} + +function storedUuid(value: unknown): string { + if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { + return invalidEvidence(); + } + return value.toLowerCase(); +} + +function parseInstant(value: unknown, invalid: () => never): string { + const candidate = + value instanceof Date + ? value.toISOString() + : typeof value === 'string' + ? value + : ''; + if (!ISO_INSTANT_PATTERN.test(candidate)) { + return invalid(); + } + const parsed = new Date(candidate); + if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== candidate) { + return invalid(); + } + return candidate; +} + +function boundedInputText(value: unknown, maximumLength: number): string { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > maximumLength || + CONTROL_CHARACTER_PATTERN.test(value) + ) { + return invalidInput(); + } + return value; +} + +function boundedStoredText(value: unknown, maximumLength: number): string { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > maximumLength || + CONTROL_CHARACTER_PATTERN.test(value) + ) { + return invalidEvidence(); + } + return value; +} + +function inputDigest(value: unknown): string { + if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) { + return invalidInput(); + } + return value; +} + +function storedDigest(value: unknown): string { + if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) { + return invalidEvidence(); + } + return value; +} + +function inputCapabilities(value: unknown): readonly string[] { + if (!Array.isArray(value) || value.length > MAXIMUM_CAPABILITY_COUNT) { + return invalidInput(); + } + const items = value.map((item) => + boundedInputText(item, MAXIMUM_CAPABILITY_LENGTH), + ); + const normalized = [...new Set(items)].sort(); + if ( + normalized.length !== items.length || + normalized.some((item, index) => item !== items[index]) + ) { + return invalidInput(); + } + return Object.freeze(normalized); +} + +function storedCapabilities(value: unknown): readonly string[] { + if (!Array.isArray(value) || value.length > MAXIMUM_CAPABILITY_COUNT) { + return invalidEvidence(); + } + const items = value.map((item) => + boundedStoredText(item, MAXIMUM_CAPABILITY_LENGTH), + ); + const normalized = [...new Set(items)].sort(); + if ( + normalized.length !== items.length || + normalized.some((item, index) => item !== items[index]) + ) { + return invalidEvidence(); + } + return Object.freeze(normalized); +} + +function oneOrUndefined(rows: readonly Row[]): Row | undefined { + if (rows.length > 1) { + return invalidEvidence(); + } + return rows[0]; +} + +function validateCreate(record: PluginInstallationRecord): PluginInstallationRecord { + if (record.status !== 'active' || record.revokedAt !== null) { + return invalidInput(); + } + return Object.freeze({ + installationId: inputUuid(record.installationId), + workspaceId: inputUuid(record.workspaceId), + installedByUserId: inputUuid(record.installedByUserId), + pluginId: boundedInputText(record.pluginId, MAXIMUM_PLUGIN_ID_LENGTH), + pluginContractVersion: boundedInputText( + record.pluginContractVersion, + MAXIMUM_CONTRACT_VERSION_LENGTH, + ), + manifestSha256: inputDigest(record.manifestSha256), + grantedCapabilities: inputCapabilities(record.grantedCapabilities), + status: 'active', + installedAt: parseInstant(record.installedAt, invalidInput), + revokedAt: null, + }); +} + +function validateRevocation( + input: RevokePluginInstallation, +): RevokePluginInstallation { + return Object.freeze({ + installationId: inputUuid(input.installationId), + workspaceId: inputUuid(input.workspaceId), + revokedAt: parseInstant(input.revokedAt, invalidInput), + }); +} + +function parseRow(row: PluginInstallationRow): PluginInstallationRecord { + const status = + row.installation_status === 'active' || row.installation_status === 'revoked' + ? row.installation_status + : invalidEvidence(); + const installedAt = parseInstant(row.installed_at, invalidEvidence); + const revokedAt = + row.revoked_at === null + ? null + : parseInstant(row.revoked_at, invalidEvidence); + if ( + (status === 'active' && revokedAt !== null) || + (status === 'revoked' && revokedAt === null) || + (revokedAt !== null && + new Date(revokedAt).getTime() < new Date(installedAt).getTime()) + ) { + return invalidEvidence(); + } + return Object.freeze({ + installationId: storedUuid(row.installation_id), + workspaceId: storedUuid(row.workspace_id), + installedByUserId: storedUuid(row.installed_by_user_id), + pluginId: boundedStoredText(row.plugin_id, MAXIMUM_PLUGIN_ID_LENGTH), + pluginContractVersion: boundedStoredText( + row.plugin_contract_version, + MAXIMUM_CONTRACT_VERSION_LENGTH, + ), + manifestSha256: storedDigest(row.manifest_sha256), + grantedCapabilities: storedCapabilities(row.granted_capabilities), + status, + installedAt, + revokedAt, + }); +} + +function exactCandidate( + actual: PluginInstallationRecord, + expected: PluginInstallationRecord, +): boolean { + return ( + actual.installationId === expected.installationId && + actual.workspaceId === expected.workspaceId && + actual.installedByUserId === expected.installedByUserId && + actual.pluginId === expected.pluginId && + actual.pluginContractVersion === expected.pluginContractVersion && + actual.manifestSha256 === expected.manifestSha256 && + actual.status === 'active' && + actual.installedAt === expected.installedAt && + actual.revokedAt === null && + actual.grantedCapabilities.length === expected.grantedCapabilities.length && + actual.grantedCapabilities.every( + (capability, index) => capability === expected.grantedCapabilities[index], + ) + ); +} + +const RETURNING_COLUMNS = `installation_id, workspace_id, installed_by_user_id, + plugin_id, plugin_contract_version, manifest_sha256, + granted_capabilities, installation_status, installed_at, revoked_at`; + +/** PostgreSQL implementation of the host-owned plugin installation store. */ +export class PostgresPluginInstallationStore implements PluginInstallationStore { + /** Creates the store over a bounded parameterized SQL client. */ + constructor(private readonly client: PluginInstallationSqlClient) {} + + /** Creates an active installation or returns the exact durable replay winner. */ + async createIfAbsent( + record: PluginInstallationRecord, + ): Promise { + const safe = validateCreate(record); + const inserted = await this.client.query( + `INSERT INTO plugin_integration.plugin_installation_record ( + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at + ) VALUES ($1::uuid, $2::uuid, $3::uuid, $4, $5, $6, $7::text[], 'active', $8::timestamptz) + ON CONFLICT (installation_id) DO NOTHING + RETURNING ${RETURNING_COLUMNS}`, + [ + safe.installationId, + safe.workspaceId, + safe.installedByUserId, + safe.pluginId, + safe.pluginContractVersion, + safe.manifestSha256, + safe.grantedCapabilities, + safe.installedAt, + ], + ); + let row = oneOrUndefined(inserted.rows); + if (!row) { + const existing = await this.client.query( + `SELECT ${RETURNING_COLUMNS} + FROM plugin_integration.plugin_installation_record + WHERE installation_id = $1::uuid + AND workspace_id = $2::uuid + LIMIT 2`, + [safe.installationId, safe.workspaceId], + ); + row = oneOrUndefined(existing.rows); + } + if (!row) { + return invalidEvidence(); + } + const durable = parseRow(row); + if (!exactCandidate(durable, safe)) { + return invalidEvidence(); + } + return durable; + } + + /** Reads one installation by opaque identifier for application-level tenant filtering. */ + async findById( + installationIdInput: string, + ): Promise { + const installationId = inputUuid(installationIdInput); + const result = await this.client.query( + `SELECT ${RETURNING_COLUMNS} + FROM plugin_integration.plugin_installation_record + WHERE installation_id = $1::uuid + LIMIT 2`, + [installationId], + ); + const row = oneOrUndefined(result.rows); + return row ? parseRow(row) : undefined; + } + + /** Atomically revokes active workspace-owned authority or returns an exact revoked replay. */ + async revokeActive( + input: RevokePluginInstallation, + ): Promise { + const safe = validateRevocation(input); + const updated = await this.client.query( + `UPDATE plugin_integration.plugin_installation_record + SET installation_status = 'revoked', + revoked_at = $3::timestamptz + WHERE installation_id = $1::uuid + AND workspace_id = $2::uuid + AND installation_status = 'active' + AND installed_at <= $3::timestamptz + RETURNING ${RETURNING_COLUMNS}`, + [safe.installationId, safe.workspaceId, safe.revokedAt], + ); + let row = oneOrUndefined(updated.rows); + if (!row) { + const existing = await this.client.query( + `SELECT ${RETURNING_COLUMNS} + FROM plugin_integration.plugin_installation_record + WHERE installation_id = $1::uuid + AND workspace_id = $2::uuid + AND installation_status = 'revoked' + LIMIT 2`, + [safe.installationId, safe.workspaceId], + ); + row = oneOrUndefined(existing.rows); + } + if (!row) { + return undefined; + } + const durable = parseRow(row); + if ( + durable.installationId !== safe.installationId || + durable.workspaceId !== safe.workspaceId || + durable.status !== 'revoked' + ) { + return invalidEvidence(); + } + return durable; + } +} From 886a148df65847b3ae73491be069b54e583a7f78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:17:57 +0900 Subject: [PATCH 05/28] test(plugin): preserve durable timestamp on idempotent replay --- .../src/plugin-installation-repository.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-repository.test.ts b/apps/integration-service/src/plugin-installation-repository.test.ts index 49473392..b310dc39 100644 --- a/apps/integration-service/src/plugin-installation-repository.test.ts +++ b/apps/integration-service/src/plugin-installation-repository.test.ts @@ -5,6 +5,7 @@ const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; const USER_ID = '33333333-3333-4333-8333-333333333333'; const INSTALLED_AT = '2026-08-10T02:00:00.000Z'; +const REPLAY_AT = '2026-08-10T02:30:00.000Z'; const REVOKED_AT = '2026-08-10T03:00:00.000Z'; interface SqlCall { @@ -43,7 +44,7 @@ function activeRow(overrides: Readonly> = {}) { }; } -function candidate(): PluginInstallationRecord { +function candidate(installedAt = INSTALLED_AT): PluginInstallationRecord { return { installationId: INSTALLATION_ID, workspaceId: WORKSPACE_ID, @@ -53,7 +54,7 @@ function candidate(): PluginInstallationRecord { manifestSha256: 'a'.repeat(64), grantedCapabilities: ['task.completed'], status: 'active', - installedAt: INSTALLED_AT, + installedAt, revokedAt: null, }; } @@ -91,7 +92,7 @@ describe('PostgresPluginInstallationStore', () => { ]); }); - it('returns the durable winner after an installation-id conflict without widening tenant authority', async () => { + it('returns the original durable timestamp after an exact installation-id replay', async () => { const module = await repositoryModule(); const Store = module.PostgresPluginInstallationStore as new ( client: RecordingSqlClient, @@ -99,7 +100,7 @@ describe('PostgresPluginInstallationStore', () => { const client = new RecordingSqlClient([[], [activeRow()]]); const store = new Store(client); - await expect(store.createIfAbsent(candidate())).resolves.toEqual(candidate()); + await expect(store.createIfAbsent(candidate(REPLAY_AT))).resolves.toEqual(candidate()); expect(client.calls).toHaveLength(2); expect(client.calls[1]?.text).toContain('WHERE installation_id = $1::uuid'); expect(client.calls[1]?.text).toContain('workspace_id = $2::uuid'); From 8c9b7d5bbf4acbc1eaabb2387225ce2aa2b8aaf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:18:43 +0900 Subject: [PATCH 06/28] fix(plugin): keep original timestamp on installation replay --- apps/integration-service/src/plugin-installation-repository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/integration-service/src/plugin-installation-repository.ts b/apps/integration-service/src/plugin-installation-repository.ts index b63df65a..a8814fc9 100644 --- a/apps/integration-service/src/plugin-installation-repository.ts +++ b/apps/integration-service/src/plugin-installation-repository.ts @@ -256,7 +256,6 @@ function exactCandidate( actual.pluginContractVersion === expected.pluginContractVersion && actual.manifestSha256 === expected.manifestSha256 && actual.status === 'active' && - actual.installedAt === expected.installedAt && actual.revokedAt === null && actual.grantedCapabilities.length === expected.grantedCapabilities.length && actual.grantedCapabilities.every( From 89401ed50903943ed8310119dcc12a8d457d1a2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:19:35 +0900 Subject: [PATCH 07/28] docs(plugin): trace durable installation persistence --- ...plugin-installation-authority-standards.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/research/2026-08-10-plugin-installation-authority-standards.md b/docs/research/2026-08-10-plugin-installation-authority-standards.md index 5da765c7..05786c4d 100644 --- a/docs/research/2026-08-10-plugin-installation-authority-standards.md +++ b/docs/research/2026-08-10-plugin-installation-authority-standards.md @@ -1,26 +1,35 @@ # Plugin installation authority standards note -Status: Implemented on active PR +Status: Partial ## Scope -This note records the standards basis for the first host-owned plugin installation authority slice. It does not claim that LifeOS has completed durable plugin persistence, secret storage, outbound delivery, operator APIs, or marketplace governance. +This note records the standards basis for LifeOS-owned plugin installation authority and the current durable persistence slice. The application authority from PR #151 is implemented on protected main. PR #156 adds restart-safe PostgreSQL installation evidence, but does not claim that LifeOS has completed plugin secret storage, outbound delivery, operator APIs, retry/dead-letter handling, or marketplace governance. ## Decisions 1. Installation, workspace, and actor identifiers remain opaque UUIDv4 values. RFC 9562 is the current IETF UUID specification and obsoletes RFC 4122. LifeOS uses UUIDv4 as an identifier format, not as an authentication secret or proof of authorization. 2. A validated plugin manifest expresses requested integration intent. It is never installation authority. The authenticated LifeOS host persists only an explicitly granted subset of the manifest's declared subscriptions, preventing plugin input from widening its own effective permissions. -3. The grant boundary applies least privilege: the host grants only capabilities necessary for the approved installation. This is consistent with NIST SP 800-53 Rev. 5 control AC-6. NIST's current Rev. 5 catalog includes the Release 5.2.0 updates published in 2025; this slice does not claim NIST conformance or certification. +3. The grant boundary applies least privilege: the host grants only capabilities necessary for the approved installation. This is consistent with NIST SP 800-53 Rev. 5 control AC-6. This repository does not claim NIST conformance or certification. 4. Installation evidence is host-owned and credential-free. The persisted contract records plugin identity, contract version, manifest digest, explicit grants, tenant ownership, installer identity, lifecycle status, and timestamps. Plugin-provided secrets are outside this slice. 5. Revocation is an authority transition, not deletion of evidence. The host retains the bounded installation record while future active use must treat a revoked installation as ineligible. -6. Exact replay of the same installation identifier and authority may be idempotent; conflicting reuse fails closed. Cross-workspace lookup does not disclose another tenant's installation record. +6. Exact replay of the same installation identifier and immutable authority may be idempotent; conflicting reuse fails closed. The durable original installation timestamp wins over a later retry timestamp, so retry timing cannot rewrite historical authority evidence. +7. PostgreSQL 18 is the current major PostgreSQL line used as the primary database-semantics reference for this decision. The installation store uses a unique primary-key conflict arbiter with `INSERT ... ON CONFLICT DO NOTHING`, followed by a bounded exact winner read when needed. PostgreSQL documents `ON CONFLICT` as the concurrency-aware alternative to a uniqueness error and `RETURNING` as the direct mechanism for obtaining modified-row evidence. LifeOS still validates every returned row because database success is not by itself application-authority proof. +8. Revocation uses one conditional `UPDATE ... RETURNING` scoped by installation UUIDv4, workspace UUIDv4, current `active` lifecycle state, and a non-retroactive timestamp condition. A zero-row transition is not success; only an exact already-revoked workspace-owned row may satisfy replay. +9. The database migration uses descriptive multiword `snake_case` schema, table, column, and explicit index names and contains no plaintext secret/token/credential column. Future secret persistence must use a separately reviewed encrypted secret-handle/KMS contract. ## Acceptance evidence -The active PR must preserve RED-to-GREEN tests for explicit-grant narrowing, unauthorized-grant rejection, exact replay, conflicting identifier reuse, tenant isolation, revocation, malformed UUIDv4 inputs, and immutable returned evidence. Exact-head CI, security, coverage, and current review findings must pass before merge. Protected main remains the shipped source of truth. +Protected main already preserves RED-to-GREEN tests for explicit-grant narrowing, unauthorized-grant rejection, exact application replay, conflicting identifier reuse, tenant isolation, revocation, malformed UUIDv4 inputs, and immutable returned evidence from PR #151. + +PR #156 must additionally prove the migration naming/constraint contract, parameterized create/read/revoke SQL, durable replay after process restart semantics, preservation of the original installation timestamp across a later retry, rejection of malformed input before SQL, rejection of duplicate/corrupt rows, and absence of plaintext credential columns. Exact-head CI, security, coverage, and current review findings must pass before merge. Protected main remains the shipped source of truth. ## APA 7 references Davis, K. R., Peabody, B. G., & Leach, P. J. (2024). *Universally Unique IDentifiers (UUIDs) (RFC 9562).* RFC Editor. https://doi.org/10.17487/RFC9562 Joint Task Force. (2020). *Security and privacy controls for information systems and organizations (NIST Special Publication 800-53 Rev. 5).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-53r5 + +PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: INSERT.* https://www.postgresql.org/docs/18/sql-insert.html + +PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: Returning data from modified rows.* https://www.postgresql.org/docs/18/dml-returning.html From 8032f80353bf9a99a97b1ea687d2ac28a7a73485 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:28:23 +0900 Subject: [PATCH 08/28] test(plugin): require workspace-scoped installation lookup --- .../plugin-installation-tenant-lookup.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 apps/integration-service/src/plugin-installation-tenant-lookup.test.ts diff --git a/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts b/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts new file mode 100644 index 00000000..e3ed7668 --- /dev/null +++ b/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; +import { + PluginInstallationApplication, + type PluginInstallationRecord, + type PluginInstallationStore, + type RevokePluginInstallation, +} from './plugin-installation'; + +const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; +const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; +const USER_ID = '33333333-3333-4333-8333-333333333333'; + +class ScopeRecordingStore implements PluginInstallationStore { + readonly lookupArguments: unknown[][] = []; + + async createIfAbsent( + record: PluginInstallationRecord, + ): Promise { + return record; + } + + async findById( + installationId: string, + workspaceId?: string, + ): Promise { + this.lookupArguments.push([installationId, workspaceId]); + return undefined; + } + + async revokeActive( + _input: RevokePluginInstallation, + ): Promise { + return undefined; + } +} + +describe('PluginInstallationApplication tenant lookup', () => { + it('passes trusted workspace authority into the persistence lookup instead of widening by installation id', async () => { + const store = new ScopeRecordingStore(); + const application = new PluginInstallationApplication(store); + + await expect( + application.getInstallation( + { workspaceId: WORKSPACE_ID, actorUserId: USER_ID }, + INSTALLATION_ID, + ), + ).resolves.toBeUndefined(); + + expect(store.lookupArguments).toEqual([[INSTALLATION_ID, WORKSPACE_ID]]); + }); +}); From c3e19aaa3e3c7282d710e1fdcf7a4f7eb99fd020 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:29:02 +0900 Subject: [PATCH 09/28] fix(plugin): keep installation lookup tenant-scoped in persistence --- apps/integration-service/src/plugin-installation.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/integration-service/src/plugin-installation.ts b/apps/integration-service/src/plugin-installation.ts index 7a0b085b..b9b845d0 100644 --- a/apps/integration-service/src/plugin-installation.ts +++ b/apps/integration-service/src/plugin-installation.ts @@ -55,7 +55,11 @@ export interface PluginInstallationStore { * is not sufficient because concurrent conflicting grants must fail closed. */ createIfAbsent(record: PluginInstallationRecord): Promise; - findById(installationId: string): Promise; + /** Reads one installation only inside the already-authenticated workspace scope. */ + findById( + installationId: string, + workspaceId: string, + ): Promise; /** * Atomically transitions one active workspace-owned installation to revoked, * returning the already-revoked durable winner for an exact replay. @@ -202,7 +206,7 @@ export class PluginInstallationApplication { ): Promise { const context = requireContext(trustedContext); const installationId = requireUuidV4(installationIdInput); - const existing = await this.store.findById(installationId); + const existing = await this.store.findById(installationId, context.workspaceId); if (!existing || existing.workspaceId !== context.workspaceId) { return undefined; } From 6446b21802a273e339c782d2828fa5aa07c4a2a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:29:48 +0900 Subject: [PATCH 10/28] test(plugin): require workspace predicate for installation lookup --- .../src/plugin-installation-repository.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-repository.test.ts b/apps/integration-service/src/plugin-installation-repository.test.ts index b310dc39..cc9804f7 100644 --- a/apps/integration-service/src/plugin-installation-repository.test.ts +++ b/apps/integration-service/src/plugin-installation-repository.test.ts @@ -111,7 +111,10 @@ describe('PostgresPluginInstallationStore', () => { const Store = module.PostgresPluginInstallationStore as new ( client: RecordingSqlClient, ) => { - findById(installationId: string): Promise; + findById( + installationId: string, + workspaceId: string, + ): Promise; revokeActive(input: { installationId: string; workspaceId: string; @@ -129,7 +132,11 @@ describe('PostgresPluginInstallationStore', () => { ]); const store = new Store(client); - await expect(store.findById(INSTALLATION_ID)).resolves.toEqual(candidate()); + await expect(store.findById(INSTALLATION_ID, WORKSPACE_ID)).resolves.toEqual(candidate()); + expect(client.calls[0]?.text).toContain('WHERE installation_id = $1::uuid'); + expect(client.calls[0]?.text).toContain('workspace_id = $2::uuid'); + expect(client.calls[0]?.values).toEqual([INSTALLATION_ID, WORKSPACE_ID]); + await expect( store.revokeActive({ installationId: INSTALLATION_ID, From 35b6ac7039de11f685b756d0d17ab632a9497be1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:30:29 +0900 Subject: [PATCH 11/28] fix(plugin): scope persisted installation reads by workspace --- .../src/plugin-installation-repository.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-repository.ts b/apps/integration-service/src/plugin-installation-repository.ts index a8814fc9..2a9cde4a 100644 --- a/apps/integration-service/src/plugin-installation-repository.ts +++ b/apps/integration-service/src/plugin-installation-repository.ts @@ -319,20 +319,33 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore return durable; } - /** Reads one installation by opaque identifier for application-level tenant filtering. */ + /** Reads one installation only inside the already-authenticated workspace scope. */ async findById( installationIdInput: string, + workspaceIdInput: string, ): Promise { const installationId = inputUuid(installationIdInput); + const workspaceId = inputUuid(workspaceIdInput); const result = await this.client.query( `SELECT ${RETURNING_COLUMNS} FROM plugin_integration.plugin_installation_record WHERE installation_id = $1::uuid + AND workspace_id = $2::uuid LIMIT 2`, - [installationId], + [installationId, workspaceId], ); const row = oneOrUndefined(result.rows); - return row ? parseRow(row) : undefined; + if (!row) { + return undefined; + } + const durable = parseRow(row); + if ( + durable.installationId !== installationId || + durable.workspaceId !== workspaceId + ) { + return invalidEvidence(); + } + return durable; } /** Atomically revokes active workspace-owned authority or returns an exact revoked replay. */ From 2e57e3f4d5218655cedaaee63d214a1586051636 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:43:16 +0900 Subject: [PATCH 12/28] test(plugin): require installer user authority for lookup and revoke --- ...plugin-installation-user-authority.test.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 apps/integration-service/src/plugin-installation-user-authority.test.ts diff --git a/apps/integration-service/src/plugin-installation-user-authority.test.ts b/apps/integration-service/src/plugin-installation-user-authority.test.ts new file mode 100644 index 00000000..4ca2c8f2 --- /dev/null +++ b/apps/integration-service/src/plugin-installation-user-authority.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { + PluginInstallationApplication, + PluginInstallationError, + type PluginInstallationRecord, + type PluginInstallationStore, + type RevokePluginInstallation, +} from './plugin-installation'; + +const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; +const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; +const USER_ALPHA = '33333333-3333-4333-8333-333333333333'; +const USER_BETA = '44444444-4444-4444-8444-444444444444'; +const INSTALLED_AT = '2026-08-10T02:00:00.000Z'; +const REVOKED_AT = '2026-08-10T03:00:00.000Z'; + +function record( + status: 'active' | 'revoked' = 'active', +): PluginInstallationRecord { + return Object.freeze({ + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_ALPHA, + pluginId: 'example.plugin', + pluginContractVersion: '1.0.0', + manifestSha256: 'a'.repeat(64), + grantedCapabilities: Object.freeze(['lifeos.task.completed.v1']), + status, + installedAt: INSTALLED_AT, + revokedAt: status === 'revoked' ? REVOKED_AT : null, + }); +} + +class UserScopeRecordingStore implements PluginInstallationStore { + readonly lookupArguments: unknown[][] = []; + readonly revokeArguments: RevokePluginInstallation[] = []; + + async createIfAbsent( + input: PluginInstallationRecord, + ): Promise { + return input; + } + + async findById( + installationId: string, + workspaceId: string, + installedByUserId?: string, + ): Promise { + this.lookupArguments.push([installationId, workspaceId, installedByUserId]); + return record(); + } + + async revokeActive( + input: RevokePluginInstallation, + ): Promise { + this.revokeArguments.push(input); + return record('revoked'); + } +} + +describe('PluginInstallationApplication installer-user authority', () => { + it('passes the requesting user to persistence and hides another user installation in the same workspace', async () => { + const store = new UserScopeRecordingStore(); + const application = new PluginInstallationApplication(store); + + await expect( + application.getInstallation( + { workspaceId: WORKSPACE_ID, actorUserId: USER_BETA }, + INSTALLATION_ID, + ), + ).resolves.toBeUndefined(); + + expect(store.lookupArguments).toEqual([ + [INSTALLATION_ID, WORKSPACE_ID, USER_BETA], + ]); + }); + + it('binds revoke to the requesting user and rejects another user durable result', async () => { + const store = new UserScopeRecordingStore(); + const application = new PluginInstallationApplication( + store, + () => new Date(REVOKED_AT), + ); + + await expect( + application.revoke( + { workspaceId: WORKSPACE_ID, actorUserId: USER_BETA }, + INSTALLATION_ID, + ), + ).rejects.toBeInstanceOf(PluginInstallationError); + + expect(store.revokeArguments).toEqual([ + { + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_BETA, + revokedAt: REVOKED_AT, + }, + ]); + }); +}); From d0e9aee19e78e1a59eedd80e09ac16a00c8ec592 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:43:56 +0900 Subject: [PATCH 13/28] fix(plugin): bind installation authority to installer user --- .../src/plugin-installation.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/integration-service/src/plugin-installation.ts b/apps/integration-service/src/plugin-installation.ts index b9b845d0..14da10ee 100644 --- a/apps/integration-service/src/plugin-installation.ts +++ b/apps/integration-service/src/plugin-installation.ts @@ -41,6 +41,7 @@ export interface PluginInstallationRecord { export interface RevokePluginInstallation { readonly installationId: string; readonly workspaceId: string; + readonly installedByUserId: string; readonly revokedAt: string; } @@ -55,18 +56,19 @@ export interface PluginInstallationStore { * is not sufficient because concurrent conflicting grants must fail closed. */ createIfAbsent(record: PluginInstallationRecord): Promise; - /** Reads one installation only inside the already-authenticated workspace scope. */ + /** Reads one installation only inside the authenticated workspace-and-user scope. */ findById( installationId: string, workspaceId: string, + installedByUserId: string, ): Promise; /** - * Atomically transitions one active workspace-owned installation to revoked, - * returning the already-revoked durable winner for an exact replay. + * Atomically transitions one active workspace-and-user-owned installation to + * revoked, returning the already-revoked durable winner for an exact replay. * - * Durable implementations must scope the update by installation, workspace and - * lifecycle state so concurrent revocations cannot create last-write-wins audit - * timestamps or revive a revoked record. + * Durable implementations must scope the update by installation, workspace, + * installing user and lifecycle state so another member of the same workspace + * cannot read or revoke authority they do not own. */ revokeActive(input: RevokePluginInstallation): Promise; } @@ -163,7 +165,7 @@ function sameInstallation( * * The application treats a validated manifest as requested capability intent only. * LifeOS persists the smaller host-approved grant set and never lets plugin input - * widen its own tenant authority. + * widen its own tenant or installer-user authority. */ export class PluginInstallationApplication { constructor( @@ -199,15 +201,23 @@ export class PluginInstallationApplication { return freezeRecord(durable); } - /** Returns an installation only inside the authenticated workspace boundary. */ + /** Returns an installation only inside authenticated workspace-and-user authority. */ async getInstallation( trustedContext: PluginInstallationContext, installationIdInput: string, ): Promise { const context = requireContext(trustedContext); const installationId = requireUuidV4(installationIdInput); - const existing = await this.store.findById(installationId, context.workspaceId); - if (!existing || existing.workspaceId !== context.workspaceId) { + const existing = await this.store.findById( + installationId, + context.workspaceId, + context.actorUserId, + ); + if ( + !existing || + existing.workspaceId !== context.workspaceId || + existing.installedByUserId !== context.actorUserId + ) { return undefined; } return freezeRecord(existing); @@ -223,12 +233,14 @@ export class PluginInstallationApplication { const durable = await this.store.revokeActive({ installationId, workspaceId: context.workspaceId, + installedByUserId: context.actorUserId, revokedAt: this.now().toISOString(), }); if ( !durable || durable.installationId !== installationId || durable.workspaceId !== context.workspaceId || + durable.installedByUserId !== context.actorUserId || durable.status !== 'revoked' || durable.revokedAt === null ) { From acc215199c42daf9e24ce3d93cbbb6053d9e30be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:56:47 +0900 Subject: [PATCH 14/28] fix(plugin): bind durable installation authority to installer --- .../src/plugin-installation-repository.ts | 78 ++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-repository.ts b/apps/integration-service/src/plugin-installation-repository.ts index 2a9cde4a..37657d72 100644 --- a/apps/integration-service/src/plugin-installation-repository.ts +++ b/apps/integration-service/src/plugin-installation-repository.ts @@ -14,6 +14,8 @@ const MAXIMUM_PLUGIN_ID_LENGTH = 256; const MAXIMUM_CONTRACT_VERSION_LENGTH = 128; const MAXIMUM_CAPABILITY_COUNT = 32; const MAXIMUM_CAPABILITY_LENGTH = 256; +const MAXIMUM_REPLAY_ATTEMPTS = 3; +const REPLAY_DELAY_MILLISECONDS = 10; /** Result returned by the bounded installation SQL client. */ export interface PluginInstallationSqlResult { @@ -47,6 +49,7 @@ export class PluginInstallationPersistenceEvidenceError extends Error { } } +/** Untrusted database row shape validated before it becomes installation evidence. */ interface PluginInstallationRow { installation_id: unknown; workspace_id: unknown; @@ -60,14 +63,17 @@ interface PluginInstallationRow { revoked_at: unknown; } +/** Fails closed for malformed caller input without reflecting the bad value. */ function invalidInput(): never { throw new PluginInstallationPersistenceValidationError(); } +/** Fails closed when PostgreSQL returns ambiguous or impossible durable evidence. */ function invalidEvidence(): never { throw new PluginInstallationPersistenceEvidenceError(); } +/** Normalizes a caller-supplied UUIDv4 or rejects it before SQL execution. */ function inputUuid(value: unknown): string { if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { return invalidInput(); @@ -75,6 +81,7 @@ function inputUuid(value: unknown): string { return value.toLowerCase(); } +/** Normalizes a persisted UUIDv4 or rejects corrupted database evidence. */ function storedUuid(value: unknown): string { if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { return invalidEvidence(); @@ -82,6 +89,7 @@ function storedUuid(value: unknown): string { return value.toLowerCase(); } +/** Parses one canonical UTC instant using the caller-selected fail-closed path. */ function parseInstant(value: unknown, invalid: () => never): string { const candidate = value instanceof Date @@ -99,6 +107,7 @@ function parseInstant(value: unknown, invalid: () => never): string { return candidate; } +/** Validates bounded caller text before it can become a SQL parameter. */ function boundedInputText(value: unknown, maximumLength: number): string { if ( typeof value !== 'string' || @@ -111,6 +120,7 @@ function boundedInputText(value: unknown, maximumLength: number): string { return value; } +/** Validates bounded persisted text before it can become trusted evidence. */ function boundedStoredText(value: unknown, maximumLength: number): string { if ( typeof value !== 'string' || @@ -123,6 +133,7 @@ function boundedStoredText(value: unknown, maximumLength: number): string { return value; } +/** Validates a caller-supplied lowercase SHA-256 digest. */ function inputDigest(value: unknown): string { if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) { return invalidInput(); @@ -130,6 +141,7 @@ function inputDigest(value: unknown): string { return value; } +/** Validates a persisted lowercase SHA-256 digest. */ function storedDigest(value: unknown): string { if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) { return invalidEvidence(); @@ -137,6 +149,7 @@ function storedDigest(value: unknown): string { return value; } +/** Requires a bounded, unique, already-sorted caller capability set. */ function inputCapabilities(value: unknown): readonly string[] { if (!Array.isArray(value) || value.length > MAXIMUM_CAPABILITY_COUNT) { return invalidInput(); @@ -154,6 +167,7 @@ function inputCapabilities(value: unknown): readonly string[] { return Object.freeze(normalized); } +/** Requires persisted capabilities to remain bounded, unique, and canonical. */ function storedCapabilities(value: unknown): readonly string[] { if (!Array.isArray(value) || value.length > MAXIMUM_CAPABILITY_COUNT) { return invalidEvidence(); @@ -171,6 +185,7 @@ function storedCapabilities(value: unknown): readonly string[] { return Object.freeze(normalized); } +/** Returns at most one row and rejects ambiguous duplicate durable evidence. */ function oneOrUndefined(rows: readonly Row[]): Row | undefined { if (rows.length > 1) { return invalidEvidence(); @@ -178,6 +193,7 @@ function oneOrUndefined(rows: readonly Row[]): Row | undefined { return rows[0]; } +/** Validates and freezes one new active installation before persistence. */ function validateCreate(record: PluginInstallationRecord): PluginInstallationRecord { if (record.status !== 'active' || record.revokedAt !== null) { return invalidInput(); @@ -199,16 +215,24 @@ function validateCreate(record: PluginInstallationRecord): PluginInstallationRec }); } +/** Validates revocation authority including installation, workspace, and installer. */ function validateRevocation( input: RevokePluginInstallation, ): RevokePluginInstallation { return Object.freeze({ installationId: inputUuid(input.installationId), workspaceId: inputUuid(input.workspaceId), + installedByUserId: inputUuid(input.installedByUserId), revokedAt: parseInstant(input.revokedAt, invalidInput), }); } +/** + * Converts one untrusted row into a coherent installation record. + * + * Lifecycle/timestamp contradictions and malformed identifiers fail closed rather + * than becoming application authority. + */ function parseRow(row: PluginInstallationRow): PluginInstallationRecord { const status = row.installation_status === 'active' || row.installation_status === 'revoked' @@ -244,6 +268,13 @@ function parseRow(row: PluginInstallationRow): PluginInstallationRecord { }); } +/** + * Confirms that durable active authority is the same immutable installation. + * + * `installedAt` is intentionally not compared: on an exact idempotent replay the + * original durable timestamp is authoritative and a later retry timestamp must + * not rewrite history. + */ function exactCandidate( actual: PluginInstallationRecord, expected: PluginInstallationRecord, @@ -264,6 +295,13 @@ function exactCandidate( ); } +/** Waits briefly before another conflict-winner visibility probe. */ +async function waitForReplayVisibility(): Promise { + await new Promise((resolve) => { + setTimeout(resolve, REPLAY_DELAY_MILLISECONDS); + }); +} + const RETURNING_COLUMNS = `installation_id, workspace_id, installed_by_user_id, plugin_id, plugin_contract_version, manifest_sha256, granted_capabilities, installation_status, installed_at, revoked_at`; @@ -298,14 +336,22 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore ], ); let row = oneOrUndefined(inserted.rows); - if (!row) { + for ( + let attempt = 0; + !row && attempt < MAXIMUM_REPLAY_ATTEMPTS; + attempt += 1 + ) { + if (attempt > 0) { + await waitForReplayVisibility(); + } const existing = await this.client.query( `SELECT ${RETURNING_COLUMNS} FROM plugin_integration.plugin_installation_record WHERE installation_id = $1::uuid AND workspace_id = $2::uuid + AND installed_by_user_id = $3::uuid LIMIT 2`, - [safe.installationId, safe.workspaceId], + [safe.installationId, safe.workspaceId, safe.installedByUserId], ); row = oneOrUndefined(existing.rows); } @@ -319,20 +365,23 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore return durable; } - /** Reads one installation only inside the already-authenticated workspace scope. */ + /** Reads one installation only inside authenticated workspace-and-user scope. */ async findById( installationIdInput: string, workspaceIdInput: string, + installedByUserIdInput: string, ): Promise { const installationId = inputUuid(installationIdInput); const workspaceId = inputUuid(workspaceIdInput); + const installedByUserId = inputUuid(installedByUserIdInput); const result = await this.client.query( `SELECT ${RETURNING_COLUMNS} FROM plugin_integration.plugin_installation_record WHERE installation_id = $1::uuid AND workspace_id = $2::uuid + AND installed_by_user_id = $3::uuid LIMIT 2`, - [installationId, workspaceId], + [installationId, workspaceId, installedByUserId], ); const row = oneOrUndefined(result.rows); if (!row) { @@ -341,14 +390,15 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore const durable = parseRow(row); if ( durable.installationId !== installationId || - durable.workspaceId !== workspaceId + durable.workspaceId !== workspaceId || + durable.installedByUserId !== installedByUserId ) { return invalidEvidence(); } return durable; } - /** Atomically revokes active workspace-owned authority or returns an exact revoked replay. */ + /** Atomically revokes installer-owned authority or returns an exact revoked replay. */ async revokeActive( input: RevokePluginInstallation, ): Promise { @@ -356,13 +406,19 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore const updated = await this.client.query( `UPDATE plugin_integration.plugin_installation_record SET installation_status = 'revoked', - revoked_at = $3::timestamptz + revoked_at = $4::timestamptz WHERE installation_id = $1::uuid AND workspace_id = $2::uuid + AND installed_by_user_id = $3::uuid AND installation_status = 'active' - AND installed_at <= $3::timestamptz + AND installed_at <= $4::timestamptz RETURNING ${RETURNING_COLUMNS}`, - [safe.installationId, safe.workspaceId, safe.revokedAt], + [ + safe.installationId, + safe.workspaceId, + safe.installedByUserId, + safe.revokedAt, + ], ); let row = oneOrUndefined(updated.rows); if (!row) { @@ -371,9 +427,10 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore FROM plugin_integration.plugin_installation_record WHERE installation_id = $1::uuid AND workspace_id = $2::uuid + AND installed_by_user_id = $3::uuid AND installation_status = 'revoked' LIMIT 2`, - [safe.installationId, safe.workspaceId], + [safe.installationId, safe.workspaceId, safe.installedByUserId], ); row = oneOrUndefined(existing.rows); } @@ -384,6 +441,7 @@ export class PostgresPluginInstallationStore implements PluginInstallationStore if ( durable.installationId !== safe.installationId || durable.workspaceId !== safe.workspaceId || + durable.installedByUserId !== safe.installedByUserId || durable.status !== 'revoked' ) { return invalidEvidence(); From 2cd0344ec45812d9cbad756d6be78e912a035e5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:57:33 +0900 Subject: [PATCH 15/28] test(plugin): prove installer-scoped persistence outcomes --- .../plugin-installation-repository.test.ts | 243 +++++++++++++----- 1 file changed, 174 insertions(+), 69 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-repository.test.ts b/apps/integration-service/src/plugin-installation-repository.test.ts index cc9804f7..f9aec912 100644 --- a/apps/integration-service/src/plugin-installation-repository.test.ts +++ b/apps/integration-service/src/plugin-installation-repository.test.ts @@ -1,9 +1,18 @@ import { describe, expect, it } from 'vitest'; import type { PluginInstallationRecord } from './plugin-installation'; +import { + PluginInstallationPersistenceEvidenceError, + PluginInstallationPersistenceValidationError, + PostgresPluginInstallationStore, + type PluginInstallationSqlClient, + type PluginInstallationSqlResult, +} from './plugin-installation-repository'; const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; const USER_ID = '33333333-3333-4333-8333-333333333333'; +const OTHER_WORKSPACE_ID = '44444444-4444-4444-8444-444444444444'; +const OTHER_USER_ID = '55555555-5555-4555-8555-555555555555'; const INSTALLED_AT = '2026-08-10T02:00:00.000Z'; const REPLAY_AT = '2026-08-10T02:30:00.000Z'; const REVOKED_AT = '2026-08-10T03:00:00.000Z'; @@ -13,7 +22,7 @@ interface SqlCall { readonly values: readonly unknown[]; } -class RecordingSqlClient { +class RecordingSqlClient implements PluginInstallationSqlClient { readonly calls: SqlCall[] = []; constructor(private readonly rowsByCall: readonly (readonly unknown[])[]) {} @@ -21,7 +30,7 @@ class RecordingSqlClient { async query( text: string, values: readonly unknown[] = [], - ): Promise<{ readonly rows: readonly Row[]; readonly rowCount: number | null }> { + ): Promise> { this.calls.push({ text, values }); const rows = this.rowsByCall[this.calls.length - 1] ?? []; return { rows: rows as readonly Row[], rowCount: rows.length }; @@ -44,6 +53,14 @@ function activeRow(overrides: Readonly> = {}) { }; } +function revokedRow(overrides: Readonly> = {}) { + return activeRow({ + installation_status: 'revoked', + revoked_at: new Date(REVOKED_AT), + ...overrides, + }); +} + function candidate(installedAt = INSTALLED_AT): PluginInstallationRecord { return { installationId: INSTALLATION_ID, @@ -59,26 +76,19 @@ function candidate(installedAt = INSTALLED_AT): PluginInstallationRecord { }; } -async function repositoryModule(): Promise>> { - return import('./plugin-installation-repository').catch(() => ({})); -} - describe('PostgresPluginInstallationStore', () => { - it('creates one exact workspace-owned installation with parameterized SQL and no secret columns', async () => { - const module = await repositoryModule(); - const Store = module.PostgresPluginInstallationStore as new ( - client: RecordingSqlClient, - ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; - expect(typeof Store).toBe('function'); + it('creates one exact workspace-and-installer-owned installation with parameterized SQL', async () => { const client = new RecordingSqlClient([[activeRow()]]); - const store = new Store(client); + const store = new PostgresPluginInstallationStore(client); await expect(store.createIfAbsent(candidate())).resolves.toEqual(candidate()); expect(client.calls).toHaveLength(1); expect(client.calls[0]?.text).toContain( 'INSERT INTO plugin_integration.plugin_installation_record', ); - expect(client.calls[0]?.text).toContain('ON CONFLICT (installation_id) DO NOTHING'); + expect(client.calls[0]?.text).toContain( + 'ON CONFLICT (installation_id) DO NOTHING', + ); expect(client.calls[0]?.text).not.toMatch(/secret|token|credential/iu); expect(client.calls[0]?.values).toEqual([ INSTALLATION_ID, @@ -92,88 +102,183 @@ describe('PostgresPluginInstallationStore', () => { ]); }); - it('returns the original durable timestamp after an exact installation-id replay', async () => { - const module = await repositoryModule(); - const Store = module.PostgresPluginInstallationStore as new ( - client: RecordingSqlClient, - ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; + it('returns the original durable timestamp after an exact scoped replay', async () => { const client = new RecordingSqlClient([[], [activeRow()]]); - const store = new Store(client); + const store = new PostgresPluginInstallationStore(client); - await expect(store.createIfAbsent(candidate(REPLAY_AT))).resolves.toEqual(candidate()); + await expect(store.createIfAbsent(candidate(REPLAY_AT))).resolves.toEqual( + candidate(), + ); expect(client.calls).toHaveLength(2); - expect(client.calls[1]?.text).toContain('WHERE installation_id = $1::uuid'); + expect(client.calls[1]?.text).toContain( + 'WHERE installation_id = $1::uuid', + ); expect(client.calls[1]?.text).toContain('workspace_id = $2::uuid'); + expect(client.calls[1]?.text).toContain('installed_by_user_id = $3::uuid'); + expect(client.calls[1]?.values).toEqual([ + INSTALLATION_ID, + WORKSPACE_ID, + USER_ID, + ]); }); - it('looks up only exact workspace-owned evidence and atomically revokes an active installation', async () => { - const module = await repositoryModule(); - const Store = module.PostgresPluginInstallationStore as new ( - client: RecordingSqlClient, - ) => { - findById( - installationId: string, - workspaceId: string, - ): Promise; - revokeActive(input: { - installationId: string; - workspaceId: string; - revokedAt: string; - }): Promise; - }; - const client = new RecordingSqlClient([ + it('bounds conflict-winner visibility retries instead of fabricating replay success', async () => { + const eventuallyVisible = new RecordingSqlClient([ + [], + [], + [], [activeRow()], - [ - activeRow({ - installation_status: 'revoked', - revoked_at: new Date(REVOKED_AT), - }), - ], ]); - const store = new Store(client); + const eventualStore = new PostgresPluginInstallationStore(eventuallyVisible); + await expect( + eventualStore.createIfAbsent(candidate(REPLAY_AT)), + ).resolves.toEqual(candidate()); + expect(eventuallyVisible.calls).toHaveLength(4); + + const neverVisible = new RecordingSqlClient([[], [], [], []]); + const exhaustedStore = new PostgresPluginInstallationStore(neverVisible); + await expect( + exhaustedStore.createIfAbsent(candidate(REPLAY_AT)), + ).rejects.toBeInstanceOf(PluginInstallationPersistenceEvidenceError); + expect(neverVisible.calls).toHaveLength(4); + }); + + it('looks up only exact workspace-and-installer evidence', async () => { + const matchingClient = new RecordingSqlClient([[activeRow()]]); + const matchingStore = new PostgresPluginInstallationStore(matchingClient); + await expect( + matchingStore.findById(INSTALLATION_ID, WORKSPACE_ID, USER_ID), + ).resolves.toEqual(candidate()); + expect(matchingClient.calls[0]?.values).toEqual([ + INSTALLATION_ID, + WORKSPACE_ID, + USER_ID, + ]); + expect(matchingClient.calls[0]?.text).toContain( + 'installed_by_user_id = $3::uuid', + ); - await expect(store.findById(INSTALLATION_ID, WORKSPACE_ID)).resolves.toEqual(candidate()); - expect(client.calls[0]?.text).toContain('WHERE installation_id = $1::uuid'); - expect(client.calls[0]?.text).toContain('workspace_id = $2::uuid'); - expect(client.calls[0]?.values).toEqual([INSTALLATION_ID, WORKSPACE_ID]); + const absentClient = new RecordingSqlClient([[]]); + const absentStore = new PostgresPluginInstallationStore(absentClient); + await expect( + absentStore.findById(INSTALLATION_ID, OTHER_WORKSPACE_ID, USER_ID), + ).resolves.toBeUndefined(); + await expect( + new PostgresPluginInstallationStore(new RecordingSqlClient([[]])).findById( + INSTALLATION_ID, + WORKSPACE_ID, + OTHER_USER_ID, + ), + ).resolves.toBeUndefined(); + }); + + it('fails closed when PostgreSQL returns evidence outside the requested authority scope', async () => { + const workspaceMismatch = new PostgresPluginInstallationStore( + new RecordingSqlClient([[activeRow({ workspace_id: OTHER_WORKSPACE_ID })]]), + ); + await expect( + workspaceMismatch.findById(INSTALLATION_ID, WORKSPACE_ID, USER_ID), + ).rejects.toBeInstanceOf(PluginInstallationPersistenceEvidenceError); + const userMismatch = new PostgresPluginInstallationStore( + new RecordingSqlClient([[activeRow({ installed_by_user_id: OTHER_USER_ID })]]), + ); await expect( - store.revokeActive({ + userMismatch.findById(INSTALLATION_ID, WORKSPACE_ID, USER_ID), + ).rejects.toBeInstanceOf(PluginInstallationPersistenceEvidenceError); + }); + + it('atomically revokes only installer-owned active authority and replays exact revoked evidence', async () => { + const activeClient = new RecordingSqlClient([[revokedRow()]]); + const activeStore = new PostgresPluginInstallationStore(activeClient); + await expect( + activeStore.revokeActive({ installationId: INSTALLATION_ID, workspaceId: WORKSPACE_ID, + installedByUserId: USER_ID, revokedAt: REVOKED_AT, }), - ).resolves.toEqual({ ...candidate(), status: 'revoked', revokedAt: REVOKED_AT }); - expect(client.calls[1]?.text).toContain("installation_status = 'active'"); - expect(client.calls[1]?.text).toContain('workspace_id = $2::uuid'); + ).resolves.toEqual({ + ...candidate(), + status: 'revoked', + revokedAt: REVOKED_AT, + }); + expect(activeClient.calls[0]?.text).toContain( + "installation_status = 'active'", + ); + expect(activeClient.calls[0]?.text).toContain( + 'installed_by_user_id = $3::uuid', + ); + expect(activeClient.calls[0]?.values).toEqual([ + INSTALLATION_ID, + WORKSPACE_ID, + USER_ID, + REVOKED_AT, + ]); + + const replayClient = new RecordingSqlClient([[], [revokedRow()]]); + const replayStore = new PostgresPluginInstallationStore(replayClient); + await expect( + replayStore.revokeActive({ + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_ID, + revokedAt: REVOKED_AT, + }), + ).resolves.toEqual({ + ...candidate(), + status: 'revoked', + revokedAt: REVOKED_AT, + }); + expect(replayClient.calls[1]?.values).toEqual([ + INSTALLATION_ID, + WORKSPACE_ID, + USER_ID, + ]); + + const missingStore = new PostgresPluginInstallationStore( + new RecordingSqlClient([[], []]), + ); + await expect( + missingStore.revokeActive({ + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_ID, + revokedAt: REVOKED_AT, + }), + ).resolves.toBeUndefined(); }); it('fails closed before SQL on malformed authority and rejects duplicate or corrupt durable evidence', async () => { - const module = await repositoryModule(); - const Store = module.PostgresPluginInstallationStore as new ( - client: RecordingSqlClient, - ) => { createIfAbsent(record: PluginInstallationRecord): Promise }; - const ValidationError = module.PluginInstallationPersistenceValidationError as new () => Error; - const PersistenceError = module.PluginInstallationPersistenceEvidenceError as new () => Error; - expect(typeof ValidationError).toBe('function'); - expect(typeof PersistenceError).toBe('function'); - const malformedClient = new RecordingSqlClient([]); - const malformedStore = new Store(malformedClient); + const malformedStore = new PostgresPluginInstallationStore(malformedClient); + await expect( + malformedStore.createIfAbsent({ + ...candidate(), + workspaceId: 'not-a-uuid', + }), + ).rejects.toBeInstanceOf(PluginInstallationPersistenceValidationError); await expect( - malformedStore.createIfAbsent({ ...candidate(), workspaceId: 'not-a-uuid' }), - ).rejects.toBeInstanceOf(ValidationError); + malformedStore.revokeActive({ + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: 'not-a-uuid', + revokedAt: REVOKED_AT, + }), + ).rejects.toBeInstanceOf(PluginInstallationPersistenceValidationError); expect(malformedClient.calls).toHaveLength(0); for (const rows of [ [activeRow(), activeRow()], - [activeRow({ workspace_id: '44444444-4444-4444-8444-444444444444' })], + [activeRow({ workspace_id: OTHER_WORKSPACE_ID })], + [activeRow({ installed_by_user_id: OTHER_USER_ID })], [activeRow({ manifest_sha256: 'not-a-digest' })], ]) { - const client = new RecordingSqlClient([rows]); - const store = new Store(client); + const store = new PostgresPluginInstallationStore( + new RecordingSqlClient([rows]), + ); await expect(store.createIfAbsent(candidate())).rejects.toBeInstanceOf( - PersistenceError, + PluginInstallationPersistenceEvidenceError, ); } }); From 06c90632997d8b41dc561318e7dc11e4afad9db8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:57:59 +0900 Subject: [PATCH 16/28] test(plugin): prove tenant and installer lookup isolation --- .../plugin-installation-tenant-lookup.test.ts | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts b/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts index e3ed7668..c3055a0e 100644 --- a/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts +++ b/apps/integration-service/src/plugin-installation-tenant-lookup.test.ts @@ -9,10 +9,32 @@ import { const INSTALLATION_ID = '11111111-1111-4111-8111-111111111111'; const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; const USER_ID = '33333333-3333-4333-8333-333333333333'; +const OTHER_WORKSPACE_ID = '44444444-4444-4444-8444-444444444444'; +const OTHER_USER_ID = '55555555-5555-4555-8555-555555555555'; + +function installation( + overrides: Partial = {}, +): PluginInstallationRecord { + return { + installationId: INSTALLATION_ID, + workspaceId: WORKSPACE_ID, + installedByUserId: USER_ID, + pluginId: 'example.plugin', + pluginContractVersion: '1.0.0', + manifestSha256: 'a'.repeat(64), + grantedCapabilities: ['task.completed'], + status: 'active', + installedAt: '2026-08-10T02:00:00.000Z', + revokedAt: null, + ...overrides, + }; +} class ScopeRecordingStore implements PluginInstallationStore { readonly lookupArguments: unknown[][] = []; + constructor(private readonly lookupResult?: PluginInstallationRecord) {} + async createIfAbsent( record: PluginInstallationRecord, ): Promise { @@ -21,10 +43,11 @@ class ScopeRecordingStore implements PluginInstallationStore { async findById( installationId: string, - workspaceId?: string, + workspaceId: string, + installedByUserId: string, ): Promise { - this.lookupArguments.push([installationId, workspaceId]); - return undefined; + this.lookupArguments.push([installationId, workspaceId, installedByUserId]); + return this.lookupResult; } async revokeActive( @@ -35,8 +58,9 @@ class ScopeRecordingStore implements PluginInstallationStore { } describe('PluginInstallationApplication tenant lookup', () => { - it('passes trusted workspace authority into the persistence lookup instead of widening by installation id', async () => { - const store = new ScopeRecordingStore(); + it('returns matching durable evidence and passes both trusted authorities to persistence', async () => { + const expected = installation(); + const store = new ScopeRecordingStore(expected); const application = new PluginInstallationApplication(store); await expect( @@ -44,8 +68,30 @@ describe('PluginInstallationApplication tenant lookup', () => { { workspaceId: WORKSPACE_ID, actorUserId: USER_ID }, INSTALLATION_ID, ), - ).resolves.toBeUndefined(); + ).resolves.toEqual(expected); + + expect(store.lookupArguments).toEqual([ + [INSTALLATION_ID, WORKSPACE_ID, USER_ID], + ]); + }); + + it('fails closed when a persistence implementation returns another workspace or installer', async () => { + for (const mismatched of [ + installation({ workspaceId: OTHER_WORKSPACE_ID }), + installation({ installedByUserId: OTHER_USER_ID }), + ]) { + const store = new ScopeRecordingStore(mismatched); + const application = new PluginInstallationApplication(store); - expect(store.lookupArguments).toEqual([[INSTALLATION_ID, WORKSPACE_ID]]); + await expect( + application.getInstallation( + { workspaceId: WORKSPACE_ID, actorUserId: USER_ID }, + INSTALLATION_ID, + ), + ).resolves.toBeUndefined(); + expect(store.lookupArguments).toEqual([ + [INSTALLATION_ID, WORKSPACE_ID, USER_ID], + ]); + } }); }); From d4bcafe3432b54c052816b2d288c7600cbe3f04b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:58:56 +0900 Subject: [PATCH 17/28] test(plugin): exercise installation constraints in PostgreSQL --- .../src/plugin-installation-migration.test.ts | 173 ++++++++++++++++-- 1 file changed, 157 insertions(+), 16 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts index 7b26490f..5e8b672f 100644 --- a/apps/integration-service/src/plugin-installation-migration.test.ts +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -1,19 +1,88 @@ +import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { beforeEach, describe, expect, it } from 'vitest'; const MIGRATION_PATH = join( - process.cwd(), + dirname(fileURLToPath(import.meta.url)), + '..', 'migrations', '0001_plugin_installation_record.sql', ); +const MIGRATION_SQL = readFileSync(MIGRATION_PATH, 'utf8'); +const DATABASE_URL = + process.env.INTEGRATION_DATABASE_URL ?? process.env.PLANNING_DATABASE_URL; +const describeWithPostgres = DATABASE_URL ? describe : describe.skip; -describe('plugin installation migration', () => { - it('uses descriptive multiword database names and stores authority evidence without secret material', () => { - const sql = readFileSync(MIGRATION_PATH, 'utf8'); +interface SqlExecution { + readonly status: number | null; + readonly stdout: string; + readonly stderr: string; +} + +/** Executes one isolated PostgreSQL client process against the disposable CI database. */ +function executeSql(sql: string): SqlExecution { + if (!DATABASE_URL) { + throw new Error('A PostgreSQL test database URL is required'); + } + const target = new URL(DATABASE_URL); + const result = spawnSync( + 'psql', + [ + '-X', + '-v', + 'ON_ERROR_STOP=1', + '-h', + target.hostname, + '-p', + target.port || '5432', + '-U', + decodeURIComponent(target.username), + '-d', + decodeURIComponent(target.pathname.replace(/^\//u, '')), + '-Atq', + ], + { + input: sql, + encoding: 'utf8', + env: { + ...process.env, + PGPASSWORD: decodeURIComponent(target.password), + }, + }, + ); + if (result.error) { + throw result.error; + } + return { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + }; +} + +/** Applies SQL and surfaces only bounded diagnostics when an expected setup fails. */ +function requireSqlSuccess(sql: string): string { + const result = executeSql(sql); + if (result.status !== 0) { + throw new Error(`PostgreSQL test setup failed: ${result.stderr.slice(0, 500)}`); + } + return result.stdout.trim(); +} - expect(sql).toContain('CREATE SCHEMA IF NOT EXISTS plugin_integration'); - expect(sql).toContain( +/** Proves that a statement is rejected by the real PostgreSQL constraint boundary. */ +function expectSqlFailure(sql: string): void { + const result = executeSql(sql); + expect(result.status).not.toBe(0); +} + +describe('plugin installation migration contract', () => { + it('uses descriptive multiword database names and stores authority evidence without secret material', () => { + expect(MIGRATION_SQL).toContain( + 'CREATE SCHEMA IF NOT EXISTS plugin_integration', + ); + expect(MIGRATION_SQL).toContain( 'CREATE TABLE plugin_integration.plugin_installation_record', ); for (const column of [ @@ -28,14 +97,86 @@ describe('plugin installation migration', () => { 'installed_at timestamptz NOT NULL', 'revoked_at timestamptz', ]) { - expect(sql).toContain(column); + expect(MIGRATION_SQL).toContain(column); } - expect(sql).toContain("installation_status IN ('active', 'revoked')"); - expect(sql).toContain('cardinality(granted_capabilities) BETWEEN 0 AND 32'); - expect(sql).toContain('char_length(manifest_sha256) = 64'); - expect(sql).toContain("manifest_sha256 ~ '^[0-9a-f]{64}$'"); - expect(sql).toContain("installation_status = 'active' AND revoked_at IS NULL"); - expect(sql).toContain("installation_status = 'revoked' AND revoked_at IS NOT NULL"); - expect(sql).not.toMatch(/\b(secret|token|credential|password)_/iu); + expect(MIGRATION_SQL).not.toMatch( + /\b(secret|token|credential|password)_/iu, + ); + }); +}); + +describeWithPostgres('plugin installation PostgreSQL constraints', () => { + beforeEach(() => { + requireSqlSuccess('DROP SCHEMA IF EXISTS plugin_integration CASCADE;'); + requireSqlSuccess(MIGRATION_SQL); + }); + + it('rejects impossible lifecycle, digest, and capability-count evidence', () => { + const commonColumns = ` + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at, revoked_at`; + const tooManyCapabilities = Array.from( + { length: 33 }, + (_, index) => `'capability.${index}'`, + ).join(', '); + + expectSqlFailure(` + INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) + VALUES ( + '11111111-1111-4111-8111-111111111111', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY['task.completed'], + 'active', '2026-08-10T02:00:00.000Z', '2026-08-10T03:00:00.000Z' + ); + `); + + expectSqlFailure(` + INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) + VALUES ( + '11111111-1111-4111-8111-111111111112', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', '${'a'.repeat(63)}', ARRAY['task.completed'], + 'active', '2026-08-10T02:00:00.000Z', NULL + ); + `); + + expectSqlFailure(` + INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) + VALUES ( + '11111111-1111-4111-8111-111111111113', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY[${tooManyCapabilities}], + 'active', '2026-08-10T02:00:00.000Z', NULL + ); + `); + }); + + it('preserves one durable authority row across independent PostgreSQL client processes', () => { + requireSqlSuccess(` + INSERT INTO plugin_integration.plugin_installation_record ( + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at, revoked_at + ) VALUES ( + '11111111-1111-4111-8111-111111111111', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY['task.completed'], + 'active', '2026-08-10T02:00:00.000Z', NULL + ); + `); + + const durable = requireSqlSuccess(` + SELECT installation_id || '|' || workspace_id || '|' || installed_by_user_id || '|' || installation_status + FROM plugin_integration.plugin_installation_record + WHERE installation_id = '11111111-1111-4111-8111-111111111111'::uuid; + `); + expect(durable).toBe( + '11111111-1111-4111-8111-111111111111|22222222-2222-4222-8222-222222222222|33333333-3333-4333-8333-333333333333|active', + ); }); }); From 5d0ada8951c666e0bb3bd2c1d52f69dd3bef5eb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:59:34 +0900 Subject: [PATCH 18/28] docs(plugin): align persistence evidence with current standards --- ...plugin-installation-authority-standards.md | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/research/2026-08-10-plugin-installation-authority-standards.md b/docs/research/2026-08-10-plugin-installation-authority-standards.md index 05786c4d..8b1b0fc0 100644 --- a/docs/research/2026-08-10-plugin-installation-authority-standards.md +++ b/docs/research/2026-08-10-plugin-installation-authority-standards.md @@ -4,32 +4,44 @@ Status: Partial ## Scope -This note records the standards basis for LifeOS-owned plugin installation authority and the current durable persistence slice. The application authority from PR #151 is implemented on protected main. PR #156 adds restart-safe PostgreSQL installation evidence, but does not claim that LifeOS has completed plugin secret storage, outbound delivery, operator APIs, retry/dead-letter handling, or marketplace governance. +This note records the standards basis for LifeOS-owned plugin installation authority and the current durable persistence slice. The application authority from PR #151 is implemented on protected main. PR #156 adds PostgreSQL-backed installation evidence and proves database constraints plus persistence across independent PostgreSQL client processes; it does not yet claim full application restart/recovery, plugin secret storage, outbound delivery, operator APIs, retry/dead-letter handling, or marketplace governance. ## Decisions 1. Installation, workspace, and actor identifiers remain opaque UUIDv4 values. RFC 9562 is the current IETF UUID specification and obsoletes RFC 4122. LifeOS uses UUIDv4 as an identifier format, not as an authentication secret or proof of authorization. 2. A validated plugin manifest expresses requested integration intent. It is never installation authority. The authenticated LifeOS host persists only an explicitly granted subset of the manifest's declared subscriptions, preventing plugin input from widening its own effective permissions. -3. The grant boundary applies least privilege: the host grants only capabilities necessary for the approved installation. This is consistent with NIST SP 800-53 Rev. 5 control AC-6. This repository does not claim NIST conformance or certification. +3. The grant boundary applies least privilege: the host grants only capabilities necessary for the approved installation. This is consistent with NIST SP 800-53 Rev. 5 control AC-6. NIST's current Rev. 5 publication includes the Release 5.2.0 supplemental update published in August 2025; that update does not replace the Rev. 5 publication or change this repository's no-certification stance. LifeOS does not claim NIST conformance or certification. 4. Installation evidence is host-owned and credential-free. The persisted contract records plugin identity, contract version, manifest digest, explicit grants, tenant ownership, installer identity, lifecycle status, and timestamps. Plugin-provided secrets are outside this slice. 5. Revocation is an authority transition, not deletion of evidence. The host retains the bounded installation record while future active use must treat a revoked installation as ineligible. 6. Exact replay of the same installation identifier and immutable authority may be idempotent; conflicting reuse fails closed. The durable original installation timestamp wins over a later retry timestamp, so retry timing cannot rewrite historical authority evidence. -7. PostgreSQL 18 is the current major PostgreSQL line used as the primary database-semantics reference for this decision. The installation store uses a unique primary-key conflict arbiter with `INSERT ... ON CONFLICT DO NOTHING`, followed by a bounded exact winner read when needed. PostgreSQL documents `ON CONFLICT` as the concurrency-aware alternative to a uniqueness error and `RETURNING` as the direct mechanism for obtaining modified-row evidence. LifeOS still validates every returned row because database success is not by itself application-authority proof. -8. Revocation uses one conditional `UPDATE ... RETURNING` scoped by installation UUIDv4, workspace UUIDv4, current `active` lifecycle state, and a non-retroactive timestamp condition. A zero-row transition is not success; only an exact already-revoked workspace-owned row may satisfy replay. +7. PostgreSQL 18 is the current major PostgreSQL line used as the primary database-semantics reference for this decision. The installation store uses a unique primary-key conflict arbiter with `INSERT ... ON CONFLICT DO NOTHING`, followed by a bounded exact winner read when needed. Under PostgreSQL's default Read Committed isolation, successive statements can observe newly committed rows, so the replay winner probe is bounded to three reads with short delays rather than treating one temporarily invisible row as durable corruption. PostgreSQL documents `ON CONFLICT` as the concurrency-aware alternative to a uniqueness error and `RETURNING` as the direct mechanism for obtaining modified-row evidence. LifeOS still validates every returned row because database success is not by itself application-authority proof. +8. Read and revocation persistence is scoped by installation UUIDv4, workspace UUIDv4, and installer-user UUIDv4. Revocation additionally requires current `active` lifecycle state and a non-retroactive timestamp condition. A zero-row transition is not success; only an exact already-revoked row inside the same workspace-and-installer authority may satisfy replay. 9. The database migration uses descriptive multiword `snake_case` schema, table, column, and explicit index names and contains no plaintext secret/token/credential column. Future secret persistence must use a separately reviewed encrypted secret-handle/KMS contract. ## Acceptance evidence Protected main already preserves RED-to-GREEN tests for explicit-grant narrowing, unauthorized-grant rejection, exact application replay, conflicting identifier reuse, tenant isolation, revocation, malformed UUIDv4 inputs, and immutable returned evidence from PR #151. -PR #156 must additionally prove the migration naming/constraint contract, parameterized create/read/revoke SQL, durable replay after process restart semantics, preservation of the original installation timestamp across a later retry, rejection of malformed input before SQL, rejection of duplicate/corrupt rows, and absence of plaintext credential columns. Exact-head CI, security, coverage, and current review findings must pass before merge. Protected main remains the shipped source of truth. +PR #156 must additionally prove the migration naming contract; real PostgreSQL rejection of impossible lifecycle, malformed digest, and excessive-capability rows; persistence across independent PostgreSQL client processes; parameterized create/read/revoke SQL; workspace-and-installer scoping at the application and persistence boundaries; bounded conflict-winner visibility retries; preservation of the original installation timestamp across a later retry; rejection of malformed input before SQL; rejection of duplicate/corrupt rows; and absence of plaintext credential columns. Full application restart/recovery remains outside this slice until runtime composition exists. Exact-head CI, security, coverage, and current review findings must pass before merge. Protected main remains the shipped source of truth. ## APA 7 references Davis, K. R., Peabody, B. G., & Leach, P. J. (2024). *Universally Unique IDentifiers (UUIDs) (RFC 9562).* RFC Editor. https://doi.org/10.17487/RFC9562 +Publication status: Published IETF Standards Track RFC (Proposed Standard), May 2024. + Joint Task Force. (2020). *Security and privacy controls for information systems and organizations (NIST Special Publication 800-53 Rev. 5).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-53r5 +Publication status: Final NIST Special Publication; current Rev. 5 supplemental control release is 5.2.0 (August 27, 2025). + PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: INSERT.* https://www.postgresql.org/docs/18/sql-insert.html +Publication status: Published PostgreSQL 18 release documentation. + PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: Returning data from modified rows.* https://www.postgresql.org/docs/18/dml-returning.html + +Publication status: Published PostgreSQL 18 release documentation. + +PostgreSQL Global Development Group. (2025). *PostgreSQL 18 documentation: Transaction isolation.* https://www.postgresql.org/docs/18/transaction-iso.html + +Publication status: Published PostgreSQL 18 release documentation. From 3a9ab93d9614ab3000fd94e6aed6972d62276200 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 12:03:56 +0900 Subject: [PATCH 19/28] fix(plugin): use CommonJS-safe migration path --- .../src/plugin-installation-migration.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts index 5e8b672f..f8f31eed 100644 --- a/apps/integration-service/src/plugin-installation-migration.test.ts +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -1,11 +1,10 @@ import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; import { beforeEach, describe, expect, it } from 'vitest'; const MIGRATION_PATH = join( - dirname(fileURLToPath(import.meta.url)), + __dirname, '..', 'migrations', '0001_plugin_installation_record.sql', From 1cf609c762e837738729d35132b528701c7d1779 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 12:10:30 +0900 Subject: [PATCH 20/28] test(plugin): reject invalid persisted capability elements --- .../src/plugin-installation-migration.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts index f8f31eed..01b7f3b6 100644 --- a/apps/integration-service/src/plugin-installation-migration.test.ts +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -98,6 +98,9 @@ describe('plugin installation migration contract', () => { ]) { expect(MIGRATION_SQL).toContain(column); } + expect(MIGRATION_SQL).toContain( + 'plugin_integration.capability_array_is_valid', + ); expect(MIGRATION_SQL).not.toMatch( /\b(secret|token|credential|password)_/iu, ); @@ -110,7 +113,7 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { requireSqlSuccess(MIGRATION_SQL); }); - it('rejects impossible lifecycle, digest, and capability-count evidence', () => { + it('rejects impossible lifecycle, digest, capability-count, and capability-element evidence', () => { const commonColumns = ` installation_id, workspace_id, installed_by_user_id, plugin_id, plugin_contract_version, manifest_sha256, granted_capabilities, @@ -119,6 +122,7 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { { length: 33 }, (_, index) => `'capability.${index}'`, ).join(', '); + const oversizedCapability = 'x'.repeat(257); expectSqlFailure(` INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) @@ -152,6 +156,26 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { 'active', '2026-08-10T02:00:00.000Z', NULL ); `); + + for (const [installationId, capabilities] of [ + ['11111111-1111-4111-8111-111111111114', "ARRAY['']"], + ['11111111-1111-4111-8111-111111111115', 'ARRAY[NULL::text]'], + [ + '11111111-1111-4111-8111-111111111116', + `ARRAY['${oversizedCapability}']`, + ], + ] as const) { + expectSqlFailure(` + INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) + VALUES ( + '${installationId}', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ${capabilities}, + 'active', '2026-08-10T02:00:00.000Z', NULL + ); + `); + } }); it('preserves one durable authority row across independent PostgreSQL client processes', () => { From 8089069d2ea88fdb8f7ef6ae66da5bd2f50e678b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 12:10:55 +0900 Subject: [PATCH 21/28] fix(plugin): constrain persisted capability elements --- .../0001_plugin_installation_record.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/integration-service/migrations/0001_plugin_installation_record.sql b/apps/integration-service/migrations/0001_plugin_installation_record.sql index be3cd2bb..db4ab82b 100644 --- a/apps/integration-service/migrations/0001_plugin_installation_record.sql +++ b/apps/integration-service/migrations/0001_plugin_installation_record.sql @@ -1,5 +1,19 @@ CREATE SCHEMA IF NOT EXISTS plugin_integration; +CREATE FUNCTION plugin_integration.capability_array_is_valid(capability_values text[]) +RETURNS boolean +LANGUAGE sql +IMMUTABLE +STRICT +PARALLEL SAFE +AS $$ + SELECT COALESCE( + bool_and(char_length(capability_name) BETWEEN 1 AND 256), + true + ) + FROM unnest(capability_values) AS capability_name; +$$; + CREATE TABLE plugin_integration.plugin_installation_record ( installation_id uuid PRIMARY KEY, workspace_id uuid NOT NULL, @@ -16,6 +30,8 @@ CREATE TABLE plugin_integration.plugin_installation_record ( CHECK (char_length(manifest_sha256) = 64), CHECK (manifest_sha256 ~ '^[0-9a-f]{64}$'), CHECK (cardinality(granted_capabilities) BETWEEN 0 AND 32), + CHECK (array_position(granted_capabilities, NULL) IS NULL), + CHECK (plugin_integration.capability_array_is_valid(granted_capabilities)), CHECK (installation_status IN ('active', 'revoked')), CHECK (revoked_at IS NULL OR revoked_at >= installed_at), CHECK ( From 90ae9c1e5d20efca8848cd0ccd6744364aa835a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:02:00 +0900 Subject: [PATCH 22/28] docs(plugin): record durable installation authority --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3f85d9..d04c536b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to LifeOS are documented in this file. ### Added +- Durable PostgreSQL plugin-installation authority with opaque UUIDv4 installation/workspace/installer identity, exact manifest digests, normalized explicit grants, bounded conflict replay, and atomic revocation evidence in the service-owned `plugin_integration` schema. - A durable PostgreSQL data-rights request ledger with workspace-scoped idempotency, immutable request and terminal receipt digests, one-way completion state, and real integration evidence that erasure receipts survive removal of the source workspace and user. - Migration `0006_data_rights_request_ledger.sql` for the service-owned identity ledger, retaining only bounded opaque authority references and digest/status/timestamp evidence rather than exported personal payloads. - An hourly and manually dispatchable NVIDIA NIM live-conformance harness that pins contextual-orchestrator to an exact reviewed commit, compares strong single-route reasoning with bounded conducted workflows, and retains only validated credential-free quality, safety, orchestration, usage, and ablation evidence. @@ -36,6 +37,7 @@ All notable changes to LifeOS are documented in this file. ### Security +- Plugin installation lookup, conflict replay, and revocation now carry authenticated workspace and installing-user authority through the PostgreSQL boundary; the durable record contains no plaintext plugin secret, token, credential, or password material. - The data-rights request ledger keeps personal export payloads out of durable audit rows and normalizes primary-key/idempotency collisions before dependency errors can escape the service boundary. - The commercial-development model account no longer performs Docker commands, never receives Docker-socket authority, and cannot trigger provider-wide model discovery through the credential bridge. - The scheduled live-model harness uses only `NVIDIA_NIM_API_KEY`, seeds it through the encrypted contextual-orchestrator credential registry, installs hash-locked dependencies from an exact commit, confines LifeOS traffic to loopback, allowlists NVIDIA NIM egress, and excludes provider credentials, prompts, responses, traces, and hidden reasoning from retained artifacts. From 674a49ad28f59709635c86b115fabb1d8eb4d447 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:10:42 +0900 Subject: [PATCH 23/28] test(plugin): enforce durable installation constraints --- .../src/plugin-installation-migration.test.ts | 161 +++++++++++------- 1 file changed, 99 insertions(+), 62 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts index 01b7f3b6..56866ffa 100644 --- a/apps/integration-service/src/plugin-installation-migration.test.ts +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -70,10 +70,11 @@ function requireSqlSuccess(sql: string): string { return result.stdout.trim(); } -/** Proves that a statement is rejected by the real PostgreSQL constraint boundary. */ -function expectSqlFailure(sql: string): void { +/** Proves that a specific PostgreSQL constraint rejects one fixed hostile fixture. */ +function expectSqlFailure(sql: string, expectedConstraint: string): void { const result = executeSql(sql); expect(result.status).not.toBe(0); + expect(result.stderr).toContain(expectedConstraint); } describe('plugin installation migration contract', () => { @@ -113,69 +114,105 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { requireSqlSuccess(MIGRATION_SQL); }); - it('rejects impossible lifecycle, digest, capability-count, and capability-element evidence', () => { - const commonColumns = ` - installation_id, workspace_id, installed_by_user_id, plugin_id, - plugin_contract_version, manifest_sha256, granted_capabilities, - installation_status, installed_at, revoked_at`; - const tooManyCapabilities = Array.from( - { length: 33 }, - (_, index) => `'capability.${index}'`, - ).join(', '); - const oversizedCapability = 'x'.repeat(257); - - expectSqlFailure(` - INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) - VALUES ( - '11111111-1111-4111-8111-111111111111', - '22222222-2222-4222-8222-222222222222', - '33333333-3333-4333-8333-333333333333', - 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY['task.completed'], - 'active', '2026-08-10T02:00:00.000Z', '2026-08-10T03:00:00.000Z' - ); - `); + it('rejects impossible lifecycle, digest, capability, and UUID authority evidence', () => { + expectSqlFailure( + `INSERT INTO plugin_integration.plugin_installation_record ( + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at, revoked_at + ) VALUES ( + '11111111-1111-4111-8111-111111111111', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a'], + 'active', '2026-08-10T02:00:00.000Z', '2026-08-10T03:00:00.000Z' + );`, + 'plugin_installation_lifecycle_consistency', + ); - expectSqlFailure(` - INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) - VALUES ( - '11111111-1111-4111-8111-111111111112', - '22222222-2222-4222-8222-222222222222', - '33333333-3333-4333-8333-333333333333', - 'example.plugin', '1.0.0', '${'a'.repeat(63)}', ARRAY['task.completed'], - 'active', '2026-08-10T02:00:00.000Z', NULL - ); - `); + expectSqlFailure( + `INSERT INTO plugin_integration.plugin_installation_record ( + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at, revoked_at + ) VALUES ( + '11111111-1111-4111-8111-111111111112', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', repeat('a', 63), ARRAY['a'], + 'active', '2026-08-10T02:00:00.000Z', NULL + );`, + 'plugin_installation_manifest_sha256', + ); - expectSqlFailure(` - INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) - VALUES ( - '11111111-1111-4111-8111-111111111113', - '22222222-2222-4222-8222-222222222222', - '33333333-3333-4333-8333-333333333333', - 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY[${tooManyCapabilities}], - 'active', '2026-08-10T02:00:00.000Z', NULL - ); - `); + expectSqlFailure( + `INSERT INTO plugin_integration.plugin_installation_record ( + installation_id, workspace_id, installed_by_user_id, plugin_id, + plugin_contract_version, manifest_sha256, granted_capabilities, + installation_status, installed_at, revoked_at + ) VALUES ( + '11111111-1111-4111-8111-111111111113', + '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', + 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY(SELECT 'capability.' || lpad(value::text, 2, '0') + FROM generate_series(1, 33) AS value ORDER BY value), + 'active', '2026-08-10T02:00:00.000Z', NULL + );`, + 'plugin_installation_capability_count', + ); - for (const [installationId, capabilities] of [ - ['11111111-1111-4111-8111-111111111114', "ARRAY['']"], - ['11111111-1111-4111-8111-111111111115', 'ARRAY[NULL::text]'], - [ - '11111111-1111-4111-8111-111111111116', - `ARRAY['${oversizedCapability}']`, - ], + for (const fixture of [ + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111114', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY[''], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_capability_array', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111115', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY[NULL::text], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_capability_array', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111116', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY[repeat('x', 257)], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_capability_array', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-7111-8111-111111111117', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_id_uuid_v4', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111118', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['a', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_capability_array', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111119', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['b', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_capability_array', + }, ] as const) { - expectSqlFailure(` - INSERT INTO plugin_integration.plugin_installation_record (${commonColumns}) - VALUES ( - '${installationId}', - '22222222-2222-4222-8222-222222222222', - '33333333-3333-4333-8333-333333333333', - 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ${capabilities}, - 'active', '2026-08-10T02:00:00.000Z', NULL - ); - `); + expectSqlFailure(fixture.sql, fixture.constraint); } + + requireSqlSuccess(`INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111120', '22222222-2222-4222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['a', 'b'], 'active', '2026-08-10T02:00:00.000Z', NULL);`); }); it('preserves one durable authority row across independent PostgreSQL client processes', () => { @@ -188,7 +225,7 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { '11111111-1111-4111-8111-111111111111', '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', - 'example.plugin', '1.0.0', '${'a'.repeat(64)}', ARRAY['task.completed'], + 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL ); `); @@ -202,4 +239,4 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { '11111111-1111-4111-8111-111111111111|22222222-2222-4222-8222-222222222222|33333333-3333-4333-8333-333333333333|active', ); }); -}); +}); \ No newline at end of file From ab01e99daa834dbb3bf03acd8f30ffbee9d2054b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:16:15 +0900 Subject: [PATCH 24/28] fix(plugin): enforce durable installation invariants --- .../0001_plugin_installation_record.sql | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/apps/integration-service/migrations/0001_plugin_installation_record.sql b/apps/integration-service/migrations/0001_plugin_installation_record.sql index db4ab82b..c8c7015c 100644 --- a/apps/integration-service/migrations/0001_plugin_installation_record.sql +++ b/apps/integration-service/migrations/0001_plugin_installation_record.sql @@ -7,11 +7,20 @@ IMMUTABLE STRICT PARALLEL SAFE AS $$ - SELECT COALESCE( - bool_and(char_length(capability_name) BETWEEN 1 AND 256), - true - ) - FROM unnest(capability_values) AS capability_name; + SELECT + COALESCE( + bool_and(char_length(capability_name) BETWEEN 1 AND 256), + true + ) + AND cardinality(capability_values) = ( + SELECT count(DISTINCT capability_name COLLATE "C") + FROM unnest(capability_values) AS capability_name + ) + AND capability_values = ARRAY( + SELECT capability_name + FROM unnest(capability_values) AS capability_name + ORDER BY capability_name COLLATE "C" + ); $$; CREATE TABLE plugin_integration.plugin_installation_record ( @@ -25,18 +34,41 @@ CREATE TABLE plugin_integration.plugin_installation_record ( installation_status text NOT NULL DEFAULT 'active', installed_at timestamptz NOT NULL, revoked_at timestamptz, - CHECK (char_length(plugin_id) BETWEEN 1 AND 256), - CHECK (char_length(plugin_contract_version) BETWEEN 1 AND 128), - CHECK (char_length(manifest_sha256) = 64), - CHECK (manifest_sha256 ~ '^[0-9a-f]{64}$'), - CHECK (cardinality(granted_capabilities) BETWEEN 0 AND 32), - CHECK (array_position(granted_capabilities, NULL) IS NULL), - CHECK (plugin_integration.capability_array_is_valid(granted_capabilities)), - CHECK (installation_status IN ('active', 'revoked')), - CHECK (revoked_at IS NULL OR revoked_at >= installed_at), - CHECK ( - (installation_status = 'active' AND revoked_at IS NULL) - OR (installation_status = 'revoked' AND revoked_at IS NOT NULL) + CONSTRAINT plugin_installation_id_uuid_v4 CHECK ( + installation_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT plugin_installation_workspace_id_uuid_v4 CHECK ( + workspace_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT plugin_installation_user_id_uuid_v4 CHECK ( + installed_by_user_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' + ), + CONSTRAINT plugin_installation_plugin_id_length CHECK ( + char_length(plugin_id) BETWEEN 1 AND 256 + ), + CONSTRAINT plugin_installation_contract_version_length CHECK ( + char_length(plugin_contract_version) BETWEEN 1 AND 128 + ), + CONSTRAINT plugin_installation_manifest_sha256 CHECK ( + char_length(manifest_sha256) = 64 + AND manifest_sha256 ~ '^[0-9a-f]{64}$' + ), + CONSTRAINT plugin_installation_capability_count CHECK ( + cardinality(granted_capabilities) BETWEEN 0 AND 32 + ), + CONSTRAINT plugin_installation_capability_array CHECK ( + array_position(granted_capabilities, NULL) IS NULL + AND plugin_integration.capability_array_is_valid(granted_capabilities) + ), + CONSTRAINT plugin_installation_status_valid CHECK ( + installation_status IN ('active', 'revoked') + ), + CONSTRAINT plugin_installation_lifecycle_consistency CHECK ( + (revoked_at IS NULL OR revoked_at >= installed_at) + AND ( + (installation_status = 'active' AND revoked_at IS NULL) + OR (installation_status = 'revoked' AND revoked_at IS NOT NULL) + ) ) ); From b45f69295f47c88bd29348e7ae3aff016e9ed537 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:17:06 +0900 Subject: [PATCH 25/28] test(plugin): isolate durable installation evidence --- .../src/plugin-installation-migration.test.ts | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/apps/integration-service/src/plugin-installation-migration.test.ts b/apps/integration-service/src/plugin-installation-migration.test.ts index 56866ffa..923f90c1 100644 --- a/apps/integration-service/src/plugin-installation-migration.test.ts +++ b/apps/integration-service/src/plugin-installation-migration.test.ts @@ -10,8 +10,7 @@ const MIGRATION_PATH = join( '0001_plugin_installation_record.sql', ); const MIGRATION_SQL = readFileSync(MIGRATION_PATH, 'utf8'); -const DATABASE_URL = - process.env.INTEGRATION_DATABASE_URL ?? process.env.PLANNING_DATABASE_URL; +const DATABASE_URL = process.env.INTEGRATION_DATABASE_URL; const describeWithPostgres = DATABASE_URL ? describe : describe.skip; interface SqlExecution { @@ -20,10 +19,10 @@ interface SqlExecution { readonly stderr: string; } -/** Executes one isolated PostgreSQL client process against the disposable CI database. */ +/** Executes one isolated PostgreSQL client process against the dedicated disposable integration database. */ function executeSql(sql: string): SqlExecution { if (!DATABASE_URL) { - throw new Error('A PostgreSQL test database URL is required'); + throw new Error('A dedicated PostgreSQL integration test database URL is required'); } const target = new URL(DATABASE_URL); const result = spawnSync( @@ -99,6 +98,17 @@ describe('plugin installation migration contract', () => { ]) { expect(MIGRATION_SQL).toContain(column); } + for (const constraint of [ + 'plugin_installation_id_uuid_v4', + 'plugin_installation_workspace_id_uuid_v4', + 'plugin_installation_user_id_uuid_v4', + 'plugin_installation_manifest_sha256', + 'plugin_installation_capability_count', + 'plugin_installation_capability_array', + 'plugin_installation_lifecycle_consistency', + ]) { + expect(MIGRATION_SQL).toContain(`CONSTRAINT ${constraint}`); + } expect(MIGRATION_SQL).toContain( 'plugin_integration.capability_array_is_valid', ); @@ -193,14 +203,28 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { }, { sql: `INSERT INTO plugin_integration.plugin_installation_record - VALUES ('11111111-1111-4111-8111-111111111118', '22222222-2222-4222-8222-222222222222', + VALUES ('11111111-1111-4111-8111-111111111118', '22222222-2222-7222-8222-222222222222', + '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_workspace_id_uuid_v4', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111119', '22222222-2222-4222-8222-222222222222', + '33333333-3333-7333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), + ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, + constraint: 'plugin_installation_user_id_uuid_v4', + }, + { + sql: `INSERT INTO plugin_integration.plugin_installation_record + VALUES ('11111111-1111-4111-8111-111111111120', '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, constraint: 'plugin_installation_capability_array', }, { sql: `INSERT INTO plugin_integration.plugin_installation_record - VALUES ('11111111-1111-4111-8111-111111111119', '22222222-2222-4222-8222-222222222222', + VALUES ('11111111-1111-4111-8111-111111111121', '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['b', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, constraint: 'plugin_installation_capability_array', @@ -210,7 +234,7 @@ describeWithPostgres('plugin installation PostgreSQL constraints', () => { } requireSqlSuccess(`INSERT INTO plugin_integration.plugin_installation_record - VALUES ('11111111-1111-4111-8111-111111111120', '22222222-2222-4222-8222-222222222222', + VALUES ('11111111-1111-4111-8111-111111111122', '22222222-2222-4222-8222-222222222222', '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a', 'b'], 'active', '2026-08-10T02:00:00.000Z', NULL);`); }); From a9520445bfb4369730a51f7d8432cbdcb1bd80f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:17:21 +0900 Subject: [PATCH 26/28] ci(plugin): expose dedicated integration database --- turbo.json | 1 + 1 file changed, 1 insertion(+) diff --git a/turbo.json b/turbo.json index 4f939473..6bb3ff56 100644 --- a/turbo.json +++ b/turbo.json @@ -5,6 +5,7 @@ "AI_TEST_DATABASE_URL", "HABIT_DATABASE_URL", "IDENTITY_DATABASE_URL", + "INTEGRATION_DATABASE_URL", "NOTIFICATION_DATABASE_URL", "PLANNING_DATABASE_URL", "PRIVACY_DATABASE_URL" From 7206b09d8297bad8a68c4b03235235d4f4bf9937 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:18:15 +0900 Subject: [PATCH 27/28] ci(plugin): run dedicated integration persistence tests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb967454..cdf970ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,7 @@ jobs: AI_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test AI_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test IDENTITY_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test + INTEGRATION_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test PLANNING_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test HABIT_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test NOTIFICATION_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test @@ -208,6 +209,7 @@ jobs: AI_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test AI_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test IDENTITY_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test + INTEGRATION_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test PLANNING_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test HABIT_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test NOTIFICATION_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/life_os_test From 55c42f92d225d0dca3f358aa4df03f68afa80fab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 13:18:51 +0900 Subject: [PATCH 28/28] docs(plugin): clarify revocation authority --- apps/integration-service/src/plugin-installation.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/integration-service/src/plugin-installation.ts b/apps/integration-service/src/plugin-installation.ts index 14da10ee..7423f91a 100644 --- a/apps/integration-service/src/plugin-installation.ts +++ b/apps/integration-service/src/plugin-installation.ts @@ -37,7 +37,12 @@ export interface PluginInstallationRecord { readonly revokedAt: string | null; } -/** Atomic revocation request owned by the host persistence boundary. */ +/** + * Atomic revocation request owned by the host persistence boundary. + * + * `installedByUserId` is derived from authenticated host context and remains part + * of the durable lookup/update scope; plugin input never supplies this authority. + */ export interface RevokePluginInstallation { readonly installationId: string; readonly workspaceId: string;