Some checks failed
CI/CD / lint (pull_request) Successful in 1m25s
CI/CD / test (pull_request) Successful in 1m32s
CI/CD / typecheck (pull_request) Successful in 3m0s
CI/CD / smoke (pull_request) Failing after 2m0s
CI/CD / build (pull_request) Successful in 4m34s
CI/CD / publish (pull_request) Has been skipped
Migrating docmost and my-home-assistant, both rendered their secret and neither picked it up. The pod spec showed why: command: (empty) args: ["node","build/index.js"] server.entrypoint: (unset) wrapCommand consulted only `server.entrypoint` for non-package servers, so with `entrypoint` unset it returned undefined, the wrapper was skipped entirely, and the container ran its normal command — which never sourced /vault/secrets/<name>. The failure is silent by construction: the agent init container succeeds, the file is there, and the server simply starts with empty credentials. An explicit `command` on an image server is already a complete command line — mcpd's exec mode would run exactly it — so it should be wrapped verbatim. `entrypoint` is only needed when there is no command at all and the image's own ENTRYPOINT would take over. Now branches on the three real shapes: package server (prepend the runner entrypoint mcpd owns), image + command (use verbatim), image only (require the declared entrypoint). Seven tests, one per shape plus the two undefined cases. Reintroducing the old logic fails two of them — checked before keeping. Both servers were rolled back to secretDelivery: env and are healthy; they can migrate once this ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018vybEitX4FykeMatKe5Xki
59 lines
2.9 KiB
TypeScript
59 lines
2.9 KiB
TypeScript
/**
|
|
* Which argv the injector wrapper wraps, by server shape.
|
|
*
|
|
* Getting this wrong is SILENT: wrapCommand returns undefined, the wrapper is
|
|
* skipped, the agent still renders /vault/secrets/<name>, nothing sources it,
|
|
* and the server starts with empty credentials. Observed live on docmost and
|
|
* my-home-assistant, which carry a `command` but no `entrypoint`.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { ServerIdentityService } from '../src/services/server-identity.service.js';
|
|
import type { SecretBackendService } from '../src/services/secret-backend.service.js';
|
|
|
|
const svc = new ServerIdentityService(
|
|
{} as unknown as SecretBackendService,
|
|
{ namespace: 'mcpctl-servers', ensure: async () => undefined, remove: async () => undefined },
|
|
);
|
|
|
|
const withSecret = { env: [{ name: 'T', valueFrom: { secretRef: { name: 'creds', key: 'K' } } }] };
|
|
/** The wrapper is `sh -c <script> arg0 arg1...`; argv starts at index 3. */
|
|
const argvOf = (r: string[] | undefined): string[] | undefined => r?.slice(3);
|
|
|
|
describe('wrapCommand argv by server shape', () => {
|
|
it('prepends the node runner entrypoint for a package server', () => {
|
|
const r = svc.wrapCommand({ ...withSecret, packageName: '@leval/mcp-grafana', runtime: 'node', entrypoint: null } as never, ['@leval/mcp-grafana']);
|
|
expect(argvOf(r)).toEqual(['npx', '-y', '@leval/mcp-grafana']);
|
|
});
|
|
|
|
it('prepends uvx for a python package server', () => {
|
|
const r = svc.wrapCommand({ ...withSecret, packageName: 'mcp-searxng', runtime: 'python', entrypoint: null } as never, ['mcp-searxng']);
|
|
expect(argvOf(r)).toEqual(['uvx', 'mcp-searxng']);
|
|
});
|
|
|
|
it('uses an image server\'s command verbatim — prepending anything breaks it', () => {
|
|
// The docmost/home-assistant regression: this used to return undefined.
|
|
const r = svc.wrapCommand({ ...withSecret, packageName: null, entrypoint: null } as never, ['node', 'build/index.js']);
|
|
expect(argvOf(r)).toEqual(['node', 'build/index.js']);
|
|
});
|
|
|
|
it('falls back to the declared entrypoint for an image server with no command', () => {
|
|
const r = svc.wrapCommand({ ...withSecret, packageName: null, entrypoint: ['/usr/local/bin/gitea-mcp'] } as never, undefined);
|
|
expect(argvOf(r)).toEqual(['/usr/local/bin/gitea-mcp']);
|
|
});
|
|
|
|
it('returns undefined when there is genuinely nothing to run', () => {
|
|
expect(svc.wrapCommand({ ...withSecret, packageName: null, entrypoint: null } as never, undefined)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined for a server with no secret refs', () => {
|
|
expect(svc.wrapCommand({ env: [], packageName: 'p', runtime: 'node', entrypoint: null } as never, ['p'])).toBeUndefined();
|
|
});
|
|
|
|
it('always sources before exec, whatever the shape', () => {
|
|
const r = svc.wrapCommand({ ...withSecret, packageName: null, entrypoint: null } as never, ['node', 'x.js']);
|
|
expect(r?.[0]).toBe('/bin/sh');
|
|
expect(r?.[2]).toContain('. /vault/secrets/creds');
|
|
expect(r?.[2]).toContain('exec "$0" "$@"');
|
|
});
|
|
});
|