Skip to content

Commit 0168452

Browse files
committed
Reject tilde paths in SSH runtime srcBaseDir
SSH runtime now requires absolute paths for srcBaseDir to simplify logic and avoid ambiguity about which user's home directory is being referenced. Changes: - SSHRuntime constructor validates srcBaseDir doesn't start with tilde - Updated default SSH srcBaseDir from ~/cmux to /home/cmux - Updated all tests to use absolute paths or sshConfig.workdir - Replaced tilde-handling tests with tilde-rejection tests - Updated Runtime.ts documentation to clarify path requirements LocalRuntime continues to expand tilde paths at construction (previous commit), ensuring all runtime paths are fully resolved at the IPC boundary.
1 parent f4e7218 commit 0168452

File tree

6 files changed

+85
-82
lines changed

6 files changed

+85
-82
lines changed

src/runtime/Runtime.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
*
1515
* srcBaseDir (base directory for all workspaces):
1616
* - Where cmux stores ALL workspace directories
17-
* - Local: ~/.cmux/src
18-
* - SSH: /home/user/workspace (or custom remote path)
17+
* - Local: ~/.cmux/src (tilde expanded to full path by LocalRuntime)
18+
* - SSH: /home/user/workspace (must be absolute path, no tilde allowed)
1919
*
2020
* Workspace Path Computation:
2121
* {srcBaseDir}/{projectName}/{workspaceName}
@@ -27,14 +27,14 @@
2727
* Example: "feature-123" or "main"
2828
*
2929
* Full Example (Local):
30-
* srcBaseDir: ~/.cmux/src
30+
* srcBaseDir: ~/.cmux/src (expanded to /home/user/.cmux/src)
3131
* projectPath: /Users/me/git/my-project (local git repo)
3232
* projectName: my-project (extracted)
3333
* workspaceName: feature-123
34-
* → Workspace: ~/.cmux/src/my-project/feature-123
34+
* → Workspace: /home/user/.cmux/src/my-project/feature-123
3535
*
3636
* Full Example (SSH):
37-
* srcBaseDir: /home/user/workspace
37+
* srcBaseDir: /home/user/workspace (absolute path required)
3838
* projectPath: /Users/me/git/my-project (local git repo)
3939
* projectName: my-project (extracted)
4040
* workspaceName: feature-123

src/runtime/SSHRuntime.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { SSHRuntime } from "./SSHRuntime";
3+
4+
describe("SSHRuntime constructor", () => {
5+
it("should reject tilde in srcBaseDir", () => {
6+
expect(() => {
7+
new SSHRuntime({
8+
host: "example.com",
9+
srcBaseDir: "~/cmux",
10+
});
11+
}).toThrow(/cannot start with tilde/);
12+
});
13+
14+
it("should reject bare tilde in srcBaseDir", () => {
15+
expect(() => {
16+
new SSHRuntime({
17+
host: "example.com",
18+
srcBaseDir: "~",
19+
});
20+
}).toThrow(/cannot start with tilde/);
21+
});
22+
23+
it("should accept absolute paths in srcBaseDir", () => {
24+
expect(() => {
25+
new SSHRuntime({
26+
host: "example.com",
27+
srcBaseDir: "/home/user/cmux",
28+
});
29+
}).not.toThrow();
30+
});
31+
});

