feat: replace profiles with kubernetes-style secrets

Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).

- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-22 18:40:58 +00:00
parent ede9e10990
commit 6d9a9f572c
77 changed files with 1014 additions and 1931 deletions

View File

@@ -2,8 +2,6 @@ import { describe, it, expect } from 'vitest';
import {
CreateMcpServerSchema,
UpdateMcpServerSchema,
CreateMcpProfileSchema,
UpdateMcpProfileSchema,
} from '../src/validation/index.js';
describe('CreateMcpServerSchema', () => {
@@ -14,7 +12,7 @@ describe('CreateMcpServerSchema', () => {
transport: 'STDIO',
});
expect(result.name).toBe('my-server');
expect(result.envTemplate).toEqual([]);
expect(result.env).toEqual([]);
});
it('rejects empty name', () => {
@@ -39,15 +37,40 @@ describe('CreateMcpServerSchema', () => {
expect(result.transport).toBe('STDIO');
});
it('validates envTemplate entries', () => {
it('validates env entries with inline value', () => {
const result = CreateMcpServerSchema.parse({
name: 'test',
envTemplate: [
{ name: 'API_KEY', description: 'The key', isSecret: true },
env: [
{ name: 'API_URL', value: 'https://example.com' },
],
});
expect(result.envTemplate).toHaveLength(1);
expect(result.envTemplate[0]?.isSecret).toBe(true);
expect(result.env).toHaveLength(1);
expect(result.env[0]?.value).toBe('https://example.com');
});
it('validates env entries with secretRef', () => {
const result = CreateMcpServerSchema.parse({
name: 'test',
env: [
{ name: 'API_KEY', valueFrom: { secretRef: { name: 'my-secret', key: 'api-key' } } },
],
});
expect(result.env).toHaveLength(1);
expect(result.env[0]?.valueFrom?.secretRef.name).toBe('my-secret');
});
it('rejects env entry with neither value nor valueFrom', () => {
expect(() => CreateMcpServerSchema.parse({
name: 'test',
env: [{ name: 'FOO' }],
})).toThrow();
});
it('rejects env entry with both value and valueFrom', () => {
expect(() => CreateMcpServerSchema.parse({
name: 'test',
env: [{ name: 'FOO', value: 'bar', valueFrom: { secretRef: { name: 'x', key: 'y' } } }],
})).toThrow();
});
it('rejects invalid transport', () => {
@@ -78,47 +101,3 @@ describe('UpdateMcpServerSchema', () => {
});
});
describe('CreateMcpProfileSchema', () => {
it('validates valid input', () => {
const result = CreateMcpProfileSchema.parse({
name: 'readonly',
serverId: 'server-123',
});
expect(result.name).toBe('readonly');
expect(result.permissions).toEqual([]);
expect(result.envOverrides).toEqual({});
});
it('rejects empty name', () => {
expect(() => CreateMcpProfileSchema.parse({ name: '', serverId: 'x' })).toThrow();
});
it('accepts permissions array', () => {
const result = CreateMcpProfileSchema.parse({
name: 'admin',
serverId: 'x',
permissions: ['read', 'write', 'delete'],
});
expect(result.permissions).toHaveLength(3);
});
it('accepts envOverrides', () => {
const result = CreateMcpProfileSchema.parse({
name: 'staging',
serverId: 'x',
envOverrides: { API_URL: 'https://staging.example.com' },
});
expect(result.envOverrides['API_URL']).toBe('https://staging.example.com');
});
});
describe('UpdateMcpProfileSchema', () => {
it('allows partial updates', () => {
const result = UpdateMcpProfileSchema.parse({ permissions: ['read'] });
expect(result.permissions).toEqual(['read']);
});
it('allows empty object', () => {
expect(UpdateMcpProfileSchema.parse({})).toBeDefined();
});
});