133 lines
4.1 KiB
Go
133 lines
4.1 KiB
Go
// Package workspace sets up an isolated temp directory for each Cursor CLI /
|
|
// ACP child. It pre-populates a minimal .cursor config so the agent does not
|
|
// load the real user's global rules from ~/.cursor, and returns a set of
|
|
// environment overrides (HOME, CURSOR_CONFIG_DIR, XDG_CONFIG_HOME, APPDATA…)
|
|
// so the child cannot escape back to the real profile.
|
|
//
|
|
// Ported from cursor-api-proxy/src/lib/workspace.ts.
|
|
package workspace
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
// ChatOnly prepares an isolated temp workspace and returns:
|
|
// - dir: the absolute path of the new temp directory (caller is responsible
|
|
// for removing it when the child process exits).
|
|
// - env: a map of environment variables to override on the child.
|
|
//
|
|
// Auth is the tricky part. The Cursor CLI resolves login tokens from the
|
|
// real user profile (macOS keychain on darwin, ~/.cursor/agent-cli-state.json
|
|
// elsewhere); if we override HOME to the temp dir, `agent --print` dies with
|
|
// "Authentication required. Please run 'agent login' first…". So we keep
|
|
// HOME untouched unless either:
|
|
// - CURSOR_API_KEY is set (the CLI uses the env var and doesn't need HOME), or
|
|
// - authConfigDir is non-empty (account-pool mode, not used here yet).
|
|
//
|
|
// We *do* always override CURSOR_CONFIG_DIR → tempDir/.cursor. That's the
|
|
// setting Cursor uses to locate rules/, cli-config.json, and per-project
|
|
// state, so this single override is enough to stop the agent from loading
|
|
// the user's real ~/.cursor/rules/* into the prompt.
|
|
func ChatOnly(authConfigDir string) (dir string, env map[string]string, err error) {
|
|
tempDir, err := os.MkdirTemp("", "cursor-adapter-ws-*")
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("mkdtemp: %w", err)
|
|
}
|
|
|
|
cursorDir := filepath.Join(tempDir, ".cursor")
|
|
if err := os.MkdirAll(filepath.Join(cursorDir, "rules"), 0o755); err != nil {
|
|
_ = os.RemoveAll(tempDir)
|
|
return "", nil, fmt.Errorf("mkdir .cursor/rules: %w", err)
|
|
}
|
|
|
|
minimalConfig := map[string]any{
|
|
"version": 1,
|
|
"editor": map[string]any{"vimMode": false},
|
|
"permissions": map[string]any{
|
|
"allow": []any{},
|
|
"deny": []any{},
|
|
},
|
|
}
|
|
cfgBytes, _ := json.Marshal(minimalConfig)
|
|
if err := os.WriteFile(filepath.Join(cursorDir, "cli-config.json"), cfgBytes, 0o644); err != nil {
|
|
_ = os.RemoveAll(tempDir)
|
|
return "", nil, fmt.Errorf("write cli-config.json: %w", err)
|
|
}
|
|
|
|
env = map[string]string{}
|
|
if authConfigDir != "" {
|
|
env["CURSOR_CONFIG_DIR"] = authConfigDir
|
|
return tempDir, env, nil
|
|
}
|
|
|
|
env["CURSOR_CONFIG_DIR"] = cursorDir
|
|
|
|
// Only fully isolate HOME if the child will auth via CURSOR_API_KEY.
|
|
// With keychain/home-based auth, replacing HOME makes agent exit 1.
|
|
if os.Getenv("CURSOR_API_KEY") != "" {
|
|
env["HOME"] = tempDir
|
|
env["USERPROFILE"] = tempDir
|
|
|
|
if runtime.GOOS == "windows" {
|
|
appDataRoaming := filepath.Join(tempDir, "AppData", "Roaming")
|
|
appDataLocal := filepath.Join(tempDir, "AppData", "Local")
|
|
_ = os.MkdirAll(appDataRoaming, 0o755)
|
|
_ = os.MkdirAll(appDataLocal, 0o755)
|
|
env["APPDATA"] = appDataRoaming
|
|
env["LOCALAPPDATA"] = appDataLocal
|
|
} else {
|
|
xdg := filepath.Join(tempDir, ".config")
|
|
_ = os.MkdirAll(xdg, 0o755)
|
|
env["XDG_CONFIG_HOME"] = xdg
|
|
}
|
|
}
|
|
|
|
return tempDir, env, nil
|
|
}
|
|
|
|
// MergeEnv takes the current process env (as "KEY=VALUE" strings) and
|
|
// overlays overrides on top, returning a new slice suitable for exec.Cmd.Env.
|
|
// Keys from overrides replace any existing entries with the same key.
|
|
func MergeEnv(base []string, overrides map[string]string) []string {
|
|
if len(overrides) == 0 {
|
|
return base
|
|
}
|
|
|
|
out := make([]string, 0, len(base)+len(overrides))
|
|
seen := make(map[string]bool, len(overrides))
|
|
|
|
for _, kv := range base {
|
|
eq := indexOf(kv, '=')
|
|
if eq < 0 {
|
|
out = append(out, kv)
|
|
continue
|
|
}
|
|
key := kv[:eq]
|
|
if v, ok := overrides[key]; ok {
|
|
out = append(out, key+"="+v)
|
|
seen[key] = true
|
|
} else {
|
|
out = append(out, kv)
|
|
}
|
|
}
|
|
for k, v := range overrides {
|
|
if !seen[k] {
|
|
out = append(out, k+"="+v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func indexOf(s string, c byte) int {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == c {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|