Merge pull request 'fix(ungated-global): create settings secret with a valid backend FK' (#138) from fix/ungated-global-secret-fk into main
Some checks failed
CI/CD / lint (push) Successful in 1m18s
CI/CD / test (push) Successful in 1m23s
CI/CD / typecheck (push) Successful in 3m15s
CI/CD / smoke (push) Failing after 1m56s
CI/CD / build (push) Successful in 5m7s
CI/CD / publish (push) Has been skipped

This commit was merged in pull request #138.
This commit is contained in:
2026-10-03 00:58:57 +00:00
3 changed files with 27 additions and 6 deletions

View File

@@ -15,6 +15,7 @@
import type { PrismaClient } from '@prisma/client';
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. */
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 },
});
} else {
// backendId '' is repointed at the default plaintext backend by
// bootstrapSecretBackends — same convention as pre-existing rows.
// The Secret.backendId FK is enforced at insert time, so a fresh row
// 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({
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames },
data: { name: SYSTEM_SETTINGS_SECRET, data, keyNames, backendId: backend.id },
});
}
}

View File

@@ -414,8 +414,6 @@ async function main(): Promise<void> {
await bootstrapSystemProject(prisma);
// PR-4: bootstrap system-owned global skills (e.g. propose-learnings).
await bootstrapSystemSkills(prisma);
// Seed-once ungated-global project (default calcmcp server, deletable).
await bootstrapUngatedGlobal(prisma);
// Repositories
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.
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
const CUID_RE = /^c[^\s-]{8,}$/i;
const nameResolvers: Record<string, { findById(id: string): Promise<{ name: string } | null> }> = {

View File

@@ -18,6 +18,10 @@ function mockPrisma(opts: { settingsData?: Record<string, unknown> | null } = {}
update: 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: {
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 () => {
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.objectContaining({
data: expect.objectContaining({
name: SYSTEM_SETTINGS_SECRET,
backendId: 'backend-default',
data: expect.objectContaining({ [SETTING_UNGATED_GLOBAL_SEEDED]: true }),
}),
}),