@axiom-lattice/pg-stores 5.1.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +69 -0
- package/dist/index.d.mts +87 -3
- package/dist/index.d.ts +87 -3
- package/dist/index.js +534 -68
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +477 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgreSQLMcpServerConfigStore.integration.test.ts +122 -0
- package/src/__tests__/PostgreSQLMcpServerConfigStore.test.ts +81 -0
- package/src/__tests__/PostgreSQLPlatformAuditStore.test.ts +446 -0
- package/src/__tests__/PostgreSQLUserTenantLinkStore.test.ts +62 -0
- package/src/createPgStoreConfig.ts +6 -1
- package/src/index.ts +2 -0
- package/src/migrations/mcp_server_config_migrations.ts +47 -0
- package/src/migrations/platform_audit_migration.ts +45 -0
- package/src/stores/PostgreSQLMcpServerConfigStore.ts +343 -15
- package/src/stores/PostgreSQLPlatformAuditStore.ts +209 -0
- package/src/stores/PostgreSQLUserTenantLinkStore.ts +2 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import {
|
|
4
|
+
PLATFORM_TENANT_ID,
|
|
5
|
+
PLATFORM_TENANT_PROTECTED_ERROR,
|
|
6
|
+
} from "@axiom-lattice/protocols";
|
|
7
|
+
import { createUserTenantLinkTable } from "../migrations/user_tenant_link_migrations";
|
|
8
|
+
import { PostgreSQLUserTenantLinkStore } from "../stores/PostgreSQLUserTenantLinkStore";
|
|
9
|
+
|
|
10
|
+
const databaseUrl = process.env.TEST_DATABASE_URL;
|
|
11
|
+
const describeIfDatabase = databaseUrl ? describe : describe.skip;
|
|
12
|
+
|
|
13
|
+
describeIfDatabase("PostgreSQLUserTenantLinkStore platform tenant guard", () => {
|
|
14
|
+
let pool: Pool;
|
|
15
|
+
let store: PostgreSQLUserTenantLinkStore;
|
|
16
|
+
const prefix = `utl-${randomUUID()}`;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
pool = new Pool({ connectionString: databaseUrl });
|
|
20
|
+
const client = await pool.connect();
|
|
21
|
+
try {
|
|
22
|
+
await createUserTenantLinkTable.up(client);
|
|
23
|
+
} finally {
|
|
24
|
+
client.release();
|
|
25
|
+
}
|
|
26
|
+
store = new PostgreSQLUserTenantLinkStore({ pool });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await pool.query(
|
|
31
|
+
`DELETE FROM lattice_user_tenant_links WHERE user_id LIKE $1`,
|
|
32
|
+
[`${prefix}%`],
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await pool.query(
|
|
38
|
+
`DELETE FROM lattice_user_tenant_links WHERE user_id LIKE $1`,
|
|
39
|
+
[`${prefix}%`],
|
|
40
|
+
);
|
|
41
|
+
await pool.end();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("still creates links for ordinary tenants", async () => {
|
|
45
|
+
await expect(
|
|
46
|
+
store.createLink({ userId: `${prefix}-u1`, tenantId: "acme" }),
|
|
47
|
+
).resolves.toMatchObject({ tenantId: "acme", role: "member" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects creating a link to the hidden platform tenant", async () => {
|
|
51
|
+
await expect(
|
|
52
|
+
store.createLink({
|
|
53
|
+
userId: `${prefix}-u2`,
|
|
54
|
+
tenantId: PLATFORM_TENANT_ID,
|
|
55
|
+
}),
|
|
56
|
+
).rejects.toMatchObject({ code: PLATFORM_TENANT_PROTECTED_ERROR });
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
store.getLink(`${prefix}-u2`, PLATFORM_TENANT_ID),
|
|
60
|
+
).resolves.toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -63,7 +63,7 @@ import { createChannelBindingsTable } from "./migrations/channel_bindings_migrat
|
|
|
63
63
|
import { createDatabaseConfigsTable } from "./migrations/database_config_migrations";
|
|
64
64
|
import { createConnectionConfigsTable, addConnectionSecretFields, reencryptLegacyConnectionSecrets } from "./migrations/connection_config_migrations";
|
|
65
65
|
import { createMetricsConfigsTable } from "./migrations/metrics_config_migrations";
|
|
66
|
-
import { createMcpServerConfigsTable } from "./migrations/mcp_server_config_migrations";
|
|
66
|
+
import { createMcpServerConfigsTable, createPersonalMcpServerConfigsTable } from "./migrations/mcp_server_config_migrations";
|
|
67
67
|
import { registerThreadMessageQueueMigrations } from "./migrations/queue_migrations";
|
|
68
68
|
import { createWorkflowTrackingTables, addStepThreadId, addWorkflowRunsTenantStatusUpdatedIndex } from "./migrations/workflow_tracking_migrations";
|
|
69
69
|
import { evalMigrations } from "./migrations/eval_migrations";
|
|
@@ -83,6 +83,7 @@ import { createInviteLinksTable } from "./migrations/invite_link_migration";
|
|
|
83
83
|
import { addOidcIdentityClaimInviteFields } from "./migrations/oidc_identity_claim_invite_migration";
|
|
84
84
|
import { createPlatformSettingsTable } from "./migrations/platform_settings_migration";
|
|
85
85
|
import { addOidcProviderAllowedHosts } from "./migrations/oidc_provider_allowed_hosts_migration";
|
|
86
|
+
import { createPlatformAuditTable } from "./migrations/platform_audit_migration";
|
|
86
87
|
import { taskMigrations } from "./migrations/task_migration";
|
|
87
88
|
import { createMenuItemsTable } from "./migrations/menu_items_migration";
|
|
88
89
|
import { addFileContentType } from "./migrations/menu_items_add_file_type";
|
|
@@ -141,6 +142,7 @@ import { PostgreSQLOutboxStore } from "./stores/PostgreSQLOutboxStore";
|
|
|
141
142
|
import { PostgreSQLPluginDeploymentStore } from "./stores/PostgreSQLPluginDeploymentStore";
|
|
142
143
|
import { PostgreSQLAgentRunLedger } from "./stores/PostgreSQLAgentRunLedger";
|
|
143
144
|
import { PostgreSQLPlatformSettingsStore } from "./stores/PostgreSQLPlatformSettingsStore";
|
|
145
|
+
import { PostgreSQLPlatformAuditStore } from "./stores/PostgreSQLPlatformAuditStore";
|
|
144
146
|
import { PostgreSQLChannelReplyJobStore } from "./stores/PostgreSQLChannelReplyJobStore";
|
|
145
147
|
import { PostgreSQLCheckpointStorage } from "./stores/PostgreSQLCheckpointStorage";
|
|
146
148
|
import { PostgreSQLStore } from "./stores/PostgreSQLStore";
|
|
@@ -253,6 +255,8 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
253
255
|
mm.register(addOidcIdentityClaimInviteFields); // v216
|
|
254
256
|
mm.register(createPlatformSettingsTable); // v217
|
|
255
257
|
mm.register(addOidcProviderAllowedHosts); // v218
|
|
258
|
+
mm.register(createPlatformAuditTable); // v219
|
|
259
|
+
mm.register(createPersonalMcpServerConfigsTable); // v220
|
|
256
260
|
|
|
257
261
|
await mm.migrate();
|
|
258
262
|
|
|
@@ -281,6 +285,7 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
281
285
|
oidcIdentityClaim: new PostgreSQLOidcIdentityClaimStore(opts),
|
|
282
286
|
inviteLink: new PostgreSQLInviteLinkStore(opts),
|
|
283
287
|
platformSettings: new PostgreSQLPlatformSettingsStore(opts),
|
|
288
|
+
platformAudit: new PostgreSQLPlatformAuditStore(opts),
|
|
284
289
|
channelBinding: new ChannelBindingStore(opts),
|
|
285
290
|
channelInstallation: new PostgreSQLChannelInstallationStore(opts),
|
|
286
291
|
thread: new PostgreSQLThreadStore(opts),
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,8 @@ export * from "./migrations/oidc_identity_claim_invite_migration";
|
|
|
40
40
|
export * from "./stores/PostgreSQLInviteLinkStore";
|
|
41
41
|
export * from "./migrations/platform_settings_migration";
|
|
42
42
|
export * from "./stores/PostgreSQLPlatformSettingsStore";
|
|
43
|
+
export * from "./migrations/platform_audit_migration";
|
|
44
|
+
export * from "./stores/PostgreSQLPlatformAuditStore";
|
|
43
45
|
export * from "./migrations/migration";
|
|
44
46
|
export * from "./migrations/thread_migrations";
|
|
45
47
|
export * from "./migrations/thread_tenant_migration";
|
|
@@ -53,3 +53,50 @@ export const createMcpServerConfigsTable: Migration = {
|
|
|
53
53
|
await client.query('DROP TABLE IF EXISTS lattice_mcp_server_configs');
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Migration: create the personal MCP server config table.
|
|
59
|
+
*
|
|
60
|
+
* Personal configs are deliberately stored outside
|
|
61
|
+
* `lattice_mcp_server_configs` so tenant-only code paths (and older replicas
|
|
62
|
+
* during a rolling upgrade) can never read or register them.
|
|
63
|
+
*/
|
|
64
|
+
export const createPersonalMcpServerConfigsTable: Migration = {
|
|
65
|
+
version: 220,
|
|
66
|
+
name: 'create_personal_mcp_server_configs_table',
|
|
67
|
+
up: async (client: PoolClient) => {
|
|
68
|
+
await client.query(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS lattice_personal_mcp_server_configs (
|
|
70
|
+
id VARCHAR(255) NOT NULL,
|
|
71
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
72
|
+
owner_assistant_id VARCHAR(255) NOT NULL,
|
|
73
|
+
key VARCHAR(255) NOT NULL,
|
|
74
|
+
name VARCHAR(255),
|
|
75
|
+
description TEXT,
|
|
76
|
+
config JSONB NOT NULL,
|
|
77
|
+
selected_tools JSONB NOT NULL DEFAULT '[]',
|
|
78
|
+
is_env_encrypted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
79
|
+
status VARCHAR(50) NOT NULL DEFAULT 'disconnected',
|
|
80
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
81
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
82
|
+
|
|
83
|
+
PRIMARY KEY (tenant_id, owner_assistant_id, id)
|
|
84
|
+
)
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
await client.query(`
|
|
88
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_lattice_personal_mcp_assistant_key
|
|
89
|
+
ON lattice_personal_mcp_server_configs(tenant_id, owner_assistant_id, key)
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
await client.query(`
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_personal_mcp_assistant
|
|
94
|
+
ON lattice_personal_mcp_server_configs(tenant_id, owner_assistant_id)
|
|
95
|
+
`);
|
|
96
|
+
},
|
|
97
|
+
down: async (client: PoolClient) => {
|
|
98
|
+
await client.query('DROP INDEX IF EXISTS idx_lattice_personal_mcp_assistant_key');
|
|
99
|
+
await client.query('DROP INDEX IF EXISTS idx_lattice_personal_mcp_assistant');
|
|
100
|
+
await client.query('DROP TABLE IF EXISTS lattice_personal_mcp_server_configs');
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
|
|
3
|
+
/** Creates the append-only platform-level audit log table. */
|
|
4
|
+
export const createPlatformAuditTable: Migration = {
|
|
5
|
+
version: 219,
|
|
6
|
+
name: "create_platform_audit_table",
|
|
7
|
+
up: async (client) => {
|
|
8
|
+
await client.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS lattice_platform_audit (
|
|
10
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
|
+
initiator_user_id VARCHAR(255),
|
|
12
|
+
initiator_role VARCHAR(16),
|
|
13
|
+
initiator JSONB NOT NULL,
|
|
14
|
+
source VARCHAR(32) NOT NULL,
|
|
15
|
+
executor JSONB NOT NULL,
|
|
16
|
+
tenant_id VARCHAR(255),
|
|
17
|
+
action VARCHAR(16) NOT NULL,
|
|
18
|
+
status VARCHAR(16) NOT NULL,
|
|
19
|
+
summary JSONB NOT NULL,
|
|
20
|
+
before JSONB,
|
|
21
|
+
after JSONB,
|
|
22
|
+
error_code VARCHAR(64),
|
|
23
|
+
duration_ms INTEGER NOT NULL,
|
|
24
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
await client.query(`
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_platform_audit_created
|
|
30
|
+
ON lattice_platform_audit(created_at DESC, id DESC)
|
|
31
|
+
`);
|
|
32
|
+
await client.query(`
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_platform_audit_initiator
|
|
34
|
+
ON lattice_platform_audit(initiator_user_id, created_at DESC)
|
|
35
|
+
`);
|
|
36
|
+
await client.query(`
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_platform_audit_tenant
|
|
38
|
+
ON lattice_platform_audit(tenant_id, created_at DESC)
|
|
39
|
+
WHERE tenant_id IS NOT NULL
|
|
40
|
+
`);
|
|
41
|
+
},
|
|
42
|
+
down: async (client) => {
|
|
43
|
+
await client.query("DROP TABLE IF EXISTS lattice_platform_audit");
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -7,12 +7,17 @@ import type { PoolConfig } from 'pg';
|
|
|
7
7
|
import {
|
|
8
8
|
McpServerConfigStore,
|
|
9
9
|
McpServerConfigEntry,
|
|
10
|
+
PersonalMcpServerConfigStore,
|
|
11
|
+
PersonalMcpServerConfigEntry,
|
|
10
12
|
CreateMcpServerConfigRequest,
|
|
11
13
|
UpdateMcpServerConfigRequest,
|
|
12
14
|
McpServerConfig,
|
|
13
15
|
} from '@axiom-lattice/protocols';
|
|
14
16
|
import { MigrationManager } from '../migrations/migration';
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
createMcpServerConfigsTable,
|
|
19
|
+
createPersonalMcpServerConfigsTable,
|
|
20
|
+
} from '../migrations/mcp_server_config_migrations';
|
|
16
21
|
import { encrypt, safeDecrypt } from '@axiom-lattice/core';
|
|
17
22
|
|
|
18
23
|
/**
|
|
@@ -38,6 +43,26 @@ export interface PostgreSQLMcpServerConfigStoreOptions {
|
|
|
38
43
|
autoMigrate?: boolean;
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
/** Row shape shared by the tenant and personal MCP config tables. */
|
|
47
|
+
interface McpConfigRow {
|
|
48
|
+
id: string;
|
|
49
|
+
tenant_id: string;
|
|
50
|
+
key: string;
|
|
51
|
+
name: string | null;
|
|
52
|
+
description: string | null;
|
|
53
|
+
config: any;
|
|
54
|
+
selected_tools: string[];
|
|
55
|
+
is_env_encrypted: boolean;
|
|
56
|
+
status: string;
|
|
57
|
+
created_at: Date;
|
|
58
|
+
updated_at: Date;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Row shape for `lattice_personal_mcp_server_configs`. */
|
|
62
|
+
interface PersonalMcpConfigRow extends McpConfigRow {
|
|
63
|
+
owner_assistant_id: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
/**
|
|
42
67
|
* PostgreSQL implementation of McpServerConfigStore
|
|
43
68
|
*
|
|
@@ -46,7 +71,8 @@ export interface PostgreSQLMcpServerConfigStoreOptions {
|
|
|
46
71
|
* - Automatic env encryption/decryption
|
|
47
72
|
* - Unique constraint on (tenant_id, key)
|
|
48
73
|
*/
|
|
49
|
-
export class PostgreSQLMcpServerConfigStore
|
|
74
|
+
export class PostgreSQLMcpServerConfigStore
|
|
75
|
+
implements McpServerConfigStore, PersonalMcpServerConfigStore {
|
|
50
76
|
private pool: Pool;
|
|
51
77
|
private migrationManager!: MigrationManager;
|
|
52
78
|
private initialized: boolean = false;
|
|
@@ -73,6 +99,7 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
73
99
|
|
|
74
100
|
this.migrationManager = new MigrationManager(this.pool);
|
|
75
101
|
this.migrationManager.register(createMcpServerConfigsTable);
|
|
102
|
+
this.migrationManager.register(createPersonalMcpServerConfigsTable);
|
|
76
103
|
|
|
77
104
|
// Auto-migrate by default
|
|
78
105
|
if (options.autoMigrate !== false) {
|
|
@@ -411,6 +438,309 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
411
438
|
return result.rows.length > 0;
|
|
412
439
|
}
|
|
413
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Get all personal MCP server configurations owned by one assistant.
|
|
443
|
+
*/
|
|
444
|
+
async getAllConfigsForAssistant(
|
|
445
|
+
tenantId: string,
|
|
446
|
+
ownerAssistantId: string,
|
|
447
|
+
): Promise<PersonalMcpServerConfigEntry[]> {
|
|
448
|
+
await this.ensureInitialized();
|
|
449
|
+
|
|
450
|
+
const result = await this.pool.query<PersonalMcpConfigRow>(
|
|
451
|
+
`
|
|
452
|
+
SELECT id, tenant_id, owner_assistant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at
|
|
453
|
+
FROM lattice_personal_mcp_server_configs
|
|
454
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2
|
|
455
|
+
ORDER BY created_at DESC
|
|
456
|
+
`,
|
|
457
|
+
[tenantId, ownerAssistantId],
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
return result.rows.map((row) => this.mapPersonalRowToEntry(row));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get every personal MCP server configuration across tenants.
|
|
465
|
+
*
|
|
466
|
+
* Operations/tests only; startup restoration must not call this.
|
|
467
|
+
*/
|
|
468
|
+
async getAllPersonalConfigsWithoutTenant(): Promise<PersonalMcpServerConfigEntry[]> {
|
|
469
|
+
await this.ensureInitialized();
|
|
470
|
+
|
|
471
|
+
const result = await this.pool.query<PersonalMcpConfigRow>(
|
|
472
|
+
`
|
|
473
|
+
SELECT id, tenant_id, owner_assistant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at
|
|
474
|
+
FROM lattice_personal_mcp_server_configs
|
|
475
|
+
ORDER BY created_at DESC
|
|
476
|
+
`,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
return result.rows.map((row) => this.mapPersonalRowToEntry(row));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Get one personal MCP server configuration by id.
|
|
484
|
+
*/
|
|
485
|
+
async getConfigByIdForAssistant(
|
|
486
|
+
tenantId: string,
|
|
487
|
+
ownerAssistantId: string,
|
|
488
|
+
id: string,
|
|
489
|
+
): Promise<PersonalMcpServerConfigEntry | null> {
|
|
490
|
+
await this.ensureInitialized();
|
|
491
|
+
|
|
492
|
+
const result = await this.pool.query<PersonalMcpConfigRow>(
|
|
493
|
+
`
|
|
494
|
+
SELECT id, tenant_id, owner_assistant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at
|
|
495
|
+
FROM lattice_personal_mcp_server_configs
|
|
496
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2 AND id = $3
|
|
497
|
+
`,
|
|
498
|
+
[tenantId, ownerAssistantId, id],
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (result.rows.length === 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return this.mapPersonalRowToEntry(result.rows[0]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Get one personal MCP server configuration by business key.
|
|
510
|
+
*/
|
|
511
|
+
async getConfigByKeyForAssistant(
|
|
512
|
+
tenantId: string,
|
|
513
|
+
ownerAssistantId: string,
|
|
514
|
+
key: string,
|
|
515
|
+
): Promise<PersonalMcpServerConfigEntry | null> {
|
|
516
|
+
await this.ensureInitialized();
|
|
517
|
+
|
|
518
|
+
const result = await this.pool.query<PersonalMcpConfigRow>(
|
|
519
|
+
`
|
|
520
|
+
SELECT id, tenant_id, owner_assistant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at
|
|
521
|
+
FROM lattice_personal_mcp_server_configs
|
|
522
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2 AND key = $3
|
|
523
|
+
`,
|
|
524
|
+
[tenantId, ownerAssistantId, key],
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
if (result.rows.length === 0) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return this.mapPersonalRowToEntry(result.rows[0]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Create a personal MCP server configuration.
|
|
536
|
+
*/
|
|
537
|
+
async createConfigForAssistant(
|
|
538
|
+
tenantId: string,
|
|
539
|
+
ownerAssistantId: string,
|
|
540
|
+
id: string,
|
|
541
|
+
data: CreateMcpServerConfigRequest,
|
|
542
|
+
): Promise<PersonalMcpServerConfigEntry> {
|
|
543
|
+
await this.ensureInitialized();
|
|
544
|
+
|
|
545
|
+
const now = new Date();
|
|
546
|
+
const nowString = now.toISOString();
|
|
547
|
+
const { config: configWithEncryptedEnv, isEnvEncrypted } =
|
|
548
|
+
this.encryptEnvInConfig(data.config);
|
|
549
|
+
|
|
550
|
+
await this.pool.query(
|
|
551
|
+
`
|
|
552
|
+
INSERT INTO lattice_personal_mcp_server_configs (id, tenant_id, owner_assistant_id, key, name, description, config, selected_tools, is_env_encrypted, status, created_at, updated_at)
|
|
553
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::timestamp, $12::timestamp)
|
|
554
|
+
ON CONFLICT (tenant_id, owner_assistant_id, id) DO UPDATE SET
|
|
555
|
+
key = EXCLUDED.key,
|
|
556
|
+
name = EXCLUDED.name,
|
|
557
|
+
description = EXCLUDED.description,
|
|
558
|
+
config = EXCLUDED.config,
|
|
559
|
+
selected_tools = EXCLUDED.selected_tools,
|
|
560
|
+
is_env_encrypted = EXCLUDED.is_env_encrypted,
|
|
561
|
+
status = EXCLUDED.status,
|
|
562
|
+
updated_at = EXCLUDED.updated_at
|
|
563
|
+
`,
|
|
564
|
+
[
|
|
565
|
+
id,
|
|
566
|
+
tenantId,
|
|
567
|
+
ownerAssistantId,
|
|
568
|
+
data.key,
|
|
569
|
+
data.name || null,
|
|
570
|
+
data.description || null,
|
|
571
|
+
JSON.stringify(configWithEncryptedEnv),
|
|
572
|
+
JSON.stringify(data.selectedTools || []),
|
|
573
|
+
isEnvEncrypted,
|
|
574
|
+
'disconnected',
|
|
575
|
+
nowString,
|
|
576
|
+
nowString,
|
|
577
|
+
],
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
id,
|
|
582
|
+
tenantId,
|
|
583
|
+
ownerAssistantId,
|
|
584
|
+
key: data.key,
|
|
585
|
+
config: data.config,
|
|
586
|
+
selectedTools: data.selectedTools || [],
|
|
587
|
+
isEnvEncrypted,
|
|
588
|
+
status: 'disconnected',
|
|
589
|
+
name: data.name,
|
|
590
|
+
description: data.description,
|
|
591
|
+
createdAt: now,
|
|
592
|
+
updatedAt: now,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Update a personal MCP server configuration.
|
|
598
|
+
*/
|
|
599
|
+
async updateConfigForAssistant(
|
|
600
|
+
tenantId: string,
|
|
601
|
+
ownerAssistantId: string,
|
|
602
|
+
id: string,
|
|
603
|
+
updates: Partial<UpdateMcpServerConfigRequest>,
|
|
604
|
+
): Promise<PersonalMcpServerConfigEntry | null> {
|
|
605
|
+
await this.ensureInitialized();
|
|
606
|
+
|
|
607
|
+
const existing = await this.getConfigByIdForAssistant(
|
|
608
|
+
tenantId,
|
|
609
|
+
ownerAssistantId,
|
|
610
|
+
id,
|
|
611
|
+
);
|
|
612
|
+
if (!existing) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const updateData: Record<string, any> = {};
|
|
617
|
+
|
|
618
|
+
if (updates.key !== undefined) {
|
|
619
|
+
updateData.key = updates.key;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (updates.name !== undefined) {
|
|
623
|
+
updateData.name = updates.name || null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (updates.description !== undefined) {
|
|
627
|
+
updateData.description = updates.description || null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (updates.config !== undefined) {
|
|
631
|
+
const { config: configWithEncryptedEnv, isEnvEncrypted } =
|
|
632
|
+
this.encryptEnvInConfig(updates.config);
|
|
633
|
+
updateData.config = JSON.stringify(configWithEncryptedEnv);
|
|
634
|
+
updateData.is_env_encrypted = isEnvEncrypted;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (updates.selectedTools !== undefined) {
|
|
638
|
+
updateData.selected_tools = JSON.stringify(updates.selectedTools);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (updates.status !== undefined) {
|
|
642
|
+
updateData.status = updates.status;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (Object.keys(updateData).length === 0) {
|
|
646
|
+
return existing;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
updateData.updated_at = new Date().toISOString();
|
|
650
|
+
|
|
651
|
+
const fields = Object.keys(updateData);
|
|
652
|
+
const values = Object.values(updateData);
|
|
653
|
+
values.push(tenantId);
|
|
654
|
+
values.push(ownerAssistantId);
|
|
655
|
+
values.push(id);
|
|
656
|
+
|
|
657
|
+
const setClauses = fields.map((field, index) => {
|
|
658
|
+
if (field === 'updated_at') {
|
|
659
|
+
return `${field} = $${index + 1}::timestamp`;
|
|
660
|
+
}
|
|
661
|
+
return `${field} = $${index + 1}`;
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
const whereTenantIndex = fields.length + 1;
|
|
665
|
+
const whereOwnerIndex = fields.length + 2;
|
|
666
|
+
const whereIdIndex = fields.length + 3;
|
|
667
|
+
|
|
668
|
+
const sql = `
|
|
669
|
+
UPDATE lattice_personal_mcp_server_configs
|
|
670
|
+
SET ${setClauses.join(', ')}
|
|
671
|
+
WHERE tenant_id = $${whereTenantIndex}
|
|
672
|
+
AND owner_assistant_id = $${whereOwnerIndex}
|
|
673
|
+
AND id = $${whereIdIndex}
|
|
674
|
+
`;
|
|
675
|
+
|
|
676
|
+
await this.pool.query(sql, values);
|
|
677
|
+
|
|
678
|
+
return this.getConfigByIdForAssistant(tenantId, ownerAssistantId, id);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Delete one personal MCP server configuration by id.
|
|
683
|
+
*/
|
|
684
|
+
async deleteConfigForAssistant(
|
|
685
|
+
tenantId: string,
|
|
686
|
+
ownerAssistantId: string,
|
|
687
|
+
id: string,
|
|
688
|
+
): Promise<boolean> {
|
|
689
|
+
await this.ensureInitialized();
|
|
690
|
+
|
|
691
|
+
const result = await this.pool.query(
|
|
692
|
+
`
|
|
693
|
+
DELETE FROM lattice_personal_mcp_server_configs
|
|
694
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2 AND id = $3
|
|
695
|
+
`,
|
|
696
|
+
[tenantId, ownerAssistantId, id],
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Delete every personal MCP server configuration owned by one assistant.
|
|
704
|
+
*/
|
|
705
|
+
async deleteConfigsForAssistant(
|
|
706
|
+
tenantId: string,
|
|
707
|
+
ownerAssistantId: string,
|
|
708
|
+
): Promise<number> {
|
|
709
|
+
await this.ensureInitialized();
|
|
710
|
+
|
|
711
|
+
const result = await this.pool.query(
|
|
712
|
+
`
|
|
713
|
+
DELETE FROM lattice_personal_mcp_server_configs
|
|
714
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2
|
|
715
|
+
`,
|
|
716
|
+
[tenantId, ownerAssistantId],
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
return result.rowCount ?? 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Check whether a personal MCP server configuration exists.
|
|
724
|
+
*/
|
|
725
|
+
async hasConfigForAssistant(
|
|
726
|
+
tenantId: string,
|
|
727
|
+
ownerAssistantId: string,
|
|
728
|
+
id: string,
|
|
729
|
+
): Promise<boolean> {
|
|
730
|
+
await this.ensureInitialized();
|
|
731
|
+
|
|
732
|
+
const result = await this.pool.query(
|
|
733
|
+
`
|
|
734
|
+
SELECT 1 FROM lattice_personal_mcp_server_configs
|
|
735
|
+
WHERE tenant_id = $1 AND owner_assistant_id = $2 AND id = $3
|
|
736
|
+
LIMIT 1
|
|
737
|
+
`,
|
|
738
|
+
[tenantId, ownerAssistantId, id],
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
return result.rows.length > 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
414
744
|
/**
|
|
415
745
|
* Dispose resources and close the connection pool
|
|
416
746
|
*/
|
|
@@ -433,19 +763,7 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
433
763
|
* Map database row to McpServerConfigEntry object
|
|
434
764
|
* Automatically decrypts env values if present
|
|
435
765
|
*/
|
|
436
|
-
private mapRowToEntry(row: {
|
|
437
|
-
id: string;
|
|
438
|
-
tenant_id: string;
|
|
439
|
-
key: string;
|
|
440
|
-
name: string | null;
|
|
441
|
-
description: string | null;
|
|
442
|
-
config: any;
|
|
443
|
-
selected_tools: string[];
|
|
444
|
-
is_env_encrypted: boolean;
|
|
445
|
-
status: string;
|
|
446
|
-
created_at: Date;
|
|
447
|
-
updated_at: Date;
|
|
448
|
-
}): McpServerConfigEntry {
|
|
766
|
+
private mapRowToEntry(row: McpConfigRow): McpServerConfigEntry {
|
|
449
767
|
const config: McpServerConfig = typeof row.config === 'string'
|
|
450
768
|
? JSON.parse(row.config)
|
|
451
769
|
: row.config;
|
|
@@ -483,6 +801,16 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
483
801
|
};
|
|
484
802
|
}
|
|
485
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Map a personal table row, tagging the owning assistant.
|
|
806
|
+
*/
|
|
807
|
+
private mapPersonalRowToEntry(row: PersonalMcpConfigRow): PersonalMcpServerConfigEntry {
|
|
808
|
+
return {
|
|
809
|
+
...this.mapRowToEntry(row),
|
|
810
|
+
ownerAssistantId: row.owner_assistant_id,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
486
814
|
/**
|
|
487
815
|
* Encrypt env values in config before storing
|
|
488
816
|
*/
|