# Add CORS and Rate Limiting to a Ktor Server from a Linear Issue

Yes. An agent reads the labeled Linear issue, adds Ktor's CORS and RateLimit plugins to the kweet sample with tests, and opens a pull request linked to the issue.

[Run this on Rush](https://cloud.getrush.ai/run/add-cors/ktor/), in a Project that has your repository.

## What the agent does

The agent works from the issue text and the repository as they are, not from a template. In a repository shaped like ktorio/ktor-samples it opens `kweet/build.gradle.kts` and finds the `io.ktor.plugin` at 3.6.0 with Kotlin 2.4.20, a Gradle 9.2.1 wrapper, `jvmToolchain(17)`, and dependencies on `ktor-server-sessions`, `ktor-server-resources` and FreeMarker, with tests on `ktor-server-test-host-jvm` and mockk. It reads `kweet/src/main/kotlin/io/ktor/samples/kweet/KweetApplication.kt`, where `mainWithDependencies` installs DefaultHeaders, CallLogging, ConditionalHeaders, PartialContent, Resources, FreeMarker and Sessions, and it reads the route files to see which handlers change state: `PostNew.kt` posts new kweets at `/post-new` and `Delete.kt` posts deletes at `/kweet/{id}/delete`, while `Index.kt`, `ViewKweet.kt` and `UserPage.kt` only read.

The change is two plugins and a routing wrapper. In `KweetApplication.kt` the agent installs `CORS` with one configured origin via `allowHost` rather than `anyHost`, and installs `RateLimit` with a named limiter set to 60 requests per minute. Inside the `routing` block it applies that named limiter only to the posting routes, the `post<PostNew>` handler and the delete handler, and leaves every read route unlimited. If the CORS or RateLimit artifacts are not already on the compile classpath it adds them to the dependencies block of `build.gradle.kts`, and nothing else in the file moves.

For tests it extends `kweet/src/test/kotlin/KweetApplicationTest.kt`, which already builds the app with `testApplication` and a relaxed mockk `DAOFacade`, so no database is needed. It adds cases that match the issue: a request carrying the configured `Origin` header gets `Access-Control-Allow-Origin` back and a request from another origin does not, and posting to `/post-new` more than 60 times within the window answers 429 while a GET on `/` stays unlimited. Then it runs `./gradlew test` in the `kweet` directory with the sample's own wrapper until the suite passes.

Only then does it push a branch and open a pull request that names the Linear issue in its description. The repository's own CI does the rest: `.github/workflows/build-all.yml` runs `./build-all.sh` on every pull request to main on JDK 24, so the new test cases run there too, not only in the sandbox.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add CORS and Rate Limiting to a Ktor Server. In the kweet sample, install the CORS plugin for one configured origin and the RateLimit plugin at 60 requests per minute on the posting routes.\" Do the work in this repository, add tests for it, run ./gradlew test in the sample, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch. The diff touches `kweet/src/main/kotlin/io/ktor/samples/kweet/KweetApplication.kt` for the two `install` blocks and the rate-limited routing wrapper, `PostNew.kt` and `Delete.kt` only where the wrapper requires it, `kweet/build.gradle.kts` when a plugin artifact was missing, and `kweet/src/test/kotlin/KweetApplicationTest.kt` with the new CORS and rate-limit cases. The description quotes the issue, names the configured origin and the 60-per-minute limit, and links back to the Linear issue so the tracker closes the loop.

The pull request's checks are the proof. The `build-all` workflow the repository already has runs `./build-all.sh` on the pull request, and the session's trajectory, read at `GET /o/{org}/sessions/{id}/messages`, keeps every `./gradlew test` invocation with its output, so a reviewer can see the suite pass rather than take the description's word for it.

Before merging, a reviewer checks three things: that the allowed origin is the real frontend rather than a placeholder, that the named limiter wraps only the posting routes and not the whole site, and that a rate-limited form post answers 429 where the handlers otherwise redirect on bad input. Everything the agent did is in the diff and the trajectory; nothing reaches main until the pull request merges.

## Run it on every Linear issue labeled agent

Sign in to Linear once with `POST /me/connections {"type": "app", "name": "linear"}` and let the console finish the authorize URL. Your server then watches Linear for the label (a Linear webhook on `issue.labeled` with the `agent` label) and starts one run per labeled issue, naming the issue URL in `resources`: `https://linear.app/<workspace>/issue/ENG-42` puts the issue text in the prompt, read through your Linear connection, and `resources` names the repository the agent works in. The prompt tells the agent to do what the issue says and open a pull request that links back.

Set a webhook URL on the API key and Rush POSTs `run.completed` or `run.failed` when each run's turn finishes, with the session URL, so your server can post the pull request link as a comment on the issue. A key runs at most `max_concurrent` sessions at once (4 by default) and answers `429` past that, so a burst of labeled issues should queue. When `resources` names a repository, the Rush GitHub App must be installed on it, which is the one step the API cannot do for you.

## Why run it on Rush?

Picking up a labeled issue on a laptop means checking out the repository, warming a Gradle daemon and running `./gradlew test` yourself before you even start, and the work stalls the moment you close the machine. On Rush the run happens in its own cloud sandbox with its own clone: the agent edits `KweetApplication.kt`, runs the sample's Gradle 9.2.1 wrapper against the JDK 17 toolchain the build file declares, and pushes only after the suite is green, while your working tree stays untouched.

Labeling issues also rarely stops at one. Ten labeled issues means ten sandboxes and ten independent pull requests at once, each within the key's `max_concurrent` limit, instead of ten serial checkouts on one machine. And the result is checkable rather than claimed: the pull request carries the diff and the issue link, the repository's `build-all` workflow runs on it, and the trajectory keeps every Gradle command and its output, so the proof of the change is the change itself.

## Frequently asked questions

### How does the run read the Linear issue?

You sign in to Linear once from the console, which creates a `linear` app connection on your account. After that, naming an issue URL like `https://linear.app/<workspace>/issue/ENG-42` in `resources` puts the issue text in the prompt, read through that connection. The agent never sees your Linear credentials, and the run itself carries only `agent`, `input` and `resources`.

### What if the issue does not say which origin or limit to use?

This issue names both: one configured origin for CORS and 60 requests per minute for the limiter. If your issue leaves one out, the agent picks a conservative default, states it in the pull request description, and links back so you can correct it in review before merging.

### Will rate limiting slow down the whole site?

No. The limiter is a named `RateLimit` configuration applied inside the `routing` block only to the posting routes, the new-kweet and delete handlers in `PostNew.kt` and `Delete.kt`. Read routes such as `/`, `/kweet/{id}` and `/user/{user}` stay outside the wrapper, and the diff shows exactly which routes are wrapped.

### Which Java and Gradle versions does it test with?

The sample's own toolchain: the Gradle 9.2.1 wrapper checked into `kweet/gradle/wrapper` and the `jvmToolchain(17)` line in `kweet/build.gradle.kts`. The agent runs `./gradlew test` from the `kweet` directory, the same versions the repository's `build-all.yml` workflow builds with on pull requests.

### How do I know the tests actually ran?

Two ways. The session's trajectory at `GET /o/{org}/sessions/{id}/messages` keeps every `./gradlew test` invocation with its full output, and the pull request triggers the repository's existing `build-all` workflow, which runs `./build-all.sh` on JDK 24. If the suite could not run, the run ends `run.failed` and the webhook says so instead of reporting a pull request.
