feat: add Origin (Cursor) adapter - #133
Conversation
Adds a Git adapter for Origin, Cursor's code hosting platform:
- EdDSA (Ed25519) app JWTs signed with ext-sodium, exchanged for
short-lived installation access tokens
- Content writes (files, branches, tags) over Git HTTPS, since the
partner API is read-only for repository contents
- Check runs mapped onto Origin's upsert-by-key model
- Ed25519 webhook signature validation and delivery-envelope parsing,
including multi-ref push deliveries
- Full partner API surface: pull request update/merge/listing, reviews,
commit listing/files/compare, batch contents, batch check-run upsert,
app/installation/webhook-delivery management, rate limit, mirror sync
- Repository deletion via the Cursor web API (not in the partner API)
Base test additions: capability flags for providers without repository
deletion, visibility flags, or archive downloads. Empty JSON POST bodies
now encode as {} so proto3-JSON endpoints accept them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Origin models CI feedback as check runs only, so updateCommitStatus() upserts a check run keyed on the status context - repeated updates for one context land on one run - and getCommitStatuses() reads the commit's check runs back in commit-status shape. Consumers that only speak commit statuses now work against Origin unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every other adapter reports repositories with a pushed_at timestamp, so consumers read that name - Appwrite's repository listing does, and was rendering an unknown date for every Origin repository. Alias Origin's camelCase pushedAt (falling back to updatedAt/createdAt) on every repository object the adapter returns, keeping the provider's own fields intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Origin answers every failure with a Google-RPC body whose message names the exact rule that refused the request (missing namespace access, plan eligibility, stale page tokens). Bare status codes hid that, so every failed-call exception now appends it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/commits/{sha} takes one path segment, so a branch like feature/x read
as extra segments and answered 404. Origin's gateway accepts a
percent-encoded slash, so the commits endpoints (commit, files, check
runs) now fully encode the ref. The git/ref path keeps literal slashes;
its binding spans segments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Origin renders comment markdown without an image proxy, so images on a consumer's own host cannot display there; consumers can now ask and fall back to text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cally Repository creation is denied to app installations and the partner API has no repository deletion endpoint, so the shared adapter tests can neither create nor clean up their fixture repositories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Origin scopes every repository to its workspace, so nothing it hosts is anonymously reachable. The new Base test proves publicness end to end: a public repository has to answer an anonymous git ref advertisement, and a private one has to refuse the same request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Origin E2E suite that needed deletion-less teardown and gated visibility checks is gone, so the supportsRepositoryDeletion, reportsRepositoryVisibility and supportsRepositoryArchives test flags guarded nothing. Base.php now diverges from main only by the anonymous public-access test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
supportsRepositoryArchives(), supportsCommentImages(), supportsPublicRepositories(), the anonymous public-access test and the non-Origin README marks now land through #134; Origin keeps only its own overrides here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThe PR adds an Origin/Cursor Git provider with Ed25519 authentication and webhook verification, normalized repository and pull-request operations, Git-backed content writes, and provider-specific tests.
Confidence Score: 3/5The PR is not safe to merge until createFile validates the destination before any recursive directory creation can follow repository-controlled symlinks. Recursive mkdir can modify a writable host path through a checked-out symlink before the subsequent confinement check rejects the file operation. Files Needing Attention: src/VCS/Adapter/Git/Origin.php
|
| Filename | Overview |
|---|---|
| src/VCS/Adapter/Git/Origin.php | Adds the Origin integration, but createFile performs recursive directory creation before validating symlink-resolved confinement. |
| tests/VCS/Adapter/OriginTest.php | Adds runnable credential-free Origin behavior tests while isolating the unsupported live suite. |
| src/VCS/Adapter.php | Serializes empty JSON parameter arrays as an empty object for APIs requiring object-shaped request bodies. |
| phpunit.xml | Registers the Origin test suite, although existing CI suite matrices do not select it. |
| README.md | Lists Origin as a supported VCS adapter. |
Prompt To Fix All With AI
### Issue 1
src/VCS/Adapter/Git/Origin.php:1805
**Symlink followed before confinement**
When a checked-out repository contains a directory symlink to a writable host directory and `createFile()` targets a missing descendant, recursive `mkdir()` follows the symlink before the parent is validated, creating a persistent directory outside the temporary checkout. Cleanup removes only the checkout.
**How this was verified:** The checkout materializes repository symlinks, recursive `mkdir()` runs before `realpath()` confinement, and cleanup removes only the checkout.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (3): Last reviewed commit: "fix: refuse symlinked write targets and ..." | Re-trigger Greptile
…he end A caller-supplied path could climb out of the temporary checkout through ../ segments and write onto the host filesystem before git add ever saw it; paths are now normalized lexically and rejected once they escape. Repository listing also stopped after a thousand entries even with a next-page token in hand, hiding later repositories from lookups; it now follows the token until the provider stops advancing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Webhook signature verification and delivery parsing never touch the network, so they run in CI again: Ed25519 validation with rotation and PEM keys, push and pull request normalization, lifecycle action mapping, and installation events. Only the live half of the shared suite stays skipped, since fixture repositories can neither be created nor deleted through the partner API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lexical confinement was not enough once the checkout itself carried symlinks - a linked directory or file would carry the write outside the temporary directory - so createFile() now resolves the parent against the checkout and refuses link targets, validating the path before any network call. createRepository() and deleteRepository() no longer pretend: creation is denied to app installations wholesale and the partner API has no deletion endpoint, so both report themselves unsupported, matching the new capability methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What does this PR do?
Adds a new Git adapter for Origin, Cursor's code hosting platform (launched 2026-08-17), plus its test class.
Adapter (
src/VCS/Adapter/Git/Origin.php)ext-sodium, since Origin apps use Ed25519 keys — exchanged for a short-lived installation access token (oit_…), cached like the GitHub adapter's tokens.createFile,createBranchandcreateTagshell out togitwith the installation token (Basic auth, usernamex-access-token), with the token scrubbed from any error output.webhook-signature: v1ed,BASE64over the hex SHA-256 of<webhook-id>.<webhook-timestamp>.<raw body>);validateWebhookEvent()verifies that.getEvents()parses the delivery envelope, maps PR lifecycle event types to GitHub-style actions, and expands multi-ref push deliveries into one event per ref.POST cursor.com/api/origin/delete-repo) — the partner API has no deletion endpoint. Auth compatibility with app tokens is pending E2E verification.Not supported by Origin (methods throw or degrade gracefully)
Commit statuses (check runs only), repository visibility flags, archive/presigned downloads, language statistics, user lookup, per-repository webhooks (Origin delivers per app installation), namespace listing.
Test changes
tests/VCS/Adapter/OriginTest.php: full Base suite wiring plus Origin-specific tests (Ed25519 webhook validation incl. PEM keys, PR lifecycle action mapping, multi-ref pushes, merge/update/list PRs, reviews, batch contents, batch check runs, rate limit, app/installation metadata). NeedsTESTS_ORIGIN_PRIVATE_KEY,TESTS_ORIGIN_APP_IDENTIFIER,TESTS_ORIGIN_INSTALLATION_ID.tests/VCS/Base.php: new capability flags ($supportsRepositoryDeletion,$reportsRepositoryVisibility,$supportsRepositoryArchives) and teardown now goes throughdiscardRepositories()everywhere.src/VCS/Adapter.php: empty JSON POST bodies encode as{}instead of[](Origin's proto3-JSON endpoints reject bare arrays).Status
🤖 Generated with Claude Code