fix(cursor-local): resolve sandbox agent installs from cursor bin (#5686)

> _Stacked on top of #5685 (Harden remote sandbox runtime). Diff against
master includes commits from earlier PRs in the stack — review focuses
on the new commit only._

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The cursor-local adapter wraps the Cursor Agent CLI so a Paperclip
workflow can drive it inside a sandbox
> - When the adapter runs in a remote sandbox, the Cursor Agent CLI
installs under `$HOME/.local/bin/cursor-agent` (or wherever
`$XDG_BIN_HOME` points), not on the global PATH
> - The existing post-install resolution assumed `cursor-agent` would
resolve via the sandbox's login shell PATH after `npm install -g`, which
fails on sandboxes where the install lands in a user-prefixed directory
that isn't on PATH at probe time
> - This pull request resolves the agent CLI from the cursor binary's
own directory (`dirname "$(command -v cursor)"`) so the install probe
and execute path agree on a real binary location
> - The benefit is that cursor-local works correctly on any sandbox
provider where `npm install` lands in a user-prefixed directory

## What Changed

- `packages/adapters/cursor-local/src/server/remote-command.ts`: resolve
the cursor-agent binary from the cursor bin directory after install,
instead of relying on PATH.
- `packages/adapters/cursor-local/src/server/test.ts`: corresponding
probe tweak.
- `packages/adapters/cursor-local/src/server/test.test.ts` (new) +
`remote-command.test.ts`: focused coverage that exercises the install +
resolve path against a sandbox runner that places the binary in a
user-prefixed directory.

## Verification

- `pnpm exec vitest run --no-coverage
packages/adapters/cursor-local/src/server/test.test.ts
packages/adapters/cursor-local/src/server/remote-command.test.ts
packages/adapters/cursor-local/src/server/execute.test.ts`

All passing locally.

## Risks

- Local cursor-local runs are unaffected — the resolution change only
kicks in for the sandbox install path.
- Low risk; isolated to one adapter.

## Model Used

- Provider: Anthropic
- Model: Claude Opus 4.7 (1M context)
- Capabilities used: tool use (Read/Edit/Bash), no code execution beyond
local repo commands

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots — N/A, no UI change
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley
2026-05-11 00:41:20 -07:00
committed by GitHub
parent b24c6909e8
commit 0fe39a2d5c
6 changed files with 315 additions and 23 deletions
@@ -148,7 +148,6 @@ export async function testEnvironment(
});
command = sandboxCommand.command;
env = sandboxCommand.env;
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
const installCheck = await maybeRunSandboxInstallCommand({
runId,
target,
@@ -158,6 +157,19 @@ export async function testEnvironment(
env,
});
if (installCheck) checks.push(installCheck);
const finalSandboxCommand = await prepareCursorSandboxCommand({
runId,
target,
command,
cwd,
env,
remoteSystemHomeDirHint: sandboxCommand.remoteSystemHomeDir,
timeoutSec: 45,
graceSec: 5,
});
command = finalSandboxCommand.command;
env = finalSandboxCommand.env;
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
try {
await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv);
checks.push({
@@ -218,6 +230,58 @@ export async function testEnvironment(
hint: "Use `agent` or `cursor-agent` to run the automatic installation and auth probe.",
});
} else {
const versionProbe = await runAdapterExecutionTargetProcess(
runId,
target,
command,
["--version"],
{
cwd,
env,
timeoutSec: 45,
graceSec: 5,
onLog: async () => {},
},
);
const versionDetail = summarizeProbeDetail(versionProbe.stdout, versionProbe.stderr, null);
if (versionProbe.timedOut) {
checks.push({
code: "cursor_version_probe_timed_out",
level: "error",
message: "Cursor version probe timed out.",
hint: "Run `agent --version` manually in this working directory to confirm the installed CLI is reachable non-interactively.",
});
} else if ((versionProbe.exitCode ?? 1) === 0) {
checks.push({
code: "cursor_version_probe_passed",
level: "info",
message: "Cursor version probe succeeded.",
...(versionDetail ? { detail: versionDetail } : {}),
});
} else {
checks.push({
code: "cursor_version_probe_failed",
level: "error",
message: "Cursor version probe failed.",
...(versionDetail ? { detail: versionDetail } : {}),
hint: "Run `agent --version` manually in this working directory to confirm the installed CLI is reachable non-interactively.",
});
}
const canRunHelloProbe = checks.every(
(check) =>
check.code !== "cursor_version_probe_failed" &&
check.code !== "cursor_version_probe_timed_out",
);
if (!canRunHelloProbe) {
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}
const model = asString(config.model, DEFAULT_CURSOR_LOCAL_MODEL).trim();
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);