mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
fix(runner): surface dangling-symlink errors instead of misleading EEXIST (#892)
* fix(runner): surface dangling-symlink errors instead of misleading EEXIST
When a session's workspace path is a symbolic link to a directory that no
longer exists, the runner previously failed with a kernel-level error:
Unable to create directory at '/path'. System error: EEXIST: file
already exists, mkdir '/path'.
The cause: `fs.access` follows symlinks and throws ENOENT when the target
is missing, then the fallback `fs.mkdir(dir, { recursive: true })` cannot
tolerate the existing non-directory entry (the dangling symlink itself)
and surfaces EEXIST verbatim. Users had no way to tell that the symlink
target was the actual problem.
Replace the inline `fs.access` + `fs.mkdir` pair with a small
`validateWorkspaceDirectory` helper that uses `fs.lstat` so symlinks are
inspected without being followed, then explicitly handles:
- missing path -> approval flow / mkdir as before
- existing directory -> ok
- regular file at the workspace path -> "non-directory file" error
- symlink to existing directory -> ok
- symlink to a non-directory -> "not a directory" error
- dangling symlink -> diagnostic naming both the symlink path and the
missing target, with recovery options (recreate the target, remove
the symlink, archive the session)
The mkdir error switch is preserved (EACCES, ENOTDIR, ENOSPC, EROFS) and
extended with an EEXIST race-recheck that lstat's the path again so the
kernel error code never leaks to the user.
Adds focused unit tests for both the fs-touching paths (real tmpdir +
symlinks) and the pure errno-to-message mapper.
Closes #890
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runner): drop copy-pasteable rm command from dangling-symlink hint
The dangling-symlink recovery message embedded the user-controlled
workspace path inside a literal `rm '...'` shell command shape. A path
containing a single quote would break the quoting and turn the
diagnostic into a shell-injection / accidental-delete vector when the
user copy-pasted the suggested command.
Describe the recovery action in prose instead ("remove the dangling
symlink at '<path>'") so the path is no longer presented as a
copy-pasteable command. Adds a regression test that exercises a path
containing a single quote and asserts the message never contains the
literal `rm ...` shape.
Codex review on PR #892.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runner): preserve ENOTDIR diagnostic on lstat parent-path failure
When the workspace path sits under a regular-file parent, fs.lstat throws
ENOTDIR before mkdir runs. Route that through describeMkdirError so the
user still sees the historic "file already exists at this path" message
instead of the generic inspect-workspace-path text.
Codex follow-up review on PR #892 (post-rebase).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+22
-40
@@ -21,6 +21,7 @@ import { isRetryableConnectionError } from '@/utils/errorUtils';
|
||||
import { cleanupRunnerState, getInstalledCliMtimeMs, isRunnerRunningCurrentlyInstalledHappyVersion, stopRunner, waitForRunnerHandoff } from './controlClient';
|
||||
import { startRunnerControlServer } from './controlServer';
|
||||
import { createWorktree, removeWorktree, type WorktreeInfo } from './worktree';
|
||||
import { validateWorkspaceDirectory } from './validateWorkspaceDirectory';
|
||||
import { join } from 'path';
|
||||
import { buildMachineMetadata } from '@/agent/sessionFactory';
|
||||
import { resolveWorkspaceRoots } from '@/utils/workspaceRoot';
|
||||
@@ -295,47 +296,28 @@ export async function startRunner(options: { workspaceRoots?: string[] } = {}):
|
||||
let happyProcess: ReturnType<typeof spawnHappyCLI> | null = null;
|
||||
|
||||
if (sessionType === 'simple') {
|
||||
try {
|
||||
await fs.access(directory);
|
||||
const validation = await validateWorkspaceDirectory(directory, {
|
||||
approvedNewDirectoryCreation
|
||||
});
|
||||
if (validation.type === 'requestApproval') {
|
||||
logger.debug(`[RUNNER RUN] Directory creation not approved for: ${directory}`);
|
||||
return {
|
||||
type: 'requestToApproveDirectoryCreation',
|
||||
directory
|
||||
};
|
||||
}
|
||||
if (validation.type === 'error') {
|
||||
logger.debug(`[RUNNER RUN] Workspace directory validation failed: ${validation.errorMessage}`);
|
||||
return {
|
||||
type: 'error',
|
||||
errorMessage: validation.errorMessage
|
||||
};
|
||||
}
|
||||
directoryCreated = validation.created;
|
||||
if (validation.created) {
|
||||
logger.debug(`[RUNNER RUN] Successfully created directory: ${directory}`);
|
||||
} else {
|
||||
logger.debug(`[RUNNER RUN] Directory exists: ${directory}`);
|
||||
} catch (error) {
|
||||
logger.debug(`[RUNNER RUN] Directory doesn't exist, creating: ${directory}`);
|
||||
|
||||
// Check if directory creation is approved
|
||||
if (!approvedNewDirectoryCreation) {
|
||||
logger.debug(`[RUNNER RUN] Directory creation not approved for: ${directory}`);
|
||||
return {
|
||||
type: 'requestToApproveDirectoryCreation',
|
||||
directory
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(directory, { recursive: true });
|
||||
logger.debug(`[RUNNER RUN] Successfully created directory: ${directory}`);
|
||||
directoryCreated = true;
|
||||
} catch (mkdirError: any) {
|
||||
let errorMessage = `Unable to create directory at '${directory}'. `;
|
||||
|
||||
// Provide more helpful error messages based on the error code
|
||||
if (mkdirError.code === 'EACCES') {
|
||||
errorMessage += `Permission denied. You don't have write access to create a folder at this location. Try using a different path or check your permissions.`;
|
||||
} else if (mkdirError.code === 'ENOTDIR') {
|
||||
errorMessage += `A file already exists at this path or in the parent path. Cannot create a directory here. Please choose a different location.`;
|
||||
} else if (mkdirError.code === 'ENOSPC') {
|
||||
errorMessage += `No space left on device. Your disk is full. Please free up some space and try again.`;
|
||||
} else if (mkdirError.code === 'EROFS') {
|
||||
errorMessage += `The file system is read-only. Cannot create directories here. Please choose a writable location.`;
|
||||
} else {
|
||||
errorMessage += `System error: ${mkdirError.message || mkdirError}. Please verify the path is valid and you have the necessary permissions.`;
|
||||
}
|
||||
|
||||
logger.debug(`[RUNNER RUN] Directory creation failed: ${errorMessage}`);
|
||||
return {
|
||||
type: 'error',
|
||||
errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user