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:
co-authored by
Claude Opus 4.5
parent
95c821a4c6
commit
1e9d8bf075
@@ -244,6 +244,9 @@ export default function DraftAgentScreen() {
|
|||||||
const isNonGitDirectory =
|
const isNonGitDirectory =
|
||||||
repoRequestStatus === "error" &&
|
repoRequestStatus === "error" &&
|
||||||
/not in a git repository/i.test(repoRequestError?.message ?? "");
|
/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
|
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
|
||||||
? "idle"
|
? "idle"
|
||||||
: repoAvailabilityError
|
: repoAvailabilityError
|
||||||
@@ -490,6 +493,10 @@ export default function DraftAgentScreen() {
|
|||||||
setErrorMessage("Working directory is required");
|
setErrorMessage("Working directory is required");
|
||||||
throw new Error("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) {
|
if (!trimmedPrompt) {
|
||||||
setErrorMessage("Initial prompt is required");
|
setErrorMessage("Initial prompt is required");
|
||||||
throw new Error("Initial prompt is required");
|
throw new Error("Initial prompt is required");
|
||||||
@@ -562,6 +569,7 @@ export default function DraftAgentScreen() {
|
|||||||
createNewBranch,
|
createNewBranch,
|
||||||
createWorktree,
|
createWorktree,
|
||||||
gitBlockingError,
|
gitBlockingError,
|
||||||
|
isDirectoryNotExists,
|
||||||
isLoading,
|
isLoading,
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
modeOptions,
|
modeOptions,
|
||||||
@@ -692,6 +700,13 @@ export default function DraftAgentScreen() {
|
|||||||
wrapInContainer={false}
|
wrapInContainer={false}
|
||||||
renderTrigger={renderDropdownTrigger}
|
renderTrigger={renderDropdownTrigger}
|
||||||
/>
|
/>
|
||||||
|
{isDirectoryNotExists && (
|
||||||
|
<View style={styles.warningContainer}>
|
||||||
|
<Text style={styles.warningText}>
|
||||||
|
Directory does not exist on the selected host
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{renderConfigRow({
|
{renderConfigRow({
|
||||||
label: "Agent",
|
label: "Agent",
|
||||||
value: `${providerDefinitions.find((p) => p.id === selectedProvider)?.label ?? selectedProvider} · ${selectedModel || "auto"}`,
|
value: `${providerDefinitions.find((p) => p.id === selectedProvider)?.label ?? selectedProvider} · ${selectedModel || "auto"}`,
|
||||||
@@ -1037,6 +1052,17 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.destructiveForeground,
|
color: theme.colors.destructiveForeground,
|
||||||
fontSize: theme.fontSize.sm,
|
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: {
|
hostBadge: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ describe("daemon E2E", () => {
|
|||||||
expect(hasAssistantMessage).toBe(true);
|
expect(hasAssistantMessage).toBe(true);
|
||||||
}, 180000); // 3 minute timeout for E2E test
|
}, 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", () => {
|
describe("permission flow: Codex", () => {
|
||||||
test(
|
test(
|
||||||
"approves permission and executes command",
|
"approves permission and executes command",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
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 { exec } from "child_process";
|
||||||
import { promisify, inspect } from "util";
|
import { promisify, inspect } from "util";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
@@ -1273,6 +1273,24 @@ export class Session {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
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(
|
const sessionConfig = await this.buildAgentSessionConfig(
|
||||||
config,
|
config,
|
||||||
git,
|
git,
|
||||||
|
|||||||
@@ -197,6 +197,50 @@ export class DaemonClient {
|
|||||||
); // 60 second timeout for initialization
|
); // 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> {
|
async deleteAgent(agentId: string): Promise<void> {
|
||||||
this.send({ type: "delete_agent_request", agentId });
|
this.send({ type: "delete_agent_request", agentId });
|
||||||
await this.waitFor((msg) => {
|
await this.waitFor((msg) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user