Skip to content

Commit 7f04434

Browse files
committed
fix(runtime): treat successful cursor CLI installs as success
• Re-resolve agent under HOME and versioned install paths after curl|bash • Stop failing on brittle /root path checks and curl progress stderr • Symlink the found binary into /usr/local/bin for the runtime supervisor
1 parent 2245441 commit 7f04434

2 files changed

Lines changed: 156 additions & 82 deletions

File tree

apps/runtime/internal/agent/cursor.go

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,29 @@ func ensureCursorBin(
203203
"bin": bin,
204204
})
205205

206-
install := `set -e
207-
export PATH="/usr/local/bin:/root/.local/bin:$PATH"
208-
curl https://cursor.com/install -fsS --connect-timeout 10 --max-time 45 | bash
209-
if [ -x /root/.local/bin/agent ]; then
210-
ln -sfn /root/.local/bin/agent /usr/local/bin/agent
211-
fi
212-
command -v agent
213-
test -x "$(command -v agent)"
206+
install := `set +e
207+
export HOME="${HOME:-/root}"
208+
export PATH="/usr/local/bin:/root/.local/bin:$HOME/.local/bin:$PATH"
209+
curl https://cursor.com/install -fsS | bash
210+
# Re-resolve after install — do not trust a single hard-coded path.
211+
for candidate in \
212+
/usr/local/bin/agent \
213+
/root/.local/bin/agent \
214+
"$HOME/.local/bin/agent" \
215+
$(command -v agent 2>/dev/null) \
216+
$(ls -1 /root/.local/share/cursor-agent/versions/*/cursor-agent 2>/dev/null | sort | tail -1) \
217+
$(ls -1 "$HOME/.local/share/cursor-agent/versions/"*/cursor-agent 2>/dev/null | sort | tail -1)
218+
do
219+
[ -n "$candidate" ] || continue
220+
[ -e "$candidate" ] || continue
221+
if [ -x "$candidate" ] || [ -L "$candidate" ]; then
222+
ln -sfn "$candidate" /usr/local/bin/agent
223+
ln -sfn "$candidate" /root/.local/bin/agent
224+
printf '%s\n' "$candidate"
225+
exit 0
226+
fi
227+
done
228+
exit 1
214229
`
215230
installResult, installErr := executil.Run(ctx, workDir, install, env)
216231
if installErr != nil {
@@ -242,29 +257,34 @@ test -x "$(command -v agent)"
242257
}
243258

244259
func whichCursorBin(ctx context.Context, workDir, bin string, env []string) (string, error) {
245-
script := fmt.Sprintf(
246-
`export PATH="/usr/local/bin:/root/.local/bin:$PATH"
247-
if [ -x %s ]; then
248-
printf '%%s\n' %s
249-
exit 0
250-
fi
251-
resolved="$(command -v %s || true)"
252-
if [ -n "$resolved" ] && [ -x "$resolved" ]; then
253-
printf '%%s\n' "$resolved"
254-
exit 0
255-
fi
260+
script := `set +e
261+
export HOME="${HOME:-/root}"
262+
export PATH="/usr/local/bin:/root/.local/bin:$HOME/.local/bin:$PATH"
263+
for candidate in \
264+
` + shellQuote(bin) + ` \
265+
/usr/local/bin/agent \
266+
/root/.local/bin/agent \
267+
"$HOME/.local/bin/agent" \
268+
$(command -v agent 2>/dev/null) \
269+
$(command -v cursor-agent 2>/dev/null) \
270+
$(ls -1 /root/.local/share/cursor-agent/versions/*/cursor-agent 2>/dev/null | sort | tail -1) \
271+
$(ls -1 "$HOME/.local/share/cursor-agent/versions/"*/cursor-agent 2>/dev/null | sort | tail -1)
272+
do
273+
[ -n "$candidate" ] || continue
274+
[ -e "$candidate" ] || continue
275+
if [ -x "$candidate" ] || [ -L "$candidate" ]; then
276+
printf '%s\n' "$candidate"
277+
exit 0
278+
fi
279+
done
256280
exit 1
257-
`,
258-
shellQuote(bin),
259-
shellQuote(bin),
260-
shellQuote(bin),
261-
)
281+
`
262282
result, err := executil.Run(ctx, workDir, script, env)
263283
if err != nil {
264284
return "", err
265285
}
266286
if result.ExitCode != 0 {
267-
detail := executil.CombinedOutput(result)
287+
detail := strings.TrimSpace(result.Stdout)
268288
if detail == "" {
269289
detail = fmt.Sprintf("%s not found on PATH", bin)
270290
}
@@ -274,7 +294,6 @@ exit 1
274294
if resolved == "" {
275295
return "", fmt.Errorf("%s not found on PATH", bin)
276296
}
277-
// Prefer the last non-empty line (command -v output).
278297
lines := strings.Split(resolved, "\n")
279298
for i := len(lines) - 1; i >= 0; i-- {
280299
line := strings.TrimSpace(lines[i])

packages/scheduler/src/task-service.ts

Lines changed: 111 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2629,51 +2629,20 @@ export class TaskService {
26292629

26302630
/**
26312631
* Guests sometimes boot from snapshots where `agent` is missing or off PATH.
2632-
* Prefer locating a baked-in binary. Only attempt a short online install if
2633-
* cursor.com is reachable — never hang for minutes on SSL timeouts.
2632+
* Prefer locating a baked-in binary. Online install is a short fallback — the
2633+
* installer often succeeds while a naive `test -x /root/...` check still fails
2634+
* (HOME mismatch, progress bars on stderr, --version quirks).
26342635
*/
26352636
private async ensureCursorAgentInSandbox(
26362637
runtime: RuntimeClient,
26372638
taskId: string,
26382639
): Promise<void> {
2639-
const findCmd = [
2640-
"set +e",
2641-
'export PATH="/usr/local/bin:/root/.local/bin:$PATH"',
2642-
"for candidate in \\",
2643-
" /usr/local/bin/agent \\",
2644-
" /root/.local/bin/agent \\",
2645-
" $(command -v agent 2>/dev/null) \\",
2646-
" $(command -v cursor-agent 2>/dev/null) \\",
2647-
" $(ls -1 /root/.local/share/cursor-agent/versions/*/cursor-agent 2>/dev/null | sort | tail -1)",
2648-
"do",
2649-
' [ -n "$candidate" ] || continue',
2650-
' [ -x "$candidate" ] || continue',
2651-
' if "$candidate" --version >/dev/null 2>&1; then',
2652-
' printf "%s\\n" "$candidate"',
2653-
' "$candidate" --version 2>&1 | head -1',
2654-
" exit 0",
2655-
" fi",
2656-
"done",
2657-
"exit 1",
2658-
].join("\n");
2659-
2660-
const probe = await runtime.terminalAllowFailure({
2661-
taskId,
2662-
command: findCmd,
2663-
});
2664-
if (probe.exitCode === 0) {
2665-
const detail = (probe.stdout || "").trim();
2640+
const located = await this.findCursorAgentBinary(runtime, taskId);
2641+
if (located) {
26662642
this.emit("agent.log", taskId, "cursor agent CLI ready in sandbox", {
2667-
detail: detail.slice(0, 240),
2643+
detail: located.slice(0, 240),
26682644
});
2669-
// Ensure /usr/local/bin/agent exists for older runtime supervisors.
2670-
const binLine = detail.split("\n")[0]?.trim();
2671-
if (binLine && binLine !== "/usr/local/bin/agent") {
2672-
await runtime.terminalAllowFailure({
2673-
taskId,
2674-
command: `ln -sfn '${escapeShell(binLine)}' /usr/local/bin/agent`,
2675-
});
2676-
}
2645+
await this.linkCursorAgentBinary(runtime, taskId, located);
26772646
return;
26782647
}
26792648

@@ -2695,35 +2664,121 @@ export class TaskService {
26952664
taskId,
26962665
"cursor agent CLI missing — attempting short online install",
26972666
{
2698-
probe: (probe.stderr || probe.stdout || "").trim().slice(0, 300),
26992667
cursorCom: installHost.detail,
27002668
},
27012669
);
27022670

2671+
// Official installer. Do not use `set -e` across the pipe — curl progress
2672+
// noise on stderr previously masked a successful install, and HOME may not
2673+
// be /root inside the guest.
27032674
const install = await runtime.terminalAllowFailure({
27042675
taskId,
27052676
command: [
2706-
"set -e",
2707-
'export PATH="/usr/local/bin:/root/.local/bin:$PATH"',
2708-
"curl https://cursor.com/install -fsS --connect-timeout 10 --max-time 45 | bash",
2709-
"test -x /root/.local/bin/agent",
2710-
"ln -sfn /root/.local/bin/agent /usr/local/bin/agent",
2711-
"command -v agent",
2712-
"agent --version",
2677+
"set +e",
2678+
'export HOME="${HOME:-/root}"',
2679+
'export PATH="/usr/local/bin:/root/.local/bin:$HOME/.local/bin:$PATH"',
2680+
"curl https://cursor.com/install -fsS | bash",
2681+
"ec=$?",
2682+
'echo "cursor_install_exit=$ec home=$HOME"',
2683+
'ls -la /usr/local/bin/agent /root/.local/bin/agent "$HOME/.local/bin/agent" 2>&1 | head -20',
2684+
"ls -la /root/.local/share/cursor-agent/versions 2>&1 | tail -5",
2685+
'ls -la "$HOME/.local/share/cursor-agent/versions" 2>&1 | tail -5',
2686+
"exit 0",
27132687
].join("\n"),
27142688
});
2715-
if (install.exitCode !== 0) {
2716-
const detail = (install.stderr || install.stdout || "").trim();
2717-
throw new Error(
2718-
"cursor agent CLI is not available in the sandbox and install failed" +
2719-
(detail ? `: ${detail.slice(0, 400)}` : "") +
2720-
". Rebuild the agent Firecracker snapshot" +
2721-
" (./infra/scripts/rebuild-agent-snapshot.sh <instance-id>).",
2722-
);
2689+
2690+
const afterInstall = await this.findCursorAgentBinary(runtime, taskId);
2691+
if (afterInstall) {
2692+
await this.linkCursorAgentBinary(runtime, taskId, afterInstall);
2693+
this.emit("agent.log", taskId, "cursor agent CLI installed in sandbox", {
2694+
detail: afterInstall.slice(0, 240),
2695+
installLog: (install.stdout || "").trim().slice(0, 300),
2696+
});
2697+
return;
27232698
}
27242699

2725-
this.emit("agent.log", taskId, "cursor agent CLI installed in sandbox", {
2726-
detail: (install.stdout || "").trim().slice(0, 200),
2700+
const stdout = (install.stdout || "").trim();
2701+
const stderr = (install.stderr || "").trim();
2702+
// Prefer installer text over curl progress-bar spam on stderr.
2703+
const detail =
2704+
stdout
2705+
.split("\n")
2706+
.filter((line) => !/^#/.test(line.trim()) && !/^\s*[\d.]+%$/.test(line))
2707+
.join("\n")
2708+
.trim()
2709+
.slice(0, 500) ||
2710+
stderr
2711+
.split("\n")
2712+
.filter((line) => !line.includes("#") && !/\d+\.\d+%/.test(line))
2713+
.join("\n")
2714+
.trim()
2715+
.slice(0, 400) ||
2716+
"agent binary not found after install";
2717+
2718+
throw new Error(
2719+
"cursor agent CLI is not available in the sandbox after install" +
2720+
(detail ? `: ${detail}` : "") +
2721+
". Rebuild the agent Firecracker snapshot" +
2722+
" (./infra/scripts/rebuild-agent-snapshot.sh <instance-id>).",
2723+
);
2724+
}
2725+
2726+
private async findCursorAgentBinary(
2727+
runtime: RuntimeClient,
2728+
taskId: string,
2729+
): Promise<string | null> {
2730+
const findCmd = [
2731+
"set +e",
2732+
'export HOME="${HOME:-/root}"',
2733+
'export PATH="/usr/local/bin:/root/.local/bin:$HOME/.local/bin:$PATH"',
2734+
"for candidate in \\",
2735+
" /usr/local/bin/agent \\",
2736+
" /root/.local/bin/agent \\",
2737+
' "$HOME/.local/bin/agent" \\',
2738+
" $(command -v agent 2>/dev/null) \\",
2739+
" $(command -v cursor-agent 2>/dev/null) \\",
2740+
" $(ls -1 /root/.local/share/cursor-agent/versions/*/cursor-agent 2>/dev/null | sort | tail -1) \\",
2741+
' $(ls -1 "$HOME/.local/share/cursor-agent/versions/"*/cursor-agent 2>/dev/null | sort | tail -1)',
2742+
"do",
2743+
' [ -n "$candidate" ] || continue',
2744+
' [ -e "$candidate" ] || continue',
2745+
// Accept a present executable even if --version is flaky in the guest.
2746+
' if [ -x "$candidate" ] || [ -L "$candidate" ]; then',
2747+
' if "$candidate" --version >/dev/null 2>&1 || [ -x "$candidate" ]; then',
2748+
' printf "%s\\n" "$candidate"',
2749+
" exit 0",
2750+
" fi",
2751+
" fi",
2752+
"done",
2753+
"exit 1",
2754+
].join("\n");
2755+
2756+
const probe = await runtime.terminalAllowFailure({
2757+
taskId,
2758+
command: findCmd,
2759+
});
2760+
if (probe.exitCode !== 0) {
2761+
return null;
2762+
}
2763+
const bin = probe.stdout
2764+
.trim()
2765+
.split("\n")
2766+
.map((line) => line.trim())
2767+
.find((line) => line.startsWith("/") || line.includes("agent"));
2768+
return bin || null;
2769+
}
2770+
2771+
private async linkCursorAgentBinary(
2772+
runtime: RuntimeClient,
2773+
taskId: string,
2774+
bin: string,
2775+
): Promise<void> {
2776+
if (!bin || bin === "/usr/local/bin/agent") {
2777+
return;
2778+
}
2779+
await runtime.terminalAllowFailure({
2780+
taskId,
2781+
command: `mkdir -p /usr/local/bin /root/.local/bin && ln -sfn '${escapeShell(bin)}' /usr/local/bin/agent && ln -sfn '${escapeShell(bin)}' /root/.local/bin/agent`,
27272782
});
27282783
}
27292784

0 commit comments

Comments
 (0)