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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user