From 628ceaabb5f8561383a5ea33fa86439b3270d3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= Date: Mon, 18 May 2026 02:33:32 +0100 Subject: [PATCH] fix(ssh): use bare mock-agent name so the remote PATH resolves it For SSH the agent runs on a remote host whose filesystem doesn't share paths with the kandev backend's, so the host-absolute mock-agent path configureMockAgent computes can't apply. Extend the bare-name branch in MockAgent.BuildCommand to include RuntimeSSH (the SSH e2e image already bakes mock-agent into /usr/local/bin) and drop the now-redundant SSH e2e bind-mount that worked around this by mirroring the host path into the container. --- apps/backend/internal/agent/agents/mock.go | 10 +++++++++- apps/backend/internal/agent/agents/mock_test.go | 12 +++++++----- apps/web/e2e/helpers/ssh.ts | 16 ---------------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/apps/backend/internal/agent/agents/mock.go b/apps/backend/internal/agent/agents/mock.go index 3e77cb5ea5..ec4d8a8b9d 100644 --- a/apps/backend/internal/agent/agents/mock.go +++ b/apps/backend/internal/agent/agents/mock.go @@ -6,6 +6,7 @@ import ( "time" "github.com/kandev/kandev/internal/agent/usage" + "github.com/kandev/kandev/internal/agentruntime" "github.com/kandev/kandev/pkg/agent" ) @@ -139,12 +140,19 @@ func (a *MockAgent) BuildCommand(opts CommandOptions) Command { // has /usr/local/bin on PATH and container.go bind-mounts the // linux/amd64 mock-agent there, so the bare name resolves correctly. // + // SSH is the same shape: the agent runs on a remote host whose + // filesystem doesn't share paths with the kandev backend's, so the + // host-absolute binary path can't apply. The SSH e2e image bakes + // mock-agent at /usr/local/bin/mock-agent; production SSH expects + // users to install agents into the remote's $PATH (real agents like + // Claude use `npx` and never hit this branch). + // // For host runtimes (standalone) we prefer the absolute host path // that configureMockAgent computed via os.Executable() — that's the // only way the host path works in dev mode (where the shell's $PATH // doesn't include apps/backend/bin). Fall back to the bare name when // no path is set: e2e fixtures prepend the kandev bin dir to PATH. - if opts.Runtime.IsContainerized() { + if opts.Runtime.IsContainerized() || opts.Runtime == agentruntime.RuntimeSSH { return Cmd(mockAgentDefaultID).Build() } binary := mockAgentDefaultID diff --git a/apps/backend/internal/agent/agents/mock_test.go b/apps/backend/internal/agent/agents/mock_test.go index a52fe5c050..20c6c5be7e 100644 --- a/apps/backend/internal/agent/agents/mock_test.go +++ b/apps/backend/internal/agent/agents/mock_test.go @@ -36,11 +36,12 @@ func TestMockAgent_BuildCommand_NoResumeFlag(t *testing.T) { // TestMockAgent_BuildCommand_ContainerizedUsesBareName pins the docker // branch of BuildCommand: when opts.Runtime is containerized (docker, -// remote_docker, sprites), MockAgent must emit the bare binary name so -// PATH lookup inside the container resolves to the bind-mounted -// linux/amd64 mock-agent at /usr/local/bin. Regressing this would -// cause docker e2e to fail with `exec: "": not found`, -// which is exactly the bug commit `8518f65` fixed. +// remote_docker, sprites) or SSH, MockAgent must emit the bare binary +// name so PATH lookup on the remote resolves to the bind-mounted (or +// pre-baked) linux/amd64 mock-agent at /usr/local/bin. Regressing this +// would cause docker e2e to fail with `exec: "": not +// found` (the bug commit `8518f65` fixed) and SSH e2e with the same +// symptom on the remote host. func TestMockAgent_BuildCommand_ContainerizedUsesBareName(t *testing.T) { a := NewMockAgent() a.SetBinaryPath("/Users/dev/kandev/apps/backend/bin/mock-agent") @@ -48,6 +49,7 @@ func TestMockAgent_BuildCommand_ContainerizedUsesBareName(t *testing.T) { agentruntime.RuntimeDocker, agentruntime.RuntimeRemoteDocker, agentruntime.RuntimeSprites, + agentruntime.RuntimeSSH, } { t.Run(string(rt), func(t *testing.T) { cmd := a.BuildCommand(CommandOptions{Runtime: rt}) diff --git a/apps/web/e2e/helpers/ssh.ts b/apps/web/e2e/helpers/ssh.ts index 9b662fbbbc..f908486423 100644 --- a/apps/web/e2e/helpers/ssh.ts +++ b/apps/web/e2e/helpers/ssh.ts @@ -53,20 +53,6 @@ export function startSSHServer( const publicKeyFile = `${identityFile}.pub`; generateKeypair(identityFile); - // kandev's orchestrator builds the agent command using KANDEV_MOCK_AGENT_LINUX_BINARY, - // which is set to the *host* absolute path of mock-agent-linux-amd64. - // Docker e2e gets away with using that path inside the container because - // it bind-mounts the same host file at the same path; for SSH e2e we have - // to do the equivalent — otherwise the kandev backend tells agentctl on - // the remote to exec a path that doesn't exist there. - const mockAgentHostPath = path.resolve(__dirname, "../../../backend/bin/mock-agent-linux-amd64"); - if (!fs.existsSync(mockAgentHostPath)) { - throw new Error( - `mock-agent-linux-amd64 not found at ${mockAgentHostPath}; ` + - `build it with 'make -C apps/backend build-mock-agent-linux'.`, - ); - } - // The container reads /authorized_keys on entry, so mount the public key // into that path read-only. removeContainerIfExists(containerName); @@ -96,8 +82,6 @@ export function startSSHServer( "-v", `${publicKeyFile}:/authorized_keys:ro`, "-v", - `${mockAgentHostPath}:${mockAgentHostPath}:ro`, - "-v", `${remoteHome}:/home/kandev/.kandev`, imageTag, ],