Skip to content

bug: create_workspace accepts nonexistent and non-directory local paths #4611

Description

@colonelpanic8

What's broken

The create_workspace MCP tool accepts isolation: "local" for paths that do not name a real directory. Both a nonexistent path and a regular file return isError: false, allocate project/workspace IDs, and describe the result as kind: "directory".

I expected local isolation to adopt an existing directory only. A missing path should return an actionable “no such directory” error, and a regular file should return “not a directory,” without persisting a successful-looking workspace. The current result defers the failure until an agent or another filesystem operation tries to use the invalid cwd.

I searched open and closed issues and PRs before filing, including create_workspace + directory/path, isolation: local, create workspace + nonexistent, local workspace + not a directory, workspace record + does not exist, and PR number 2884. No issue reported this behavior. #3756 concerns project leakage from disposable local-isolation automation and is not the same validation failure; the only exact match was the unlinked candidate-fix PR.

Steps to reproduce

Tested from a clean checkout of current getpaseo/paseo main:

commit 92504cd525c7e594539a369ed441eb836372dd2d
package version 0.8.0-beta.1
Node v24.19.0
npm 11.17.0

I used a focused failing-first E2E test. It starts a real in-process daemon on 127.0.0.1:0, connects through the public /mcp/agents Streamable HTTP endpoint, and calls the real tool twice:

const root = await mkdtemp(path.join(os.tmpdir(), "paseo-pr2884-main-"));
const paseoHome = path.join(root, "state");
const missingPath = path.join(root, "does-not-exist");
const filePath = path.join(root, "regular-file");
await writeFile(filePath, "not a directory\n");

const daemon = await createPaseoDaemon(
  {
    listen: "127.0.0.1:0",
    paseoHome,
    corsAllowedOrigins: [],
    hostnames: true,
    mcpEnabled: true,
    staticDir: path.join(root, "static"),
    mcpDebug: false,
    agentClients: createTestAgentClients(),
    agentStoragePath: path.join(paseoHome, "agents"),
  },
  pino({ level: "silent" }),
);
await daemon.start();

const target = daemon.getListenTarget();
const client = await experimental_createMCPClient({
  transport: new StreamableHTTPClientTransport(
    new URL(`http://127.0.0.1:${target.port}/mcp/agents`),
  ),
});

const missing = await client.callTool({
  name: "create_workspace",
  args: { isolation: "local", path: missingPath },
});
const regularFile = await client.callTool({
  name: "create_workspace",
  args: { isolation: "local", path: filePath },
});

expect({
  missingIsError: missing.isError ?? false,
  fileIsError: regularFile.isError ?? false,
}).toEqual({ missingIsError: true, fileIsError: true });

Command:

npx vitest run packages/server/src/server/agent/pr2884-repro.e2e.test.ts --bail=1

Exact tool results and assertion failure:

{
  "missingPathExists": false,
  "missingResult": {
    "content": [{
      "type": "text",
      "text": "{\n  \"workspaceId\": \"wks_77d339c6f39ea579\",\n  \"projectId\": \"prj_7c8977ac9614d73e\",\n  \"cwd\": \"/tmp/paseo-pr2884-main-nTF4dQ/does-not-exist\",\n  \"isolation\": \"local\",\n  \"kind\": \"directory\",\n  \"title\": null\n}"
    }],
    "isError": false,
    "structuredContent": {
      "workspaceId": "wks_77d339c6f39ea579",
      "projectId": "prj_7c8977ac9614d73e",
      "cwd": "/tmp/paseo-pr2884-main-nTF4dQ/does-not-exist",
      "isolation": "local",
      "kind": "directory",
      "title": null
    }
  },
  "filePathIsDirectory": false,
  "fileResult": {
    "content": [{
      "type": "text",
      "text": "{\n  \"workspaceId\": \"wks_bee93beff2a9ee21\",\n  \"projectId\": \"prj_b5f25eafbf40c815\",\n  \"cwd\": \"/tmp/paseo-pr2884-main-nTF4dQ/regular-file\",\n  \"isolation\": \"local\",\n  \"kind\": \"directory\",\n  \"title\": null\n}"
    }],
    "isError": false,
    "structuredContent": {
      "workspaceId": "wks_bee93beff2a9ee21",
      "projectId": "prj_b5f25eafbf40c815",
      "cwd": "/tmp/paseo-pr2884-main-nTF4dQ/regular-file",
      "isolation": "local",
      "kind": "directory",
      "title": null
    }
  }
}

AssertionError: expected { missingIsError: false, …(1) } to deeply equal { missingIsError: true, …(1) }

- Expected
+ Received

  {
-   "fileIsError": true,
-   "missingIsError": true,
+   "fileIsError": false,
+   "missingIsError": false,
  }

Test Files  1 failed (1)
Tests       1 failed (1)

The test finally block closed the MCP client, stopped the daemon, and recursively removed its unique temporary root. The temporary reproduction test file was then deleted. Port 6767 and ~/.paseo were not used.

Where did this happen

Daemon

Paseo version

Current main 92504cd525c7e594539a369ed441eb836372dd2d (package version 0.8.0-beta.1)

OS version

NixOS 26.11 (Zokor), Linux 7.2.3 x86_64

Agent provider

Not relevant

Provider configuration

Not relevant; the MCP calls use the daemon's offline fake agent clients but do not create an agent.

Logs

The exact MCP responses and failing assertion are included above. The daemon did not log an error because both calls were treated as successful.

Screenshots or video

Not applicable (MCP API behavior).

Candidate fix

#2884 is an existing candidate fix. It was not applied during this reproduction, and this report does not assume its diagnosis is correct until reviewed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions