diff --git a/.github/workflows/build-snapshot.yml b/.github/workflows/build-snapshot.yml index 681323a7..6cb1b458 100644 --- a/.github/workflows/build-snapshot.yml +++ b/.github/workflows/build-snapshot.yml @@ -1,8 +1,9 @@ name: Build Sandbox Snapshot -# Snapshot publication is an explicit production-adjacent operation. Each main -# commit maps to one immutable candidate; promotion happens by reviewing the -# DAYTONA_SANDBOX_SNAPSHOT change in the agent-worker configuration. +# Snapshot publication is an explicit production-adjacent operation. Each +# dispatch maps its main commit and workflow run to one immutable candidate; +# promotion happens by reviewing the DAYTONA_SANDBOX_SNAPSHOT change in the +# agent-worker configuration. on: workflow_dispatch: inputs: @@ -40,7 +41,7 @@ jobs: echo "Confirmation must exactly match BUILD_SNAPSHOT." >&2 exit 1 fi - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 @@ -66,7 +67,7 @@ jobs: - name: Derive immutable snapshot name id: snapshot run: | - snapshot_name="cheatcode-sandbox-viewer-bundle-$(git rev-parse --short=12 HEAD)" + snapshot_name="cheatcode-sandbox-viewer-bundle-$(git rev-parse --short=12 HEAD)-${GITHUB_RUN_ID}" echo "name=$snapshot_name" >> "$GITHUB_OUTPUT" echo "SNAPSHOT_NAME=$snapshot_name" >> "$GITHUB_ENV" - name: Install checksum-verified Daytona CLI @@ -86,21 +87,6 @@ jobs: test -n "$DAYTONA_API_KEY" "$RUNNER_TEMP/bin/daytona" version "$RUNNER_TEMP/bin/daytona" login --api-key "$DAYTONA_API_KEY" - - name: Refuse snapshot-name reuse - run: | - page=1 - while true; do - snapshot_list="$(daytona snapshot list --format json --limit 200 --page "$page")" - if jq --exit-status --arg name "$SNAPSHOT_NAME" \ - '.[] | select(.name == $name)' <<< "$snapshot_list" > /dev/null; then - echo "Snapshot $SNAPSHOT_NAME already exists; immutable names are never overwritten." >&2 - exit 1 - fi - if [ "$(jq 'length' <<< "$snapshot_list")" -lt 200 ]; then - break - fi - page=$((page + 1)) - done - name: Build immutable image candidate run: | image_tag="cheatcode-sandbox:$GITHUB_SHA" @@ -145,26 +131,237 @@ jobs: test -f /opt/cheatcode-browser-driver/server.js node --check /opt/cheatcode-browser-driver/server.js ' - - name: Publish immutable snapshot candidate - run: | - daytona snapshot push "$IMAGE_TAG" \ - --name "$SNAPSHOT_NAME" \ - --cpu 2 --memory 4 --disk 10 - - name: Verify published candidate + - name: Publish and verify immutable snapshot candidate run: | - page=1 - while true; do - snapshot_list="$(daytona snapshot list --format json --limit 200 --page "$page")" - if jq --exit-status --arg name "$SNAPSHOT_NAME" \ - '.[] | select(.name == $name and .state == "active")' \ - <<< "$snapshot_list" > /dev/null; then - exit 0 + set -Eeuo pipefail + shopt -s inherit_errexit + + readonly MAX_PUSH_ATTEMPTS=3 + readonly STATE_POLL_ATTEMPTS=36 + readonly REMOVE_POLL_ATTEMPTS=30 + readonly MAX_LIST_PAGES=100 + readonly EXPECTED_CPU=2 + readonly EXPECTED_MEMORY=4 + readonly EXPECTED_DISK=10 + readonly EXPECTED_REGION=us + readonly TIMEOUT_REASON_REGEX='^Job [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12} timed out after 30000ms$' + readonly UUID_REGEX='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' + readonly ISO_UTC_REGEX='^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?Z$' + + snapshot_by_name() { + local accumulated='[]' + local accumulated_length + local page + local page_length + local snapshot_list + + for ((page = 1; page <= MAX_LIST_PAGES; page += 1)); do + snapshot_list="$(daytona snapshot list --format json --limit 200 --page "$page")" || return 1 + jq --exit-status 'type == "array"' <<< "$snapshot_list" > /dev/null || return 1 + accumulated="$(jq --compact-output \ + --arg name "$SNAPSHOT_NAME" \ + --argjson accumulated "$accumulated" \ + '$accumulated + [.[] | select(.name == $name)]' \ + <<< "$snapshot_list")" || return 1 + page_length="$(jq --exit-status --raw-output 'length' <<< "$snapshot_list")" || return 1 + if [ "$page_length" -lt 200 ]; then + accumulated_length="$(jq --exit-status --raw-output 'length' <<< "$accumulated")" || return 1 + if [ "$accumulated_length" -gt 1 ]; then + echo "Multiple snapshots share immutable name $SNAPSHOT_NAME." >&2 + return 1 + fi + jq --compact-output '.[0] // null' <<< "$accumulated" || return 1 + return 0 + fi + done + + echo "Snapshot pagination exceeded $MAX_LIST_PAGES pages." >&2 + return 1 + } + + is_attempt_snapshot() { + local snapshot_json="$1" + jq --exit-status \ + --arg name "$SNAPSHOT_NAME" \ + --arg created_after "$attempt_started_at" \ + --arg ref_suffix "$expected_ref_suffix" \ + --arg region "$EXPECTED_REGION" \ + --arg uuid_regex "$UUID_REGEX" \ + --arg iso_utc_regex "$ISO_UTC_REGEX" \ + --argjson cpu "$EXPECTED_CPU" \ + --argjson memory "$EXPECTED_MEMORY" \ + --argjson disk "$EXPECTED_DISK" \ + 'try ( + type == "object" and + has("id") and ((.id | type) == "string") and (.id | test($uuid_regex)) and + has("name") and ((.name | type) == "string") and .name == $name and + has("createdAt") and ((.createdAt | type) == "string") and + (.createdAt | test($iso_utc_regex)) and + .createdAt >= $created_after and + has("ref") and ((.ref | type) == "string") and (.ref | endswith($ref_suffix)) and + has("cpu") and .cpu == $cpu and + has("mem") and .mem == $memory and + has("disk") and .disk == $disk and + has("regionIds") and ((.regionIds | type) == "array") and + .regionIds == [$region] and + has("state") and ((.state | type) == "string") + ) catch false' \ + <<< "$snapshot_json" > /dev/null + } + + is_retryable_timeout() { + local snapshot_json="$1" + jq --exit-status \ + --arg reason "$TIMEOUT_REASON_REGEX" \ + 'try ( + type == "object" and + has("state") and .state == "error" and + has("lastUsedAt") and .lastUsedAt == null and + has("errorReason") and ((.errorReason | type) == "string") and + (.errorReason | test($reason)) + ) catch false' \ + <<< "$snapshot_json" > /dev/null + } + + for ((attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt += 1)); do + existing="$(snapshot_by_name)" || { + echo "Could not verify candidate-name availability." >&2 + exit 1 + } + if [ "$existing" != "null" ]; then + echo "Snapshot $SNAPSHOT_NAME already exists; immutable names are never overwritten." >&2 + exit 1 fi - if [ "$(jq 'length' <<< "$snapshot_list")" -lt 200 ]; then - echo "Published snapshot $SNAPSHOT_NAME is not active." >&2 + + push_status=0 + push_log="$RUNNER_TEMP/daytona-push-$attempt.log" + attempt_started_at="$(date --utc '+%Y-%m-%dT%H:%M:%S.000Z')" + daytona snapshot push "$IMAGE_TAG" \ + --name "$SNAPSHOT_NAME" \ + --cpu "$EXPECTED_CPU" \ + --memory "$EXPECTED_MEMORY" \ + --disk "$EXPECTED_DISK" \ + --region "$EXPECTED_REGION" \ + > "$push_log" 2>&1 || push_status=$? + cat "$push_log" + + registry_digest="$(sed -nE \ + 's/.*digest: (sha256:[0-9a-f]{64}) size: [0-9]+.*/\1/p' \ + "$push_log" | tail -n 1)" + if [[ ! "$registry_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "Daytona push did not expose a verifiable registry digest." >&2 + exit 1 + fi + expected_ref_suffix="/daytona-${registry_digest#sha256:}:daytona" + + retry_candidate='' + for ((poll = 1; poll <= STATE_POLL_ATTEMPTS; poll += 1)); do + snapshot_json="$(snapshot_by_name)" || { + echo "Could not inspect the published snapshot candidate." >&2 + exit 1 + } + if [ "$snapshot_json" = "null" ]; then + state="absent" + else + state="$(jq --exit-status --raw-output \ + 'if type == "object" and has("state") and ((.state | type) == "string") + then .state else error("invalid snapshot state") end' \ + <<< "$snapshot_json")" || { + echo "Published snapshot returned an invalid state payload." >&2 + exit 1 + } + fi + + case "$state" in + active) + if ! is_attempt_snapshot "$snapshot_json"; then + echo "Active snapshot provenance does not match this publish attempt." >&2 + exit 1 + fi + echo "Published snapshot $SNAPSHOT_NAME is active." + echo "SNAPSHOT_RETRIES=$((attempt - 1))" >> "$GITHUB_ENV" + exit 0 + ;; + absent|building|pending|pulling) + ;; + error) + if ! is_attempt_snapshot "$snapshot_json"; then + echo "Failed snapshot provenance does not match this publish attempt." >&2 + exit 1 + fi + if is_retryable_timeout "$snapshot_json"; then + retry_candidate="$snapshot_json" + break + fi + echo "Snapshot publication entered a non-retryable error state." >&2 + jq '{id, name, state, errorReason, lastUsedAt}' <<< "$snapshot_json" >&2 + exit 1 + ;; + *) + echo "Snapshot publication entered unexpected state: $state" >&2 + exit 1 + ;; + esac + + if [ "$poll" -lt "$STATE_POLL_ATTEMPTS" ]; then + sleep 5 + fi + done + + if [ -z "$retry_candidate" ]; then + echo "Snapshot $SNAPSHOT_NAME did not become active; push exit status was $push_status." >&2 + exit 1 + fi + if [ "$attempt" -eq "$MAX_PUSH_ATTEMPTS" ]; then + echo "Snapshot processing timed out without a safe retry remaining." >&2 exit 1 fi - page=$((page + 1)) + + retry_id="$(jq --exit-status --raw-output '.id' <<< "$retry_candidate")" || exit 1 + confirmed="$(snapshot_by_name)" || { + echo "Could not revalidate the retry candidate." >&2 + exit 1 + } + confirmed_id="$(jq --exit-status --raw-output \ + 'if type == "object" and has("id") and ((.id | type) == "string") + then .id else error("invalid snapshot id") end' \ + <<< "$confirmed")" || exit 1 + if [ "$confirmed_id" != "$retry_id" ] || \ + ! is_attempt_snapshot "$confirmed" || \ + ! is_retryable_timeout "$confirmed"; then + echo "Retry candidate changed before cleanup; refusing deletion." >&2 + exit 1 + fi + + daytona snapshot delete "$retry_id" || exit 1 + removed=false + for ((poll = 1; poll <= REMOVE_POLL_ATTEMPTS; poll += 1)); do + current="$(snapshot_by_name)" || { + echo "Could not verify retry-candidate cleanup." >&2 + exit 1 + } + if [ "$current" = "null" ]; then + removed=true + break + fi + current_id="$(jq --exit-status --raw-output \ + 'if type == "object" and has("id") and ((.id | type) == "string") + then .id else error("invalid snapshot id") end' \ + <<< "$current")" || exit 1 + if [ "$current_id" != "$retry_id" ]; then + echo "Snapshot identity changed during retry cleanup." >&2 + exit 1 + fi + if [ "$poll" -lt "$REMOVE_POLL_ATTEMPTS" ]; then + sleep 2 + fi + done + if [ "$removed" != "true" ]; then + echo "Timed-out snapshot candidate was not removed within the cleanup window." >&2 + exit 1 + fi + + echo "Retrying transient Daytona processing timeout ($attempt/$MAX_PUSH_ATTEMPTS); push exited $push_status." done - name: Record promotion instructions env: @@ -174,6 +371,7 @@ jobs: echo "### Daytona snapshot candidate" echo "- Candidate: \`$SNAPSHOT_NAME\`" echo "- Source commit: \`$GITHUB_SHA\`" - echo "- No existing snapshot was deleted or replaced." + echo "- Transient provider retries: \`$SNAPSHOT_RETRIES\`" + echo "- Pre-existing, active, or previously used snapshots were not deleted or replaced." echo "- Promote through review by updating \`DAYTONA_SANDBOX_SNAPSHOT\` in \`apps/agent-worker/wrangler.jsonc\`." } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/db-migrate.yml b/.github/workflows/db-migrate.yml index 332588a8..6c28802d 100644 --- a/.github/workflows/db-migrate.yml +++ b/.github/workflows/db-migrate.yml @@ -41,11 +41,11 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm @@ -138,17 +138,17 @@ jobs: outputs: deployment_url: ${{ steps.deploy.outputs.url }} steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - name: Verify release commit env: RELEASE_SHA: ${{ github.sha }} run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA" - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm @@ -252,17 +252,17 @@ jobs: timeout-minutes: 30 environment: production steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - name: Verify release commit env: RELEASE_SHA: ${{ github.sha }} run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA" - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm @@ -299,17 +299,17 @@ jobs: timeout-minutes: 30 environment: production steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - name: Verify release commit env: RELEASE_SHA: ${{ github.sha }} run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA" - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm @@ -400,17 +400,17 @@ jobs: timeout-minutes: 30 environment: production steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - name: Verify release commit env: RELEASE_SHA: ${{ github.sha }} run: test "$(git rev-parse HEAD)" = "$RELEASE_SHA" - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index 98d14c9f..3c9c469d 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -36,7 +36,7 @@ jobs: echo "Backend release confirmation must exactly match RELEASE_PRODUCTION." >&2 exit 1 fi - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} - name: Verify release commit @@ -59,10 +59,10 @@ jobs: echo "Static Checks has not succeeded for $GITHUB_SHA on main." >&2 exit 1 fi - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 7e6bbd7e..0094d091 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -17,13 +17,13 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 with: version: 10.34.5 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: 22 cache: pnpm diff --git a/README.md b/README.md index 70b5e59f..b421ff0a 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Publish a new immutable Daytona snapshot after changing `infra/containers/sandbo ```bash docker build --platform=linux/amd64 -t cheatcode-sandbox: infra/containers/sandbox -daytona snapshot push cheatcode-sandbox: --name --cpu 2 --memory 4 --disk 10 +daytona snapshot push cheatcode-sandbox: --name --cpu 2 --memory 4 --disk 10 --region us ``` Cloudflare Secrets Store sync is dry-run by default, never prints secret values, creates missing diff --git a/apps/agent-worker/wrangler.jsonc b/apps/agent-worker/wrangler.jsonc index c55825c4..1a7c9439 100644 --- a/apps/agent-worker/wrangler.jsonc +++ b/apps/agent-worker/wrangler.jsonc @@ -12,7 +12,7 @@ "DAYTONA_API_URL": "https://app.daytona.io/api", "DAYTONA_PREVIEW_HOST_SUFFIXES": "daytonaproxy01.net,proxy.daytona.work", "DAYTONA_TARGET": "us", - "DAYTONA_SANDBOX_SNAPSHOT": "cheatcode-sandbox-viewer-bundle-20260714-1220z", + "DAYTONA_SANDBOX_SNAPSHOT": "cheatcode-sandbox-viewer-bundle-3336016022d8", "OUTPUT_DOWNLOAD_BASE_URL": "https://gateway.trycheatcode.com", "PREVIEW_HOSTNAME": "trycheatcode.com", "R2_OUTPUTS_BUCKET_NAME": "cheatcode-outputs" diff --git a/infra/containers/sandbox/README.md b/infra/containers/sandbox/README.md index 2df958b0..b2ad0b81 100644 --- a/infra/containers/sandbox/README.md +++ b/infra/containers/sandbox/README.md @@ -13,8 +13,13 @@ runtime user matches `/workspace` + the baked Next/Expo templates under `/home/n The normal path is the protected **Build Sandbox Snapshot** GitHub workflow. Dispatch it from `main` and enter `BUILD_SNAPSHOT`. It publishes an immutable candidate named -`cheatcode-sandbox-viewer-bundle-<12-character-commit-sha>`. The workflow never -deletes or replaces an existing snapshot and refuses to reuse a candidate name. +`cheatcode-sandbox-viewer-bundle-<12-character-commit-sha>-` and +refuses any pre-existing or surviving candidate name. If Daytona returns its exact +transient 30-second processing timeout, the workflow may remove only the unused failed +candidate created by that same publish attempt before retrying the run-scoped name. +Candidate ID, creation time, OCI digest, region, resources, error shape, and +`lastUsedAt` are revalidated immediately before deletion. Active, previously used, +pre-existing, or ambiguous snapshots fail closed and are never deleted or replaced. Promotion is a separate reviewed source change: update the agent-worker `DAYTONA_SANDBOX_SNAPSHOT` var to the candidate name, then use the protected database @@ -33,12 +38,13 @@ docker build --platform=linux/amd64 -t cheatcode-sandbox: \ # Push the local image straight into Daytona's registry (no external registry needed) # and register it as a snapshot with baked resources (≤ Tier-2 caps: 4 vCPU / 8 GiB / 10 GiB). daytona snapshot push cheatcode-sandbox: \ - --name cheatcode-sandbox-viewer-bundle- \ - --cpu 2 --memory 4 --disk 10 + --name cheatcode-sandbox-viewer-bundle-- \ + --cpu 2 --memory 4 --disk 10 --region us ``` Then set the agent-worker `DAYTONA_SANDBOX_SNAPSHOT` var to the new snapshot name. -The current default is `cheatcode-sandbox-viewer-bundle-20260714-1220z`. +The authoritative current default is committed in +[`apps/agent-worker/wrangler.jsonc`](../../../apps/agent-worker/wrangler.jsonc). > Use an **immutable tag**, not `:latest` (rejected) and not a digest (digest pinning is > currently broken for Daytona pushed-image references). The Dockerfile base image is