Add server-side validation for working directory existence

- Validate cwd exists before creating agent in session.ts
- Show error message when directory doesn't exist
- Add client-side warning display in new.tsx
- Block form submission for non-existent directories
- Add daemon e2e test for non-existent cwd validation
- Add createAgentExpectFail helper in daemon-client.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-27 05:10:59 +00:00
co-authored by Claude Opus 4.5
parent 95c821a4c6
commit 1e9d8bf075
4 changed files with 102 additions and 1 deletions
+26
View File
@@ -244,6 +244,9 @@ export default function DraftAgentScreen() {
const isNonGitDirectory =
repoRequestStatus === "error" &&
/not in a git repository/i.test(repoRequestError?.message ?? "");
const isDirectoryNotExists =
repoRequestStatus === "error" &&
/does not exist|no such file or directory|ENOENT/i.test(repoRequestError?.message ?? "");
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
? "idle"
: repoAvailabilityError
@@ -490,6 +493,10 @@ export default function DraftAgentScreen() {
setErrorMessage("Working directory is required");
throw new Error("Working directory is required");
}
if (isDirectoryNotExists) {
setErrorMessage("Working directory does not exist on the selected host");
throw new Error("Working directory does not exist on the selected host");
}
if (!trimmedPrompt) {
setErrorMessage("Initial prompt is required");
throw new Error("Initial prompt is required");
@@ -562,6 +569,7 @@ export default function DraftAgentScreen() {
createNewBranch,
createWorktree,
gitBlockingError,
isDirectoryNotExists,
isLoading,
isNonGitDirectory,
modeOptions,
@@ -692,6 +700,13 @@ export default function DraftAgentScreen() {
wrapInContainer={false}
renderTrigger={renderDropdownTrigger}
/>
{isDirectoryNotExists && (
<View style={styles.warningContainer}>
<Text style={styles.warningText}>
Directory does not exist on the selected host
</Text>
</View>
)}
{renderConfigRow({
label: "Agent",
value: `${providerDefinitions.find((p) => p.id === selectedProvider)?.label ?? selectedProvider} · ${selectedModel || "auto"}`,
@@ -1037,6 +1052,17 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructiveForeground,
fontSize: theme.fontSize.sm,
},
warningContainer: {
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.md,
backgroundColor: theme.colors.palette.yellow[400],
},
warningText: {
color: "#000000",
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
hostBadge: {
flexDirection: "row",
alignItems: "center",
@@ -82,6 +82,19 @@ describe("daemon E2E", () => {
expect(hasAssistantMessage).toBe(true);
}, 180000); // 3 minute timeout for E2E test
test("fails to create agent with non-existent cwd", async () => {
const nonExistentCwd = "/this/path/does/not/exist/12345";
const result = await ctx.client.createAgentExpectFail({
provider: "codex",
cwd: nonExistentCwd,
title: "Should Fail Agent",
});
expect(result.error).toContain("Working directory does not exist");
expect(result.error).toContain(nonExistentCwd);
});
describe("permission flow: Codex", () => {
test(
"approves permission and executes command",
+19 -1
View File
@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from "uuid";
import { readFile, mkdir, writeFile } from "fs/promises";
import { readFile, mkdir, writeFile, stat } from "fs/promises";
import { exec } from "child_process";
import { promisify, inspect } from "util";
import { join } from "path";
@@ -1273,6 +1273,24 @@ export class Session {
);
try {
// Validate that the working directory exists
const resolvedCwd = expandTilde(config.cwd);
try {
const stats = await stat(resolvedCwd);
if (!stats.isDirectory()) {
throw new Error(
`Working directory is not a directory: ${config.cwd}`
);
}
} catch (statError: any) {
if (statError.code === "ENOENT") {
throw new Error(
`Working directory does not exist: ${config.cwd}`
);
}
throw statError;
}
const sessionConfig = await this.buildAgentSessionConfig(
config,
git,
@@ -197,6 +197,50 @@ export class DaemonClient {
); // 60 second timeout for initialization
}
/**
* Create an agent and expect it to fail.
* Returns the error message from the failure.
*/
async createAgentExpectFail(
options: CreateAgentOptions
): Promise<{ error: string }> {
const requestId = nanoid();
// Record the current queue position so we only check NEW messages
const startPosition = this.messageQueue.length;
this.send({
type: "create_agent_request",
requestId,
config: {
provider: options.provider,
cwd: options.cwd,
title: options.title,
model: options.model,
modeId: options.modeId,
mcpServers: options.mcpServers,
extra: options.extra,
},
initialPrompt: options.initialPrompt,
});
// Wait for either agent_create_failed status or timeout
return this.waitFor(
(msg) => {
if (
msg.type === "status" &&
msg.payload.status === "agent_create_failed" &&
msg.payload.requestId === requestId
) {
return { error: String(msg.payload.error ?? "Unknown error") };
}
return null;
},
10000,
{ skipQueueBefore: startPosition }
);
}
async deleteAgent(agentId: string): Promise<void> {
this.send({ type: "delete_agent_request", agentId });
await this.waitFor((msg) => {