mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 10:13:11 +02:00
> _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>
133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { runChildProcess } from "@paperclipai/adapter-utils/server-utils";
|
|
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
|
|
import { testEnvironment } from "./test.js";
|
|
|
|
function buildFakeAgentScript(): string {
|
|
return `#!/bin/sh
|
|
if [ "$1" = "--version" ]; then
|
|
printf '%s\\n' 'Cursor Agent 1.2.3'
|
|
exit 0
|
|
fi
|
|
printf '%s\\n' '{"type":"system","subtype":"init","session_id":"cursor-session-envtest-1","model":"auto"}'
|
|
printf '%s\\n' '{"type":"assistant","message":{"content":[{"type":"output_text","text":"hello"}]}}'
|
|
printf '%s\\n' '{"type":"result","subtype":"success","session_id":"cursor-session-envtest-1","result":"ok"}'
|
|
`;
|
|
}
|
|
|
|
function buildInstallSimulationCommand(commandPath: string): string {
|
|
return [
|
|
`mkdir -p ${JSON.stringify(path.dirname(commandPath))}`,
|
|
`cat > ${JSON.stringify(commandPath)} <<'EOF'`,
|
|
buildFakeAgentScript(),
|
|
"EOF",
|
|
`chmod +x ${JSON.stringify(commandPath)}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function createSandboxRunner(options: { homeDir: string; installCommandPath: string }) {
|
|
let counter = 0;
|
|
const installCommands: string[] = [];
|
|
const systemPath = "/usr/bin:/bin";
|
|
return {
|
|
installCommands,
|
|
execute: async (input: {
|
|
command: string;
|
|
args?: string[];
|
|
cwd?: string;
|
|
env?: Record<string, string>;
|
|
stdin?: string;
|
|
timeoutMs?: number;
|
|
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
|
onSpawn?: (meta: { pid: number; startedAt: string }) => Promise<void>;
|
|
}) => {
|
|
counter += 1;
|
|
const args = [...(input.args ?? [])];
|
|
if (args[1] === SANDBOX_INSTALL_COMMAND) {
|
|
installCommands.push(args[1]);
|
|
args[1] = buildInstallSimulationCommand(options.installCommandPath);
|
|
}
|
|
return await runChildProcess(`cursor-envtest-runner-${counter}`, input.command, args, {
|
|
cwd: input.cwd ?? process.cwd(),
|
|
env: {
|
|
...(input.env ?? {}),
|
|
HOME: input.env?.HOME ?? options.homeDir,
|
|
PATH: input.env?.PATH ?? systemPath,
|
|
},
|
|
stdin: input.stdin,
|
|
timeoutSec: Math.max(1, Math.ceil((input.timeoutMs ?? 30_000) / 1000)),
|
|
graceSec: 5,
|
|
onLog: input.onLog ?? (async () => {}),
|
|
onSpawn: input.onSpawn
|
|
? async (meta) => input.onSpawn?.({ pid: meta.pid, startedAt: meta.startedAt })
|
|
: undefined,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("cursor testEnvironment", () => {
|
|
it("re-resolves the installed agent under ~/.cursor/bin and verifies --version before the hello probe", async () => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-envtest-"));
|
|
const homeDir = path.join(root, "home");
|
|
const workspace = path.join(root, "workspace");
|
|
const remoteWorkspace = path.join(root, "remote-workspace");
|
|
const agentPath = path.join(homeDir, ".cursor", "bin", "agent");
|
|
await fs.mkdir(workspace, { recursive: true });
|
|
await fs.mkdir(remoteWorkspace, { recursive: true });
|
|
|
|
const runner = createSandboxRunner({
|
|
homeDir,
|
|
installCommandPath: agentPath,
|
|
});
|
|
|
|
try {
|
|
const result = await testEnvironment({
|
|
companyId: "company-1",
|
|
adapterType: "cursor",
|
|
config: {
|
|
command: "agent",
|
|
cwd: workspace,
|
|
env: {
|
|
PATH: "/usr/bin:/bin",
|
|
},
|
|
},
|
|
executionTarget: {
|
|
kind: "remote",
|
|
transport: "sandbox",
|
|
shellCommand: "bash",
|
|
remoteCwd: remoteWorkspace,
|
|
runner,
|
|
timeoutMs: 30_000,
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe("pass");
|
|
expect(runner.installCommands).toEqual([SANDBOX_INSTALL_COMMAND]);
|
|
expect(result.checks).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
code: "cursor_command_resolvable",
|
|
level: "info",
|
|
message: `Command is executable: ${agentPath}`,
|
|
}),
|
|
expect.objectContaining({
|
|
code: "cursor_version_probe_passed",
|
|
level: "info",
|
|
detail: "Cursor Agent 1.2.3",
|
|
}),
|
|
expect.objectContaining({
|
|
code: "cursor_hello_probe_passed",
|
|
level: "info",
|
|
}),
|
|
]),
|
|
);
|
|
} finally {
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|