Compare commits
2 Commits
feat/ungat
...
fix/ungate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe00f42bce | ||
| 7bf9d49a02 |
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import type { PrismaClient } from '@prisma/client';
|
import type { PrismaClient } from '@prisma/client';
|
||||||
import { SYSTEM_SETTINGS_SECRET } from './system-settings.js';
|
import { SYSTEM_SETTINGS_SECRET } from './system-settings.js';
|
||||||
|
import { DEFAULT_PLAINTEXT_BACKEND_NAME } from './secret-backends.js';
|
||||||
|
|
||||||
/** Well-known project whose servers are exposed under every project, pre-gate. */
|
/** Well-known project whose servers are exposed under every project, pre-gate. */
|
||||||
export const UNGATED_GLOBAL_PROJECT_NAME = 'ungated-global';
|
export const UNGATED_GLOBAL_PROJECT_NAME = 'ungated-global';
|
||||||
@@ -61,10 +62,22 @@ async function markSeeded(prisma: PrismaClient): Promise<void> {
|
|||||||
data: { data, keyNames, version: existing.version + 1 },
|
data: { data, keyNames, version: existing.version + 1 },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// backendId '' is repointed at the default plaintext backend by
|
// The Secret.backendId FK is enforced at insert time, so a fresh row
|
||||||
// bootstrapSecretBackends — same convention as pre-existing rows.
|
// must point at a real backend. bootstrapUngatedGlobal runs after
|
||||||
|
// bootstrapSecretBackends, so the default plaintext backend exists;
|
||||||
|
// fall back to any backend if the name ever changes.
|
||||||
|
const backend =
|
||||||
|
(await prisma.secretBackend.findUnique({ where: { name: DEFAULT_PLAINTEXT_BACKEND_NAME } }))
|
||||||
|
?? (await prisma.secretBackend.findFirst({ where: { isDefault: true } }))
|
||||||
|
?? (await prisma.secretBackend.findFirst());
|
||||||
|
if (!backend) {
|
||||||
|
// No backend at all — skip marking rather than crash startup. The seed
|
||||||
|
// above still ran; worst case it re-runs (idempotent upserts) next boot.
|
||||||
|
console.warn('ungated-global: no SecretBackend found, seed marker not written');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await prisma.secret.create({
|
await prisma.secret.create({
|
||||||
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames },
|
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames, backendId: backend.id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,8 +414,6 @@ async function main(): Promise<void> {
|
|||||||
await bootstrapSystemProject(prisma);
|
await bootstrapSystemProject(prisma);
|
||||||
// PR-4: bootstrap system-owned global skills (e.g. propose-learnings).
|
// PR-4: bootstrap system-owned global skills (e.g. propose-learnings).
|
||||||
await bootstrapSystemSkills(prisma);
|
await bootstrapSystemSkills(prisma);
|
||||||
// Seed-once ungated-global project (default calcmcp server, deletable).
|
|
||||||
await bootstrapUngatedGlobal(prisma);
|
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
const serverRepo = new McpServerRepository(prisma);
|
const serverRepo = new McpServerRepository(prisma);
|
||||||
@@ -438,6 +436,11 @@ async function main(): Promise<void> {
|
|||||||
// pre-existing `Secret` rows are pointed at it. Idempotent per run.
|
// pre-existing `Secret` rows are pointed at it. Idempotent per run.
|
||||||
await bootstrapSecretBackends(prisma);
|
await bootstrapSecretBackends(prisma);
|
||||||
|
|
||||||
|
// Seed-once ungated-global project (default calcmcp server, deletable).
|
||||||
|
// MUST run after bootstrapSecretBackends: its seed marker lives in the
|
||||||
|
// mcpctl-system-settings Secret, whose creation needs a valid backend FK.
|
||||||
|
await bootstrapUngatedGlobal(prisma);
|
||||||
|
|
||||||
// CUID detection for RBAC name resolution
|
// CUID detection for RBAC name resolution
|
||||||
const CUID_RE = /^c[^\s-]{8,}$/i;
|
const CUID_RE = /^c[^\s-]{8,}$/i;
|
||||||
const nameResolvers: Record<string, { findById(id: string): Promise<{ name: string } | null> }> = {
|
const nameResolvers: Record<string, { findById(id: string): Promise<{ name: string } | null> }> = {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ function mockPrisma(opts: { settingsData?: Record<string, unknown> | null } = {}
|
|||||||
update: vi.fn(async () => ({})),
|
update: vi.fn(async () => ({})),
|
||||||
create: vi.fn(async () => ({})),
|
create: vi.fn(async () => ({})),
|
||||||
},
|
},
|
||||||
|
secretBackend: {
|
||||||
|
findUnique: vi.fn(async () => ({ id: 'backend-default', name: 'default', isDefault: true })),
|
||||||
|
findFirst: vi.fn(async () => ({ id: 'backend-default', name: 'default', isDefault: true })),
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
upsert: vi.fn(async () => ({ id: 'sys-user-id', email: 'system@mcpctl.local' })),
|
upsert: vi.fn(async () => ({ id: 'sys-user-id', email: 'system@mcpctl.local' })),
|
||||||
},
|
},
|
||||||
@@ -68,11 +72,12 @@ describe('bootstrapUngatedGlobal', () => {
|
|||||||
it('marks the seed as done in the system settings secret', async () => {
|
it('marks the seed as done in the system settings secret', async () => {
|
||||||
await bootstrapUngatedGlobal(prisma);
|
await bootstrapUngatedGlobal(prisma);
|
||||||
|
|
||||||
// Settings secret didn't exist → created with the marker key
|
// Settings secret didn't exist → created with the marker key and a valid backend FK
|
||||||
expect(prisma.secret.create).toHaveBeenCalledWith(
|
expect(prisma.secret.create).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
name: SYSTEM_SETTINGS_SECRET,
|
name: SYSTEM_SETTINGS_SECRET,
|
||||||
|
backendId: 'backend-default',
|
||||||
data: expect.objectContaining({ [SETTING_UNGATED_GLOBAL_SEEDED]: true }),
|
data: expect.objectContaining({ [SETTING_UNGATED_GLOBAL_SEEDED]: true }),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user