( plugin: ScanInput, similar: SimilarPluginRow[], )
| 425 | } |
| 426 | |
| 427 | async function runSecurityAgent( |
| 428 | plugin: ScanInput, |
| 429 | similar: SimilarPluginRow[], |
| 430 | ): Promise<AgentVerdict> { |
| 431 | const tag = plugin.slug; |
| 432 | logInfo(tag, "runSecurityAgent start", { |
| 433 | components: plugin.components.length, |
| 434 | repository: plugin.repository, |
| 435 | similarCandidates: similar.length, |
| 436 | }); |
| 437 | |
| 438 | const apiKey = process.env.CURSOR_API_KEY; |
| 439 | if (!apiKey) { |
| 440 | throw new FatalScanError( |
| 441 | "CURSOR_API_KEY is not configured; cannot run plugin security scan.", |
| 442 | ); |
| 443 | } |
| 444 | |
| 445 | const repoMatch = plugin.repository |
| 446 | ? parseGitHubUrl(plugin.repository) |
| 447 | : null; |
| 448 | |
| 449 | // The agent runs in `local` mode against a scratch dir on the function's |
| 450 | // filesystem: either a fresh clone of the user's public repo, or an empty |
| 451 | // dir when no repo URL was supplied. This deliberately avoids the cloud |
| 452 | // runtime's GitHub-App-scoped repo permissions, which would require every |
| 453 | // plugin submitter to install Cursor's GitHub App on their repo — not a |
| 454 | // workable UX for a public marketplace. |
| 455 | let cwd: string; |
| 456 | let cleanup: () => Promise<void>; |
| 457 | let hasRepo = false; |
| 458 | try { |
| 459 | if (repoMatch) { |
| 460 | const cloned = await cloneRepo(repoMatch.owner, repoMatch.repo, tag); |
| 461 | cwd = cloned.cwd; |
| 462 | cleanup = cloned.cleanup; |
| 463 | hasRepo = true; |
| 464 | } else { |
| 465 | cwd = await mkdtemp(path.join(tmpdir(), "plugin-scan-no-repo-")); |
| 466 | cleanup = () => rm(cwd, { recursive: true, force: true }).catch(() => {}); |
| 467 | logInfo(tag, "no repo URL; using empty cwd", { cwd }); |
| 468 | } |
| 469 | } catch (err) { |
| 470 | if (err instanceof UnscannableRepoError) { |
| 471 | logError(tag, "repo unscannable; returning suspicious verdict", err); |
| 472 | return { |
| 473 | verdict: "suspicious", |
| 474 | severity: "low", |
| 475 | categories: [], |
| 476 | reasons: ["repository_clone_failed"], |
| 477 | summary: `Could not fetch ${plugin.repository}: ${err.message} Manual review required.`, |
| 478 | runId: null, |
| 479 | }; |
| 480 | } |
| 481 | // Transient failure (network, rate limit, GitHub 5xx): rethrow so the |
| 482 | // drain route leaves the message for pgmq to redeliver instead of |
| 483 | // permanently flagging the plugin over an infra hiccup. |
| 484 | logError(tag, "repo fetch failed; treating as retryable", err); |
no test coverage detected