fix(secrets): the injector wrapper must replace the entrypoint, not extend it
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m23s
CI/CD / lint (pull_request) Successful in 2m35s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / build (pull_request) Successful in 2m24s
CI/CD / smoke (pull_request) Failing after 3m5s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m23s
CI/CD / lint (pull_request) Successful in 2m35s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / build (pull_request) Successful in 2m24s
CI/CD / smoke (pull_request) Failing after 3m5s
CI/CD / publish (pull_request) Has been skipped
Caught migrating a real server: the pod crashlooped with
`.: cannot open /vault/secrets/grafana-creds`, and the reason was in the
generated spec:
args: ["/bin/sh","-c",". /vault/secrets/grafana-creds; exec \"$0\" \"$@\"",
"@leval/mcp-grafana"]
mcpd deliberately maps ContainerSpec.command -> k8s `args` so a package
server keeps its runner image's ENTRYPOINT (`npx -y`, `uvx`). Putting the
sourcing wrapper there meant the pod actually ran
`npx -y /bin/sh -c '...'` — npx trying to resolve a package called
/bin/sh. The agent had rendered the file correctly; nothing ever sourced it.
Adds `ContainerSpec.entrypoint`, which maps to k8s `command` and so
REPLACES the image entrypoint, and has wrapCommand fold that entrypoint
into the argv it returns (`npx -y` / `uvx` for package servers, the
server's own `entrypoint` field for dockerImage servers — already required
at validation for exactly this reason).
Two tests pin it: a wrapped server emits `command` and no `args`; an
unwrapped one still emits `args` and no `command`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vybEitX4FykeMatKe5Xki
This commit is contained in:
@@ -136,3 +136,23 @@ describe('shell quoting survives adversarial values', () => {
|
||||
expect(out).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the wrapper must REPLACE the image entrypoint, not extend it', () => {
|
||||
// Regression: mcpd maps ContainerSpec.command -> k8s `args` so the runner
|
||||
// image's `npx -y` / `uvx` entrypoint still runs. Emitting the wrapper there
|
||||
// meant the pod actually ran `npx -y /bin/sh -c '...'`, which crashlooped.
|
||||
it('emits container.command (entrypoint) and no args', () => {
|
||||
const pod = generatePodSpec({
|
||||
name: 'g', image: 'runner',
|
||||
entrypoint: ['/bin/sh', '-c', '. /vault/secrets/s; exec "$0" "$@"', 'npx', '-y', '@leval/mcp-grafana'],
|
||||
} as ContainerSpec, 'mcpctl-servers');
|
||||
expect(pod.spec.containers[0]?.command?.[0]).toBe('/bin/sh');
|
||||
expect(pod.spec.containers[0]?.args).toBeUndefined();
|
||||
});
|
||||
|
||||
it('leaves the normal entrypoint+args path alone when not wrapping', () => {
|
||||
const pod = generatePodSpec({ name: 'g', image: 'runner', command: ['@leval/mcp-grafana'] } as ContainerSpec, 'mcpctl-servers');
|
||||
expect(pod.spec.containers[0]?.command).toBeUndefined();
|
||||
expect(pod.spec.containers[0]?.args).toEqual(['@leval/mcp-grafana']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user