Compare commits

...

2 Commits

Author SHA1 Message Date
Michal
fe00f42bce fix(ungated-global): create settings secret with a valid backend FK
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m19s
CI/CD / test (pull_request) Failing after 27s
CI/CD / lint (pull_request) Successful in 2m50s
CI/CD / smoke (pull_request) Has been skipped
CI/CD / build (pull_request) Has been skipped
CI/CD / publish (pull_request) Has been skipped
bootstrapUngatedGlobal wrote its seed marker into the mcpctl-system-settings
Secret, creating the row with the default backendId '' when it did not exist.
The Secret_backendId_fkey constraint is enforced at insert time, so on a DB
where that secret was absent, mcpd crash-looped on startup (P2003).

Fix: run the seed after bootstrapSecretBackends (so the default plaintext
backend exists) and set backendId to the resolved default on create. If no
backend exists at all, skip writing the marker rather than crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 01:57:37 +01:00
7bf9d49a02 Merge pull request 'ungated-global: system project whose servers ride every project, pre-gate' (#137) from feat/ungated-global into main
Some checks failed
CI/CD / lint (push) Successful in 1m13s
CI/CD / test (push) Successful in 1m25s
CI/CD / typecheck (push) Successful in 3m6s
CI/CD / smoke (push) Failing after 1m58s
CI/CD / build (push) Successful in 2m11s
CI/CD / publish (push) Has been skipped
2026-10-03 00:46:08 +00:00
3 changed files with 27 additions and 6 deletions

View File

@@ -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 },
}); });
} }
} }

View File

@@ -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> }> = {

View File

@@ -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 }),
}), }),
}), }),