src/runtime/SSHRuntime.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ export class SSHRuntime implements Runtime {
6060
private readonly controlPath: string;
6161

6262
constructor(config: SSHRuntimeConfig) {
63+
// Reject tilde paths - require explicit full paths for SSH
64+
// Rationale: Simplifies logic and avoids ambiguity about which user's home directory
65+
if (config.srcBaseDir.startsWith("~")) {
66+
throw new Error(
67+
`SSH runtime srcBaseDir cannot start with tilde. ` +
68+
`Use full path (e.g., /home/username/cmux instead of ~/cmux)`
69+
);
70+
}
71+
6372
this.config = config;
6473
// Generate unique control path for SSH connection multiplexing
6574
// This allows multiple SSH sessions to reuse a single TCP connection

src/utils/chatCommands.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe("parseRuntimeString", () => {
1818
expect(result).toEqual({
1919
type: "ssh",
2020
host: "user@host",
21-
srcBaseDir: "~/cmux",
21+
srcBaseDir: "/home/cmux",
2222
});
2323
});
2424

@@ -27,7 +27,7 @@ describe("parseRuntimeString", () => {
2727
expect(result).toEqual({
2828
type: "ssh",
2929
host: "User@Host.Example.Com",
30-
srcBaseDir: "~/cmux",
30+
srcBaseDir: "/home/cmux",
3131
});
3232
});
3333

@@ -36,7 +36,7 @@ describe("parseRuntimeString", () => {
3636
expect(result).toEqual({
3737
type: "ssh",
3838
host: "user@host",
39-
srcBaseDir: "~/cmux",
39+
srcBaseDir: "/home/cmux",
4040
});
4141
});
4242

@@ -50,7 +50,7 @@ describe("parseRuntimeString", () => {
5050
expect(result).toEqual({
5151
type: "ssh",
5252
host: "hostname",
53-
srcBaseDir: "~/cmux",
53+
srcBaseDir: "/home/cmux",
5454
});
5555
});
5656

@@ -59,7 +59,7 @@ describe("parseRuntimeString", () => {
5959
expect(result).toEqual({
6060
type: "ssh",
6161
host: "dev.example.com",
62-
srcBaseDir: "~/cmux",
62+
srcBaseDir: "/home/cmux",
6363
});
6464
});
6565

src/utils/chatCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function parseRuntimeString(
5656
return {
5757
type: RUNTIME_MODE.SSH,
5858
host: hostPart,
59-
srcBaseDir: "~/cmux", // Default remote base directory (NOT including workspace name)
59+
srcBaseDir: "/home/cmux", // Default remote base directory (NOT including workspace name)
6060
};
6161
}
6262

tests/ipcMain/createWorkspace.test.ts

Lines changed: 34 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -590,45 +590,41 @@ exit 1
590590
);
591591

592592
test.concurrent(
593-
"handles tilde (~/) paths correctly (SSH only)",
593+
"rejects tilde paths in srcBaseDir (SSH only)",
594594
async () => {
595+
// Skip if SSH server not available
596+
if (!sshConfig) {
597+
console.log("Skipping SSH-specific test: SSH server not available");
598+
return;
599+
}
600+
595601
const env = await createTestEnvironment();
596602
const tempGitRepo = await createTempGitRepo();
597603

598604
try {
599-
const branchName = generateBranchName("tilde-test");
605+
const branchName = generateBranchName("tilde-rejection-test");
600606
const trunkBranch = await detectDefaultTrunkBranch(tempGitRepo);
601607

602-
// Use ~/workspace/... path instead of absolute path
608+
// Try to use tilde path - should be rejected
603609
const tildeRuntimeConfig: RuntimeConfig = {
604610
type: "ssh",
605611
host: `testuser@localhost`,
606612
srcBaseDir: `~/workspace`,
607-
identityFile: sshConfig!.privateKeyPath,
608-
port: sshConfig!.port,
613+
identityFile: sshConfig.privateKeyPath,
614+
port: sshConfig.port,
609615
};
610616

611-
const { result, cleanup } = await createWorkspaceWithCleanup(
612-
env,
617+
const result = await env.mockIpcRenderer.invoke(
618+
IPC_CHANNELS.WORKSPACE_CREATE,
613619
tempGitRepo,
614620
branchName,
615621
trunkBranch,
616622
tildeRuntimeConfig
617623
);
618624

619-
expect(result.success).toBe(true);
620-
if (!result.success) {
621-
throw new Error(`Failed to create workspace with tilde path: ${result.error}`);
622-
}
623-
624-
// Wait for init to complete
625-
await new Promise((resolve) => setTimeout(resolve, getInitWaitTime()));
626-
627-
// Verify workspace exists
628-
expect(result.metadata.id).toBeDefined();
629-
expect(result.metadata.namedWorkspacePath).toBeDefined();
630-
631-
await cleanup();
625+
// Should fail with error about tilde
626+
expect(result.success).toBe(false);
627+
expect(result.error).toMatch(/cannot start with tilde/i);
632628
} finally {
633629
await cleanupTestEnvironment(env);
634630
await cleanupTempGitRepo(tempGitRepo);
@@ -638,74 +634,41 @@ exit 1
638634
);
639635

640636
test.concurrent(
641-
"handles tilde paths with init hooks (SSH only)",
637+
"rejects bare tilde in srcBaseDir (SSH only)",
642638
async () => {
639+
// Skip if SSH server not available
640+
if (!sshConfig) {
641+
console.log("Skipping SSH-specific test: SSH server not available");
642+
return;
643+
}
644+
643645
const env = await createTestEnvironment();
644646
const tempGitRepo = await createTempGitRepo();
645647

646648
try {
647-
// Add init hook to repo
648-
await createInitHook(
649-
tempGitRepo,
650-
`#!/bin/bash
651-
echo "Init hook executed with tilde path"
652-
`
653-
);
654-
await commitChanges(tempGitRepo, "Add init hook for tilde test");
655-
656-
const branchName = generateBranchName("tilde-init-test");
649+
const branchName = generateBranchName("bare-tilde-rejection");
657650
const trunkBranch = await detectDefaultTrunkBranch(tempGitRepo);
658651

659-
// Use ~/workspace/... path instead of absolute path
652+
// Try to use bare tilde - should be rejected
660653
const tildeRuntimeConfig: RuntimeConfig = {
661654
type: "ssh",
662655
host: `testuser@localhost`,
663-
srcBaseDir: `~/workspace`,
664-
identityFile: sshConfig!.privateKeyPath,
665-
port: sshConfig!.port,
656+
srcBaseDir: `~`,
657+
identityFile: sshConfig.privateKeyPath,
658+
port: sshConfig.port,
666659
};
667660

668-
// Capture init events to verify hook output
669-
const initEvents = setupInitEventCapture(env);
670-
671-
const { result, cleanup } = await createWorkspaceWithCleanup(
672-
env,
661+
const result = await env.mockIpcRenderer.invoke(
662+
IPC_CHANNELS.WORKSPACE_CREATE,
673663
tempGitRepo,
674664
branchName,
675665
trunkBranch,
676666
tildeRuntimeConfig
677667
);
678668

679-
expect(result.success).toBe(true);
680-
if (!result.success) {
681-
throw new Error(
682-
`Failed to create workspace with tilde path + init hook: ${result.error}`
683-
);
684-
}
685-
686-
// Wait for init to complete (including hook)
687-
await new Promise((resolve) => setTimeout(resolve, getInitWaitTime()));
688-
689-
// Verify init hook was executed
690-
const outputEvents = filterEventsByType(initEvents, EVENT_TYPE_INIT_OUTPUT);
691-
const outputLines = outputEvents.map((e) => {
692-
const data = e.data as { line?: string };
693-
return data.line ?? "";
694-
});
695-
696-
// Debug: Print all output including errors
697-
console.log("=== TILDE INIT HOOK OUTPUT ===");
698-
outputEvents.forEach((e) => {
699-
const data = e.data as { line?: string; isError?: boolean };
700-
const prefix = data.isError ? "[ERROR]" : "[INFO] ";
701-
console.log(prefix + (data.line ?? ""));
702-
});
703-
console.log("=== END TILDE INIT HOOK OUTPUT ===");
704-
705-
expect(outputLines.some((line) => line.includes("Running init hook"))).toBe(true);
706-
expect(outputLines.some((line) => line.includes("Init hook executed"))).toBe(true);
707-
708-
await cleanup();
669+
// Should fail with error about tilde
670+
expect(result.success).toBe(false);
671+
expect(result.error).toMatch(/cannot start with tilde/i);
709672
} finally {
710673
await cleanupTestEnvironment(env);
711674
await cleanupTempGitRepo(tempGitRepo);
@@ -848,7 +811,7 @@ echo "Init hook executed with tilde path"
848811
const runtimeConfig: RuntimeConfig = {
849812
type: "ssh",
850813
host: "testuser@localhost",
851-
srcBaseDir: "~/workspace",
814+
srcBaseDir: sshConfig.workdir,
852815
identityFile: sshConfig.privateKeyPath,
853816
port: sshConfig.port,
854817
};

0 commit comments

Comments
 (0)