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}"