Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,37 +219,76 @@ function isNonConfigPath(pathValue: string): boolean {
return !isWithinPath(getOpenCodeConfigPrefix(), pathValue);
}

const SESSION_WORKSPACE_CACHE_LIMIT = 200;

function resolveWorkspaceDirectory(worktree: string | undefined, directory: string | undefined): string {
const envWorkspace = process.env.CURSOR_ACP_WORKSPACE?.trim();
if (envWorkspace) {
return resolve(envWorkspace);
// Filesystem roots are never a meaningful workspace: accepting "/" (or a bare
// Windows drive root like "C:\") makes every tool treat the whole machine as
// the project, which is both unsafe and a common symptom of a daemon that
// was launched without a real cwd (e.g. systemd unit without WorkingDirectory).
export function isRootPath(pathValue: string): boolean {
if (!pathValue) {
return false;
}
const resolved = resolve(pathValue);
if (resolved === "/") {
return true;
}
return /^[A-Za-z]:[\\/]?$/.test(resolved);
}

const envProjectDir = process.env.OPENCODE_CURSOR_PROJECT_DIR?.trim();
if (envProjectDir) {
return resolve(envProjectDir);
function isAcceptableWorkspace(pathValue: string, configPrefix: string): boolean {
if (!pathValue) {
return false;
}
if (isRootPath(pathValue)) {
return false;
}
if (isWithinPath(configPrefix, pathValue)) {
return false;
}
return true;
}

const SESSION_WORKSPACE_CACHE_LIMIT = 200;

export function resolveWorkspaceDirectory(
worktree: string | undefined,
directory: string | undefined,
): string {
const configPrefix = getOpenCodeConfigPrefix();

const envWorkspace = resolveCandidate(process.env.CURSOR_ACP_WORKSPACE);
if (envWorkspace && !isRootPath(envWorkspace)) {
return envWorkspace;
}

const envProjectDir = resolveCandidate(process.env.OPENCODE_CURSOR_PROJECT_DIR);
if (envProjectDir && !isRootPath(envProjectDir)) {
return envProjectDir;
}

const worktreeCandidate = resolveCandidate(worktree);
if (worktreeCandidate && !isWithinPath(configPrefix, worktreeCandidate)) {
if (isAcceptableWorkspace(worktreeCandidate, configPrefix)) {
return worktreeCandidate;
}

const dirCandidate = resolveCandidate(directory);
if (dirCandidate && !isWithinPath(configPrefix, dirCandidate)) {
if (isAcceptableWorkspace(dirCandidate, configPrefix)) {
return dirCandidate;
}

const cwd = resolve(process.cwd());
if (cwd && !isWithinPath(configPrefix, cwd)) {
if (isAcceptableWorkspace(cwd, configPrefix)) {
return cwd;
}

return dirCandidate || cwd || configPrefix;
// Fall back to the user's home directory rather than "/" when every other
// signal is unusable. $HOME is always writable for the current user and
// keeps tool scopes sane even when the daemon was spawned from root.
const home = resolveCandidate(homedir());
if (home && !isRootPath(home)) {
return home;
}

return configPrefix;
}

type ProxyRuntimeState = {
Expand Down
122 changes: 122 additions & 0 deletions tests/unit/plugin-workspace-resolution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, rmSync } from "fs";
import { homedir, tmpdir } from "os";
import { join, resolve } from "path";

import { resolveWorkspaceDirectory, isRootPath } from "../../src/plugin";

describe("isRootPath", () => {
it("recognises the POSIX root", () => {
expect(isRootPath("/")).toBe(true);
});

it.if(process.platform === "win32")("recognises Windows drive roots", () => {
expect(isRootPath("C:\\")).toBe(true);
expect(isRootPath("D:/")).toBe(true);
expect(isRootPath("C:")).toBe(true);
});

it("rejects ordinary paths", () => {
expect(isRootPath("/home/user")).toBe(false);
expect(isRootPath("/tmp")).toBe(false);
expect(isRootPath("")).toBe(false);
});
});

describe("resolveWorkspaceDirectory", () => {
let previousXdgConfigHome: string | undefined;
let previousWorkspaceEnv: string | undefined;
let previousProjectDirEnv: string | undefined;
let previousCwd: string;
let tempConfigHome: string;
let tempWorkspace: string;

beforeEach(() => {
previousXdgConfigHome = process.env.XDG_CONFIG_HOME;
previousWorkspaceEnv = process.env.CURSOR_ACP_WORKSPACE;
previousProjectDirEnv = process.env.OPENCODE_CURSOR_PROJECT_DIR;
previousCwd = process.cwd();

tempConfigHome = mkdtempSync(join(tmpdir(), "opencode-cursor-cfg-"));
tempWorkspace = mkdtempSync(join(tmpdir(), "opencode-cursor-ws-"));
process.env.XDG_CONFIG_HOME = tempConfigHome;
delete process.env.CURSOR_ACP_WORKSPACE;
delete process.env.OPENCODE_CURSOR_PROJECT_DIR;
});

afterEach(() => {
try {
process.chdir(previousCwd);
} catch {
// best-effort restore
}

if (previousXdgConfigHome === undefined) {
delete process.env.XDG_CONFIG_HOME;
} else {
process.env.XDG_CONFIG_HOME = previousXdgConfigHome;
}
if (previousWorkspaceEnv === undefined) {
delete process.env.CURSOR_ACP_WORKSPACE;
} else {
process.env.CURSOR_ACP_WORKSPACE = previousWorkspaceEnv;
}
if (previousProjectDirEnv === undefined) {
delete process.env.OPENCODE_CURSOR_PROJECT_DIR;
} else {
process.env.OPENCODE_CURSOR_PROJECT_DIR = previousProjectDirEnv;
}

rmSync(tempConfigHome, { recursive: true, force: true });
rmSync(tempWorkspace, { recursive: true, force: true });
});

it("prefers a real worktree over cwd", () => {
const result = resolveWorkspaceDirectory(tempWorkspace, undefined);
expect(result).toBe(resolve(tempWorkspace));
});

it("prefers directory when worktree is missing", () => {
const result = resolveWorkspaceDirectory(undefined, tempWorkspace);
expect(result).toBe(resolve(tempWorkspace));
});

it("rejects '/' from worktree and directory and falls back to cwd", () => {
process.chdir(tempWorkspace);
const result = resolveWorkspaceDirectory("/", "/");
expect(result).toBe(resolve(tempWorkspace));
});

it("falls back to $HOME when worktree, directory, and cwd are all '/'", () => {
process.chdir("/");
const result = resolveWorkspaceDirectory("/", "/");
expect(result).toBe(resolve(homedir()));
expect(result).not.toBe("/");
});

it("rejects '/' provided via CURSOR_ACP_WORKSPACE", () => {
process.env.CURSOR_ACP_WORKSPACE = "/";
process.chdir(tempWorkspace);
const result = resolveWorkspaceDirectory(undefined, undefined);
expect(result).toBe(resolve(tempWorkspace));
});

it("rejects '/' provided via OPENCODE_CURSOR_PROJECT_DIR", () => {
process.env.OPENCODE_CURSOR_PROJECT_DIR = "/";
process.chdir(tempWorkspace);
const result = resolveWorkspaceDirectory(undefined, undefined);
expect(result).toBe(resolve(tempWorkspace));
});

it("respects CURSOR_ACP_WORKSPACE when it is a real directory", () => {
process.env.CURSOR_ACP_WORKSPACE = tempWorkspace;
const result = resolveWorkspaceDirectory(undefined, undefined);
expect(result).toBe(resolve(tempWorkspace));
});

it("skips paths inside the opencode config prefix", () => {
const insideConfig = join(tempConfigHome, "opencode", "plugin");
const result = resolveWorkspaceDirectory(insideConfig, insideConfig);
expect(result).not.toBe(insideConfig);
});
});