Skip to content

Commit 52cbcf6

Browse files
authored
Merge pull request #1 from anysphere/cursor/aws-lambda-microvm-workers-07b1
Lambda MicroVM spawn.sh for Cursor pool workers
2 parents 106976b + e4f5a0b commit 52cbcf6

9 files changed

Lines changed: 346 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
validate:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Template shape
14+
run: |
15+
test -f spawn.sh
16+
test -f cloudformation.yaml
17+
test -f microvm-image/Dockerfile
18+
test -f microvm-image/entrypoint.sh
19+
test -f microvm-image/hook.py
20+
test ! -f handler.mjs
21+
test ! -f spawn.mjs
22+
test ! -f package.json
23+
test ! -d src
24+
test ! -d test
25+
test ! -d controller
26+
! grep -q "AWS::Lambda::Function" cloudformation.yaml
27+
! grep -q "AWS::Events::Rule" cloudformation.yaml
28+
grep -q "aws lambda-microvms run-microvm" spawn.sh
29+
grep -q -- "--run-hook-payload" spawn.sh

‎.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules/
2+
dist/
3+
.aws-sam/
4+
.env
5+
.env.*
6+
*.pem
7+
*.key
8+
.DS_Store
9+
app.zip

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Anysphere
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,51 @@
1-
# aws-lambda-workers
2-
Template for running Cursor self-hosted pool workers on AWS Lambda MicroVMs
1+
# Cursor pool workers on AWS Lambda MicroVMs
2+
3+
`spawn.sh` starts a worker MicroVM. After the AWS resources exist, run:
4+
5+
```bash
6+
export MICROVM_IMAGE_IDENTIFIER=arn:aws:lambda:REGION:ACCOUNT:microvm-image:cursor-pool-worker
7+
export MICROVM_EXECUTION_ROLE_ARN=arn:aws:iam::ACCOUNT:role/cursor-lambda-workers-microvm-execution-role
8+
export CURSOR_API_KEY=YOUR_SERVICE_ACCOUNT_KEY
9+
10+
agent worker controller --spawn ./spawn.sh
11+
```
12+
13+
(`cursor-agent worker controller --spawn ./spawn.sh` is the same CLI.) Then start an agent from [cursor.com/agents](https://cursor.com/agents) against the pool.
14+
15+
`spawn.sh` calls [`aws lambda-microvms run-microvm`](https://docs.aws.amazon.com/cli/latest/reference/lambda-microvms/run-microvm.html) and forwards `CURSOR_*` (API key, worker name, user email/id, repo, pool, request id) as `--run-hook-payload`. A tiny `/run` hook exports those vars and starts `cursor-agent worker start --pool`.
16+
17+
## AWS resources
18+
19+
Put the API key in SSM, then apply `cloudformation.yaml` (artifact bucket, image build role, MicroVM execution role, spawn role):
20+
21+
```bash
22+
aws ssm put-parameter --type SecureString \
23+
--name /cursor-lambda-workers/cursor-api-key \
24+
--value "YOUR_SERVICE_ACCOUNT_KEY"
25+
26+
aws cloudformation deploy \
27+
--template-file cloudformation.yaml \
28+
--stack-name cursor-lambda-workers \
29+
--capabilities CAPABILITY_NAMED_IAM
30+
```
31+
32+
Build the worker image from `microvm-image/` (needs the stack outputs):
33+
34+
```bash
35+
BUCKET=$(aws cloudformation describe-stacks --stack-name cursor-lambda-workers \
36+
--query "Stacks[0].Outputs[?OutputKey=='ArtifactBucketName'].OutputValue" --output text)
37+
BUILD_ROLE=$(aws cloudformation describe-stacks --stack-name cursor-lambda-workers \
38+
--query "Stacks[0].Outputs[?OutputKey=='BuildRoleArn'].OutputValue" --output text)
39+
BASE=$(aws lambda-microvms list-managed-microvm-images --query "items[0].imageArn" --output text)
40+
( cd microvm-image && zip -r /tmp/app.zip . )
41+
aws s3 cp /tmp/app.zip "s3://${BUCKET}/app.zip"
42+
aws lambda-microvms create-microvm-image \
43+
--code-artifact "uri=s3://${BUCKET}/app.zip" \
44+
--name cursor-pool-worker \
45+
--base-image-arn "${BASE}" \
46+
--build-role-arn "${BUILD_ROLE}" \
47+
--environment-variables "POOL_NAME=default,CURSOR_API_KEY_PARAM_NAME=/cursor-lambda-workers/cursor-api-key" \
48+
--hooks '{"port":9000,"microvmImageHooks":{"ready":"ENABLED","readyTimeoutInSeconds":60},"microvmHooks":{"run":"ENABLED","runTimeoutInSeconds":60}}'
49+
```
50+
51+
Assume `SpawnRoleArn` (or equivalent IAM) so `spawn.sh` can call `run-microvm`.

‎cloudformation.yaml‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Description: IAM, S3, and build role for Cursor pool-worker Lambda MicroVMs.
3+
4+
Parameters:
5+
ProjectName:
6+
Type: String
7+
Default: cursor-lambda-workers
8+
AllowedPattern: "^[a-z0-9][a-z0-9-]{1,40}[a-z0-9]$"
9+
CursorApiKeyParamName:
10+
Type: String
11+
Default: /cursor-lambda-workers/cursor-api-key
12+
Description: SSM SecureString the MicroVM reads for CURSOR_API_KEY.
13+
14+
Resources:
15+
ArtifactBucket:
16+
Type: AWS::S3::Bucket
17+
DeletionPolicy: Retain
18+
UpdateReplacePolicy: Retain
19+
Properties:
20+
BucketName: !Sub "${ProjectName}-artifacts-${AWS::AccountId}-${AWS::Region}"
21+
BucketEncryption:
22+
ServerSideEncryptionConfiguration:
23+
- ServerSideEncryptionByDefault:
24+
SSEAlgorithm: AES256
25+
PublicAccessBlockConfiguration:
26+
BlockPublicAcls: true
27+
BlockPublicPolicy: true
28+
IgnorePublicAcls: true
29+
RestrictPublicBuckets: true
30+
31+
MicroVmExecutionRole:
32+
Type: AWS::IAM::Role
33+
Properties:
34+
RoleName: !Sub "${ProjectName}-microvm-execution-role"
35+
AssumeRolePolicyDocument:
36+
Version: "2012-10-17"
37+
Statement:
38+
- Effect: Allow
39+
Principal:
40+
Service: lambda.amazonaws.com
41+
Action: ["sts:AssumeRole", "sts:TagSession"]
42+
Policies:
43+
- PolicyName: guest
44+
PolicyDocument:
45+
Version: "2012-10-17"
46+
Statement:
47+
- Effect: Allow
48+
Action: ssm:GetParameter
49+
Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CursorApiKeyParamName}"
50+
- Effect: Allow
51+
Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
52+
Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/microvms/*"
53+
54+
BuildRole:
55+
Type: AWS::IAM::Role
56+
Properties:
57+
RoleName: !Sub "${ProjectName}-build-role"
58+
AssumeRolePolicyDocument:
59+
Version: "2012-10-17"
60+
Statement:
61+
- Effect: Allow
62+
Principal:
63+
Service: lambda.amazonaws.com
64+
Action: ["sts:AssumeRole", "sts:TagSession"]
65+
Policies:
66+
- PolicyName: build
67+
PolicyDocument:
68+
Version: "2012-10-17"
69+
Statement:
70+
- Effect: Allow
71+
Action: ["s3:GetObject", "s3:PutObject"]
72+
Resource: [!GetAtt ArtifactBucket.Arn, !Sub "${ArtifactBucket.Arn}/*"]
73+
- Effect: Allow
74+
Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
75+
Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/microvms/*"
76+
77+
SpawnRole:
78+
Type: AWS::IAM::Role
79+
Properties:
80+
RoleName: !Sub "${ProjectName}-spawn-role"
81+
AssumeRolePolicyDocument:
82+
Version: "2012-10-17"
83+
Statement:
84+
- Effect: Allow
85+
Principal:
86+
AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
87+
Action: sts:AssumeRole
88+
Policies:
89+
- PolicyName: run-microvm
90+
PolicyDocument:
91+
Version: "2012-10-17"
92+
Statement:
93+
- Effect: Allow
94+
Action: lambda:RunMicroVm
95+
Resource: "*"
96+
- Effect: Allow
97+
Action: iam:PassRole
98+
Resource: !GetAtt MicroVmExecutionRole.Arn
99+
- Effect: Allow
100+
Action: lambda:PassNetworkConnector
101+
Resource:
102+
- !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:aws:network-connector:aws-network-connector:ALL_INGRESS"
103+
- !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
104+
105+
Outputs:
106+
ArtifactBucketName:
107+
Value: !Ref ArtifactBucket
108+
BuildRoleArn:
109+
Value: !GetAtt BuildRole.Arn
110+
MicroVmExecutionRoleArn:
111+
Value: !GetAtt MicroVmExecutionRole.Arn
112+
SpawnRoleArn:
113+
Value: !GetAtt SpawnRole.Arn

‎microvm-image/Dockerfile‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ARG BASE_IMAGE=public.ecr.aws/lambda/microvms:al2023-minimal
2+
FROM ${BASE_IMAGE}
3+
4+
RUN dnf install -y --setopt=install_weak_deps=0 \
5+
bash tar gzip git ca-certificates findutils awscli python3 \
6+
$(command -v curl >/dev/null 2>&1 || echo curl) && \
7+
dnf clean all
8+
9+
RUN curl -fsSL https://cursor.com/install | bash && \
10+
ln -sf /root/.local/bin/agent /usr/local/bin/agent && \
11+
ln -sf /root/.local/bin/agent /usr/local/bin/cursor-agent
12+
13+
ENV PATH="/root/.cursor/bin:/root/.local/bin:/usr/local/bin:${PATH}"
14+
WORKDIR /opt/cursor
15+
COPY entrypoint.sh hook.py /opt/cursor/
16+
RUN chmod +x /opt/cursor/entrypoint.sh /opt/cursor/hook.py && mkdir -p /opt/cursor/workspaces
17+
ENV HOOK_PORT=9000
18+
ENTRYPOINT ["python3", "/opt/cursor/hook.py"]

‎microvm-image/entrypoint.sh‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# Clone if needed, then exec cursor-agent worker start --pool.
3+
set -euo pipefail
4+
export GIT_TERMINAL_PROMPT=0
5+
export PATH="/root/.cursor/bin:/root/.local/bin:/usr/local/bin:${PATH}"
6+
7+
if [[ -z "${CURSOR_API_KEY:-}" && -n "${CURSOR_API_KEY_PARAM_NAME:-}" ]]; then
8+
CURSOR_API_KEY="$(aws ssm get-parameter --name "${CURSOR_API_KEY_PARAM_NAME}" --with-decryption --query Parameter.Value --output text)"
9+
export CURSOR_API_KEY
10+
fi
11+
if [[ -z "${CURSOR_API_KEY:-}" ]]; then
12+
echo "FATAL: set CURSOR_API_KEY or CURSOR_API_KEY_PARAM_NAME" >&2
13+
exit 1
14+
fi
15+
16+
POOL_NAME="${POOL_NAME:-${CURSOR_POOL:-default}}"
17+
IDLE_RELEASE_TIMEOUT_SECONDS="${IDLE_RELEASE_TIMEOUT_SECONDS:-300}"
18+
WORKSPACES="${WORKSPACES:-/opt/cursor/workspaces}"
19+
REPO_URL="${REPO_URL:-${CURSOR_REPO_URL:-}}"
20+
mkdir -p "${WORKSPACES}"
21+
dest="${WORKSPACES}/workspace"
22+
23+
if [[ -n "${REPO_URL}" ]]; then
24+
dest="${WORKSPACES}/$(basename "${REPO_URL}" .git)"
25+
if [[ -d "${dest}/.git" ]]; then
26+
git -C "${dest}" fetch --all --prune --tags || true
27+
else
28+
git clone --filter=blob:none --single-branch "${REPO_URL}" "${dest}"
29+
fi
30+
elif [[ ! -d "${dest}/.git" ]]; then
31+
mkdir -p "${dest}"
32+
git -C "${dest}" init
33+
git -C "${dest}" config user.email "worker@local"
34+
git -C "${dest}" config user.name "cursor-worker"
35+
git -C "${dest}" commit --allow-empty -m "workspace"
36+
fi
37+
38+
cd "${dest}"
39+
AGENT_BIN="$(command -v cursor-agent || command -v agent)"
40+
NAME_ARGS=()
41+
if [[ -n "${CURSOR_WORKER_NAME:-}" ]]; then
42+
NAME_ARGS=(--name "${CURSOR_WORKER_NAME}")
43+
fi
44+
exec "${AGENT_BIN}" worker start --pool "${POOL_NAME}" --worker-dir "${dest}" \
45+
--idle-release-timeout "${IDLE_RELEASE_TIMEOUT_SECONDS}" "${NAME_ARGS[@]}"

‎microvm-image/hook.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""Tiny MicroVM hook listener. /run applies CURSOR_* then starts the worker."""
3+
from http.server import BaseHTTPRequestHandler, HTTPServer
4+
import json
5+
import os
6+
import subprocess
7+
8+
PORT = int(os.environ.get("HOOK_PORT", "9000"))
9+
10+
11+
class Hook(BaseHTTPRequestHandler):
12+
def do_POST(self):
13+
length = int(self.headers.get("Content-Length") or 0)
14+
raw = self.rfile.read(length) if length else b"{}"
15+
if self.path.rstrip("/").endswith("/run"):
16+
body = json.loads(raw or b"{}")
17+
payload = body.get("runHookPayload") or "{}"
18+
env = json.loads(payload) if isinstance(payload, str) else payload
19+
if isinstance(env, dict):
20+
os.environ.update({str(k): str(v) for k, v in env.items() if v is not None})
21+
subprocess.Popen(
22+
["/opt/cursor/entrypoint.sh"],
23+
env=os.environ.copy(),
24+
start_new_session=True,
25+
)
26+
self.send_response(200)
27+
self.end_headers()
28+
29+
def log_message(self, *_args):
30+
return
31+
32+
33+
HTTPServer(("0.0.0.0", PORT), Hook).serve_forever()

‎spawn.sh‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Spawn hook for `agent worker controller --spawn ./spawn.sh`.
3+
# Starts one Lambda MicroVM; does not wait for the agent.
4+
# Forwards CURSOR_* via --run-hook-payload (the only per-run channel).
5+
# https://docs.aws.amazon.com/cli/latest/reference/lambda-microvms/run-microvm.html
6+
set -euo pipefail
7+
8+
: "${MICROVM_IMAGE_IDENTIFIER:?set MICROVM_IMAGE_IDENTIFIER to the MicroVM image ARN (or name)}"
9+
: "${MICROVM_EXECUTION_ROLE_ARN:?set MICROVM_EXECUTION_ROLE_ARN to the guest IAM role}"
10+
11+
REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}"
12+
IMAGE="${MICROVM_IMAGE_IDENTIFIER}"
13+
if [[ "${IMAGE}" != arn:* ]]; then
14+
ACCOUNT="$(aws sts get-caller-identity --query Account --output text --region "${REGION}")"
15+
IMAGE="arn:aws:lambda:${REGION}:${ACCOUNT}:microvm-image:${IMAGE}"
16+
fi
17+
18+
PAYLOAD="$(python3 -c 'import json,os; print(json.dumps({k:v for k,v in os.environ.items() if k.startswith("CURSOR_")}))')"
19+
20+
exec aws lambda-microvms run-microvm \
21+
--region "${REGION}" \
22+
--image-identifier "${IMAGE}" \
23+
--execution-role-arn "${MICROVM_EXECUTION_ROLE_ARN}" \
24+
--ingress-network-connectors "arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:ALL_INGRESS" \
25+
--egress-network-connectors "arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:INTERNET_EGRESS" \
26+
--maximum-duration-in-seconds 28800 \
27+
--run-hook-payload "${PAYLOAD}"

0 commit comments

Comments
 (0)