From 06a8ce5dd4f812dcf84016862635c20d1d301afa Mon Sep 17 00:00:00 2001 From: jpertschuk Date: Fri, 21 Aug 2026 15:54:06 -0400 Subject: [PATCH] Fix MicroVM guests so claimed pool workers actually connect. Install the arm64 CLI (these VMs are aarch64), stop forwarding api.cursor.com as the worker auth host, and pass the claimed worker id. Controller pool/repo filters now come from CloudFormation instead of a single PoolName. Co-authored-by: Cursor --- .dockerignore | 2 + .github/workflows/ci.yml | 4 +- .gitignore | 2 + README.md | 19 +++++---- cloudformation.yaml | 36 +++++++++++++---- controller/handler.py | 65 +++++++++++++++++++++++++++--- deploy.sh | 25 ++++++++++-- microvm-image/Dockerfile | 9 ++++- microvm-image/cursor-agent-version | 2 +- microvm-image/entrypoint.sh | 18 ++++++++- microvm-image/hook.py | 4 ++ spawn.sh | 12 +++++- 12 files changed, 167 insertions(+), 31 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9568d69..6a33e77 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,5 +4,7 @@ .env.* *.pem *.key +api_key +aws .DS_Store app.zip diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9f9607..735dca9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,9 @@ jobs: grep -q "AWS::Events::Rule" cloudformation.yaml grep -q "ReservedConcurrentExecutions: 1" cloudformation.yaml grep -q "rate(1 minute)" cloudformation.yaml - grep -q "CONTROLLER_RUN_SECONDS: \"300\"" cloudformation.yaml + grep -q "CONTROLLER_POOL_NAMES" cloudformation.yaml + grep -q "CONTROLLER_REPOSITORY_URLS" cloudformation.yaml + grep -q "CONTROLLER_ALL_POOLS" cloudformation.yaml grep -q "agent worker controller --spawn" controller/handler.py grep -q "aws lambda-microvms run-microvm" spawn.sh grep -q -- "--run-hook-payload" spawn.sh diff --git a/.gitignore b/.gitignore index cde0349..fce62ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,7 @@ dist/ .env.* *.pem *.key +api_key +aws .DS_Store app.zip diff --git a/README.md b/README.md index 15ab42d..f293b8b 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ A controller launches one MicroVM per pending pool request: 1. You start a cloud agent at [cursor.com/agents](https://cursor.com/agents) against a self-hosted pool. The request stays pending until a worker claims it. 2. The controller (`agent worker controller --spawn ./spawn.sh`) sees that request and runs [`spawn.sh`](spawn.sh). -3. `spawn.sh` calls [`aws lambda-microvms run-microvm`](https://docs.aws.amazon.com/cli/latest/reference/lambda-microvms/run-microvm.html) (`RunMicrovm`) and returns. `--run-hook-payload` forwards `CURSOR_*` into the guest. -4. The MicroVM `/run` hook ([`hook.py`](microvm-image/hook.py)) applies that payload and starts [`entrypoint.sh`](microvm-image/entrypoint.sh), which runs `cursor-agent worker start --pool --worker-dir`. The worker executes tool calls in your account. +3. `spawn.sh` calls [`aws lambda-microvms run-microvm`](https://docs.aws.amazon.com/cli/latest/reference/lambda-microvms/run-microvm.html) (`RunMicrovm`) and returns. `--run-hook-payload` forwards claim `CURSOR_*` into the guest (not `CURSOR_API_ENDPOINT` / `CURSOR_API_URL`; those point at the public REST host and make `worker start` treat the service-account key as invalid). +4. The MicroVM `/run` hook ([`hook.py`](microvm-image/hook.py)) applies that payload and starts [`entrypoint.sh`](microvm-image/entrypoint.sh), which runs `cursor-agent worker --pool --worker-dir start`. The worker executes tool calls in your account. 5. When the session is idle, the worker releases and the MicroVM can terminate. ## Key properties @@ -28,7 +28,7 @@ A controller launches one MicroVM per pending pool request: Product semantics: [Self-hosted pools](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool.md) ([repo-less / Any repo](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool.md#repo-less-pools), [pool names](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool.md#pool-names), [multiple repo roots](https://cursor.com/docs/cloud-agent/self-hosted-guides/pool.md#register-multiple-repo-roots)). `repo` and `pool` labels are reserved; the worker derives `repo=` from a git remote when one exists. -**This template’s default:** the controller Lambda runs `agent worker controller --spawn ./spawn.sh --pool default` (`PoolName` / `POOL_NAME` / `CURSOR_POOL` in [`cloudformation.yaml`](cloudformation.yaml)). The guest image is built with `POOL_NAME=default`. [`hook.py`](microvm-image/hook.py) does not clone. [`entrypoint.sh`](microvm-image/entrypoint.sh) starts the worker from `/opt/cursor/workspaces/workspace` (a `git init` with **no remote** unless `CURSOR_REPO_URL` / `REPO_URL` is set). Keep stack `PoolName` and image `POOL_NAME` the same. +**This template’s default:** the controller Lambda runs `agent worker controller --spawn ./spawn.sh --pool default` (`PoolNames` / `POOL_NAMES` in [`cloudformation.yaml`](cloudformation.yaml) and [`deploy.sh`](deploy.sh)). The guest image is built with `POOL_NAME=default`. [`hook.py`](microvm-image/hook.py) does not clone. [`entrypoint.sh`](microvm-image/entrypoint.sh) starts the worker from `/opt/cursor/workspaces/workspace` (a `git init` with **no remote** unless `CURSOR_REPO_URL` / `REPO_URL` is set). Keep stack `PoolNames` and image `POOL_NAME` the same. Pass `ALL_POOLS=true` or `REPOSITORY_URLS=...` instead of (or with) `POOL_NAMES` when you need `--all-pools` or `--repository`. ### Any-repo mode @@ -82,7 +82,10 @@ Users pick the repo in the dashboard (the pool appears under that repo). The poo 2. Deploy the stack. `./deploy.sh` builds the controller image and runs `aws cloudformation deploy` on [`cloudformation.yaml`](cloudformation.yaml): ```bash - ./deploy.sh + POOL_NAMES=default ./deploy.sh + # POOL_NAMES=gpu,default ./deploy.sh + # REPOSITORY_URLS=https://github.com/org/repo ./deploy.sh + # ALL_POOLS=true ./deploy.sh ``` 3. Build the worker image from [`microvm-image/`](microvm-image/) (needs the stack outputs): @@ -110,7 +113,7 @@ Users pick the repo in the dashboard (the pool appears under that repo). The poo Open [cursor.com/agents](https://cursor.com/agents). Choose **Self-hosted**. -- **Any-repo mode:** pick the **Any repo** group and the pool name (`default` unless you overrode `PoolName`). +- **Any-repo mode:** pick the **Any repo** group and the pool name (`default` unless you overrode `PoolNames`). - **Repo-bound mode:** pick the repo. The pool appears under that repo. ## Alternative: run the controller locally @@ -131,7 +134,7 @@ Assume stack output `SpawnRoleArn` so `spawn.sh` can call `run-microvm`. `MICROV ## Monitoring -Application logs go to CloudWatch under the MicroVM image name (`cursor-pool-worker`). +Guest logs go to CloudWatch log group `/aws/lambda/microvms/cursor-pool-worker`. Controller logs are `/aws/lambda/cursor-lambda-workers-controller`. List running MicroVMs: @@ -145,7 +148,7 @@ aws lambda-microvms list-microvms --image-identifier cursor-pool-worker | --- | --- | | Image build fails (S3 or IAM) | Confirm stack outputs `ArtifactBucketName` and `BuildRoleArn`. The zip must land in that bucket, and the build role must be able to read it. | | No MicroVM | Confirm the controller is running and can call `run-microvm`. For a local controller, assume `SpawnRoleArn`. Confirm image `cursor-pool-worker` exists. | -| Worker dies immediately | The guest needs `CURSOR_API_KEY` (SSM `/cursor-lambda-workers/cursor-api-key`). Confirm the `/run` hook started `cursor-agent worker start --pool`. | +| Worker dies immediately | The guest needs `CURSOR_API_KEY` (SSM `/cursor-lambda-workers/cursor-api-key`). Confirm the `/run` hook started `cursor-agent worker --pool … start`. If logs show `Exec format error` on `node`, the image installed the wrong CLI arch (MicroVMs here are aarch64). If auth says the API key is invalid, do not set `CURSOR_API_ENDPOINT` to `https://api.cursor.com`. | | CLI too old for `controller` | Pin a lab version in `microvm-image/cursor-agent-version` and rebuild the MicroVM image and the controller image. | ## Related resources @@ -162,6 +165,6 @@ aws lambda-microvms list-microvms --image-identifier cursor-pool-worker Optional. To test a CLI off `main`, put a lab version (`YYYY.MM.DD-`) in [`microvm-image/cursor-agent-version`](microvm-image/cursor-agent-version) and rebuild the MicroVM image **and** the controller image. -Lab builds: [cursor.com/install?channel=lab](https://cursor.com/install?channel=lab), the `agent-cli.yml` GitHub Action on `main`, or `https://downloads.cursor.com/lab//linux/x64/agent-cli-package.tar.gz`. +Lab builds: [cursor.com/install?channel=lab](https://cursor.com/install?channel=lab), the `agent-cli.yml` GitHub Action on `main`, or `https://downloads.cursor.com/lab//linux/arm64/agent-cli-package.tar.gz` (x64 for amd64 guests). Leave the file empty (comments only) to use the prod installer at [cursor.com/install](https://cursor.com/install). diff --git a/cloudformation.yaml b/cloudformation.yaml index 640afce..118163f 100644 --- a/cloudformation.yaml +++ b/cloudformation.yaml @@ -14,10 +14,23 @@ Parameters: ControllerImageUri: Type: String Description: ECR image URI for the controller Lambda (controller/Dockerfile). Build with ./deploy.sh. - PoolName: + PoolNames: Type: String - Default: default - Description: Pool label workers register as (`agent worker controller --pool` / `worker start --pool`). + Default: "" + Description: >- + Comma-separated pool names for `agent worker controller --pool` (repeatable). + Mutually exclusive with AllPools=true. Set PoolNames, AllPools, and/or RepositoryUrls. + AllPools: + Type: String + AllowedValues: ["true", "false"] + Default: "false" + Description: If true, pass `--all-pools` (team-wide). Mutually exclusive with PoolNames. + RepositoryUrls: + Type: String + Default: "" + Description: >- + Comma-separated repository URLs for `agent worker controller --repository`. + Required for repo-scoped API keys. Can combine with PoolNames or AllPools. MicroVmImageIdentifier: Type: String Default: cursor-pool-worker @@ -139,7 +152,9 @@ Resources: Statement: - Effect: Allow Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] - Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${ProjectName}-controller*" + Resource: + - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${ProjectName}-controller*" + - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/microvms/*" - Effect: Allow Action: ssm:GetParameter Resource: !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CursorApiKeyParamName}" @@ -181,8 +196,9 @@ Resources: ImageUri: !Ref ControllerImageUri Environment: Variables: - CURSOR_POOL: !Ref PoolName - POOL_NAME: !Ref PoolName + CONTROLLER_POOL_NAMES: !Ref PoolNames + CONTROLLER_ALL_POOLS: !Ref AllPools + CONTROLLER_REPOSITORY_URLS: !Ref RepositoryUrls SPAWN_SCRIPT: /var/task/spawn.sh CURSOR_AGENT_BIN: /usr/local/bin/agent CONTROLLER_RUN_SECONDS: "300" @@ -234,5 +250,9 @@ Outputs: Value: !GetAtt MicroVmExecutionRole.Arn SpawnRoleArn: Value: !GetAtt SpawnRole.Arn - PoolName: - Value: !Ref PoolName + PoolNames: + Value: !Ref PoolNames + AllPools: + Value: !Ref AllPools + RepositoryUrls: + Value: !Ref RepositoryUrls diff --git a/controller/handler.py b/controller/handler.py index b96885c..141ada6 100755 --- a/controller/handler.py +++ b/controller/handler.py @@ -15,16 +15,71 @@ SHUTDOWN_GRACE_SECONDS = 15 +def split_names(value: str | None) -> list[str]: + parts: list[str] = [] + for chunk in (value or "").replace(";", ",").split(","): + name = chunk.strip() + if name: + parts.append(name) + return parts + + +def truthy(value: str | None) -> bool: + return (value or "").strip().lower() in {"1", "true", "yes", "on"} + + +def extra_has_flag(extra: list[str], *names: str) -> bool: + flags = set(names) + prefixes = tuple(f"{name}=" for name in names) + return any(part in flags or part.startswith(prefixes) for part in extra) + + def controller_args(env: dict[str, str]) -> list[str]: script = env.get("SPAWN_SCRIPT") or os.path.join(os.getcwd(), "spawn.sh") extra = [part for part in env.get("CURSOR_WORKER_CONTROLLER_ARGS", "").split() if part] args = ["worker", "controller", "--spawn", script] - has_pool = any( - part == "--pool" or part == "--all-pools" or part.startswith("--pool=") for part in extra + + all_pools = extra_has_flag(extra, "--all-pools") or truthy( + env.get("CONTROLLER_ALL_POOLS") or env.get("ALL_POOLS") + ) + pools = split_names( + env.get("CONTROLLER_POOL_NAMES") + or env.get("POOL_NAMES") + or env.get("POOL_NAME") + or env.get("CURSOR_POOL") + ) + repos = split_names( + env.get("CONTROLLER_REPOSITORY_URLS") + or env.get("REPOSITORY_URLS") + or env.get("CURSOR_REPO_URL") ) - if not has_pool: - pool = env.get("POOL_NAME") or env.get("CURSOR_POOL") or "default" - args.extend(["--pool", pool]) + + if extra_has_flag(extra, "--pool", "--all-pools"): + pass + elif all_pools: + args.append("--all-pools") + elif pools: + for pool in pools: + args.extend(["--pool", pool]) + elif repos: + # CLI requires --pool or --all-pools; repo filters apply on top. + args.append("--all-pools") + + if not extra_has_flag(extra, "--repository"): + for repo in repos: + args.extend(["--repository", repo]) + + if ( + not extra_has_flag(extra, "--pool", "--all-pools", "--repository") + and not all_pools + and not pools + and not repos + ): + raise RuntimeError( + "configure CONTROLLER_POOL_NAMES, CONTROLLER_ALL_POOLS=true, " + "and/or CONTROLLER_REPOSITORY_URLS" + ) + args.extend(extra) return args diff --git a/deploy.sh b/deploy.sh index 8f14308..54a497f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -14,10 +14,22 @@ ACCOUNT="$(aws sts get-caller-identity --query Account --output text "${REGION_A REPO="${CONTROLLER_ECR_REPO:-cursor-lambda-workers-controller}" TAG="${CONTROLLER_IMAGE_TAG:-$(git rev-parse --short HEAD 2>/dev/null || echo local)-$(date +%Y%m%d%H%M%S)}" URI="${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${REPO}:${TAG}" -POOL_NAME="${POOL_NAME:-default}" +POOL_NAMES="${POOL_NAMES:-${POOL_NAME:-}}" +ALL_POOLS="${ALL_POOLS:-false}" +case "${ALL_POOLS}" in + 1|true|TRUE|yes|YES|on|ON) ALL_POOLS=true ;; + *) ALL_POOLS=false ;; +esac +REPOSITORY_URLS="${REPOSITORY_URLS:-}" CURSOR_API_KEY_PARAM_NAME="${CURSOR_API_KEY_PARAM_NAME:-/cursor-lambda-workers/cursor-api-key}" MICROVM_IMAGE_IDENTIFIER="${MICROVM_IMAGE_IDENTIFIER:-cursor-pool-worker}" +if [[ -z "${POOL_NAMES}" && "${ALL_POOLS}" != "true" && -z "${REPOSITORY_URLS}" ]]; then + echo "Set POOL_NAMES (comma-separated), ALL_POOLS=true, and/or REPOSITORY_URLS so the controller knows what to serve." >&2 + echo "Example: POOL_NAMES=default ./deploy.sh" >&2 + exit 1 +fi + if ! command -v docker >/dev/null 2>&1; then echo "docker is required to build the controller Lambda image." >&2 exit 1 @@ -70,7 +82,9 @@ aws cloudformation deploy \ --capabilities CAPABILITY_NAMED_IAM \ --parameter-overrides \ "ControllerImageUri=${URI}" \ - "PoolName=${POOL_NAME}" \ + "PoolNames=${POOL_NAMES}" \ + "AllPools=${ALL_POOLS}" \ + "RepositoryUrls=${REPOSITORY_URLS}" \ "CursorApiKeyParamName=${CURSOR_API_KEY_PARAM_NAME}" \ "MicroVmImageIdentifier=${MICROVM_IMAGE_IDENTIFIER}" \ "${REGION_ARG[@]}" @@ -84,4 +98,9 @@ aws lambda invoke \ "${REGION_ARG[@]}" >/dev/null echo "Stack ${STACK_NAME} deployed. Controller ${FUNC} is running (5-minute SSE window; EventBridge rate(1 minute) restarts it)." -echo "Build the MicroVM image next, then start an agent from cursor.com/agents against pool ${POOL_NAME}." +if [[ "${ALL_POOLS}" == "true" ]]; then + echo "Serving all pools${REPOSITORY_URLS:+, repositories ${REPOSITORY_URLS}}." +else + echo "Serving pools ${POOL_NAMES:-none}${REPOSITORY_URLS:+, repositories ${REPOSITORY_URLS}}." +fi +echo "Build the MicroVM image next (POOL_NAME should match a pool this controller serves), then start an agent from cursor.com/agents." diff --git a/microvm-image/Dockerfile b/microvm-image/Dockerfile index f498455..7f4348b 100644 --- a/microvm-image/Dockerfile +++ b/microvm-image/Dockerfile @@ -11,10 +11,13 @@ RUN dnf install -y --setopt=install_weak_deps=0 \ # set the version in cursor-agent-version instead (ships in the zip). COPY cursor-agent-version /tmp/cursor-agent-version ARG CURSOR_AGENT_VERSION= -ARG CURSOR_AGENT_ARCH=x64 +# Guest MicroVMs here are aarch64. The previous x64 default installed a node +# binary that failed at runtime with "cannot execute binary file". +ARG CURSOR_AGENT_ARCH=arm64 RUN VERSION="$(sed -e 's/#.*//' -e 's/[[:space:]]//g' /tmp/cursor-agent-version | sed -e '/^$/d' | head -n 1)" && \ VERSION="${VERSION:-${CURSOR_AGENT_VERSION:-}}" && \ - ARCH="${CURSOR_AGENT_ARCH:-x64}" && \ + ARCH="${CURSOR_AGENT_ARCH:-arm64}" && \ + echo "installing cursor-agent ${VERSION:-prod} linux/${ARCH} (build uname=$(uname -m))" && \ if [ -n "${VERSION}" ]; then \ mkdir -p "/root/.local/share/cursor-agent/versions/${VERSION}" /root/.local/bin && \ curl -fsSL "https://downloads.cursor.com/lab/${VERSION}/linux/${ARCH}/agent-cli-package.tar.gz" \ @@ -29,6 +32,8 @@ RUN VERSION="$(sed -e 's/#.*//' -e 's/[[:space:]]//g' /tmp/cursor-agent-version rm -f /tmp/cursor-agent-version ENV PATH="/root/.cursor/bin:/root/.local/bin:/usr/local/bin:${PATH}" +ENV HOME=/root +ENV NODE_COMPILE_CACHE=/tmp/cursor-compile-cache WORKDIR /opt/cursor COPY entrypoint.sh hook.py /opt/cursor/ RUN chmod +x /opt/cursor/entrypoint.sh /opt/cursor/hook.py && mkdir -p /opt/cursor/workspaces diff --git a/microvm-image/cursor-agent-version b/microvm-image/cursor-agent-version index d5014f0..44969c9 100644 --- a/microvm-image/cursor-agent-version +++ b/microvm-image/cursor-agent-version @@ -5,4 +5,4 @@ # This file ships in the create-microvm-image zip (AWS may not pass Docker --build-arg). # Put the version on its own line, rebuild the MicroVM image and the controller image. # Prod CLI may omit `worker controller`; pin a lab version if `controller` is missing. -2026.08.20-f582eb6 +2026.08.21-4bf0f61 diff --git a/microvm-image/entrypoint.sh b/microvm-image/entrypoint.sh index fb658c0..a93453a 100755 --- a/microvm-image/entrypoint.sh +++ b/microvm-image/entrypoint.sh @@ -2,6 +2,8 @@ # Clone if needed, then exec cursor-agent worker start --pool. set -euo pipefail export GIT_TERMINAL_PROMPT=0 +export HOME="${HOME:-/root}" +export NODE_COMPILE_CACHE="${NODE_COMPILE_CACHE:-/tmp/cursor-compile-cache}" export PATH="/root/.cursor/bin:/root/.local/bin:/usr/local/bin:${PATH}" if [[ -z "${CURSOR_API_KEY:-}" && -n "${CURSOR_API_KEY_PARAM_NAME:-}" ]]; then @@ -12,6 +14,11 @@ if [[ -z "${CURSOR_API_KEY:-}" ]]; then echo "FATAL: set CURSOR_API_KEY or CURSOR_API_KEY_PARAM_NAME" >&2 exit 1 fi +# Controller Lambda sets these to https://api.cursor.com (public REST). +# `worker start` uses --endpoint for /auth/exchange_user_api_key, which lives +# on the default CLI host (api2.cursor.sh). Forwarding api.cursor.com makes +# every service-account key look invalid. +unset CURSOR_API_ENDPOINT CURSOR_API_URL POOL_NAME="${POOL_NAME:-${CURSOR_POOL:-default}}" IDLE_RELEASE_TIMEOUT_SECONDS="${IDLE_RELEASE_TIMEOUT_SECONDS:-300}" @@ -37,9 +44,16 @@ fi cd "${dest}" AGENT_BIN="$(command -v cursor-agent || command -v agent)" +echo "entrypoint: pool=${POOL_NAME} dest=${dest} worker_id=${CURSOR_AGENT_WORKER_ID:-} agent=${AGENT_BIN} uname=$(uname -m)" >&2 NAME_ARGS=() if [[ -n "${CURSOR_WORKER_NAME:-}" ]]; then NAME_ARGS=(--name "${CURSOR_WORKER_NAME}") fi -exec "${AGENT_BIN}" worker start --pool "${POOL_NAME}" --worker-dir "${dest}" \ - --idle-release-timeout "${IDLE_RELEASE_TIMEOUT_SECONDS}" "${NAME_ARGS[@]}" +WORKER_ID_ARGS=() +if [[ -n "${CURSOR_AGENT_WORKER_ID:-}" ]]; then + WORKER_ID_ARGS=(--worker-id "${CURSOR_AGENT_WORKER_ID}") +fi +# Flags belong on `worker` (parent). `start` only accepts --verbose. +exec "${AGENT_BIN}" worker --pool "${POOL_NAME}" --worker-dir "${dest}" \ + --idle-release-timeout "${IDLE_RELEASE_TIMEOUT_SECONDS}" \ + "${WORKER_ID_ARGS[@]}" "${NAME_ARGS[@]}" start --verbose diff --git a/microvm-image/hook.py b/microvm-image/hook.py index 585d141..dfa7bc4 100755 --- a/microvm-image/hook.py +++ b/microvm-image/hook.py @@ -18,6 +18,10 @@ def do_POST(self): env = json.loads(payload) if isinstance(payload, str) else payload if isinstance(env, dict): os.environ.update({str(k): str(v) for k, v in env.items() if v is not None}) + os.environ.setdefault("HOME", "/root") + os.environ.setdefault("NODE_COMPILE_CACHE", "/tmp/cursor-compile-cache") + keys = sorted(k for k in os.environ if k.startswith("CURSOR_")) + print(f"hook /run microvmId={body.get('microvmId')} cursor_keys={keys}", flush=True) subprocess.Popen( ["/opt/cursor/entrypoint.sh"], env=os.environ.copy(), diff --git a/spawn.sh b/spawn.sh index a60330d..6ddf137 100755 --- a/spawn.sh +++ b/spawn.sh @@ -15,8 +15,16 @@ if [[ "${IMAGE}" != arn:* ]]; then IMAGE="arn:aws:lambda:${REGION}:${ACCOUNT}:microvm-image:${IMAGE}" fi -PAYLOAD="$(python3 -c 'import json,os; print(json.dumps({k:v for k,v in os.environ.items() if k.startswith("CURSOR_")}))')" +# Claim metadata plus the API key. Do not forward CURSOR_API_ENDPOINT / +# CURSOR_API_URL from the controller Lambda — those point at api.cursor.com, +# which is the public REST host, not the worker auth exchange. +PAYLOAD="$(python3 -c 'import json,os +skip={"CURSOR_API_ENDPOINT","CURSOR_API_URL","CURSOR_API_KEY_PARAM_NAME"} +print(json.dumps({k:v for k,v in os.environ.items() if k.startswith("CURSOR_") and k not in skip}))')" +# Outbound-only workers never hit the HTTPS ingress, so a short default idle +# policy would suspend the VM before the agent connects. Guest logs go to +# /aws/lambda/microvms/cursor-pool-worker (execution role already allows it). exec aws lambda-microvms run-microvm \ --region "${REGION}" \ --image-identifier "${IMAGE}" \ @@ -24,4 +32,6 @@ exec aws lambda-microvms run-microvm \ --ingress-network-connectors "arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:ALL_INGRESS" \ --egress-network-connectors "arn:aws:lambda:${REGION}:aws:network-connector:aws-network-connector:INTERNET_EGRESS" \ --maximum-duration-in-seconds 28800 \ + --idle-policy '{"autoResumeEnabled":false,"maxIdleDurationSeconds":28800,"suspendedDurationSeconds":28800}' \ + --logging '{"cloudWatch":{"logGroup":"/aws/lambda/microvms/cursor-pool-worker"}}' \ --run-hook-payload "${PAYLOAD}"