Fix socket connection issues#21
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses SSH agent socket mount breakages in managed devcontainers when the host SSH_AUTH_SOCK path changes between sessions by introducing host-side compatibility symlink management and wiring it into the up flow.
Changes:
- Added
ensureManagedContainerSshMountCompatibilityto detect stale/missing bind-mount sources and create/update a symlink to the currentSSH_AUTH_SOCK. - Invoked the compatibility check in the CLI
upflow and logged when a symlink is created/updated. - Added tests covering key outcomes (
created-symlink,updated-symlink,unchanged,not-applicable).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/runtime.ts |
Implements SSH mount compatibility logic (bind-mount source detection + symlink creation/update) and introduces outcome type. |
src/cli.ts |
Calls the compatibility function before devcontainer up and prints user-facing messages for symlink actions. |
tests/runtime.test.ts |
Adds a test suite validating the new compatibility behavior across multiple scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const resolvedMountedSource = path.resolve(mountedSource); | ||
| const resolvedCurrentSshAuthSock = path.resolve(trimmedSshAuthSockSource); | ||
| if (resolvedMountedSource === resolvedCurrentSshAuthSock) { | ||
| return "unchanged"; | ||
| } | ||
|
|
||
| let mountedSourceStat: Awaited<ReturnType<typeof lstat>> | null = null; | ||
| try { | ||
| mountedSourceStat = await lstat(resolvedMountedSource); | ||
| } catch (error) { | ||
| if (!isMissingPathError(error)) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| if (!mountedSourceStat) { | ||
| await mkdir(path.dirname(resolvedMountedSource), { recursive: true }); | ||
| await symlink(resolvedCurrentSshAuthSock, resolvedMountedSource); | ||
| return "created-symlink"; | ||
| } | ||
|
|
||
| if (!mountedSourceStat.isSymbolicLink()) { | ||
| return "unchanged"; | ||
| } | ||
|
|
||
| const existingTarget = await readlink(resolvedMountedSource); | ||
| const resolvedExistingTarget = path.resolve(path.dirname(resolvedMountedSource), existingTarget); | ||
| if (resolvedExistingTarget === resolvedCurrentSshAuthSock) { | ||
| return "unchanged"; |
There was a problem hiding this comment.
Path comparisons here use path.resolve, which does not canonicalize symlinks. On macOS (e.g. /tmp vs /private/tmp) or when SSH_AUTH_SOCK itself is a symlink, this can cause an unnecessary updated-symlink outcome even though both paths refer to the same underlying socket. Consider comparing canonicalized paths (e.g. via the existing resolveComparablePath helper or realpath where available) before deciding to update the symlink.
| return "unchanged"; | ||
| } | ||
|
|
||
| await rm(resolvedMountedSource); |
There was a problem hiding this comment.
rm(resolvedMountedSource) can throw ENOENT if the symlink is removed between the earlier lstat/readlink and this call (or if the path is concurrently modified). Using rm(..., { force: true }) (consistent with other rm usages in the repo) or unlink with a missing-path guard would make this update step more robust.
| await rm(resolvedMountedSource); | |
| await rm(resolvedMountedSource, { force: true }); |
| ), | ||
| ).resolves.toBe("unchanged"); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The new suite doesn’t currently cover the branch where the existing mount source is a symlink that already points at the current SSH_AUTH_SOCK (the isSymbolicLink() + resolvedExistingTarget === resolvedCurrentSshAuthSock path). Adding a test for that scenario would help ensure the function doesn’t rewrite a correct symlink and keeps the outcome as unchanged.
| test("does nothing when the existing mount source is a symlink that already resolves to the current host ssh agent socket", async () => { | |
| const tempDir = await mkdtemp(path.join(os.tmpdir(), "devbox-test-")); | |
| tempPaths.push(tempDir); | |
| const currentSocketPath = path.join(tempDir, "current", "agent.sock"); | |
| const compatibilitySocketPath = path.join(tempDir, "compat", "agent.sock"); | |
| await mkdir(path.dirname(currentSocketPath), { recursive: true }); | |
| await mkdir(path.dirname(compatibilitySocketPath), { recursive: true }); | |
| await writeFile(currentSocketPath, "current"); | |
| await symlink(currentSocketPath, compatibilitySocketPath); | |
| await expect( | |
| ensureManagedContainerSshMountCompatibility( | |
| { | |
| Id: "container-1", | |
| Mounts: [ | |
| { | |
| Type: "bind", | |
| Source: compatibilitySocketPath, | |
| Destination: "/run/devbox-ssh-auth.sock", | |
| }, | |
| ], | |
| }, | |
| currentSocketPath, | |
| ), | |
| ).resolves.toBe("unchanged"); | |
| expect(await readlink(compatibilitySocketPath)).toBe(currentSocketPath); | |
| }); |
This pull request introduces logic to ensure compatibility for SSH agent socket mounts in managed containers, particularly when the host's SSH agent socket path changes. The main addition is a utility that checks and, if necessary, creates or updates a symlink so the container's SSH mount always points to the current SSH agent socket. This helps prevent issues when the SSH agent socket moves between devcontainer sessions. Comprehensive tests have also been added to verify this behavior.
SSH Agent Socket Mount Compatibility:
ensureManagedContainerSshMountCompatibilityinsrc/runtime.tsto check if the SSH agent socket mount in a managed container is up-to-date, and to create or update a symlink if needed. This function handles cases where the mount source is missing, stale, or already correct, and returns a status string describing the action taken.src/cli.tsto call this new function before starting the devcontainer, and to log messages when a symlink is created or updated. [1] [2]Supporting Utilities:
src/runtime.tsfor resolving bind mount sources and handling missing paths, used by the compatibility logic.src/runtime.tsandtests/runtime.test.tsto include necessary filesystem methods for symlink management and inspection. [1] [2]Testing:
tests/runtime.test.tsto verify all code paths ofensureManagedContainerSshMountCompatibility, including creation, update, no-op, and not-applicable scenarios. [1] [2]Types:
ManagedContainerSshMountCompatibilitytype to clearly describe the possible outcomes of the compatibility check.