Launch With AWS
AWSOfficialPublished by AWS
- Skill
- Workflow
- Cloud and DevOps
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Launch with AWS provides a structured, automated workflow to migrate full-stack web applications and frontend projects to AWS. It analyzes the source code, generates a migration plan with cost estimates, and produces deployable AWS Blocks infrastructure code. The process includes secure authentication, source code analysis, and a guided execution phase that results in a migration snapshot ready for local application and deployment via generated instructions.
What it does
- Analyzes web application source code to determine migration requirements.
- Generates detailed migration plans and AWS cost estimates.
- Produces AWS Blocks infrastructure code for target applications.
- Handles secure authentication and session management for AWS migration services.
- Provides a guided 3-way merge process to integrate migrated code into existing repositories.
- Generates project-specific deployment instructions via DEPLOY.md.
When to use it
- When you need to move a vibe-coded or frontend web application to AWS.
- When you want to transition from platforms like Lovable, Bolt.new, or Replit to AWS-native infrastructure.
- When you require a structured, step-by-step migration path for static sites, SPAs, or SSR frameworks.
- When you need to estimate the AWS infrastructure costs for hosting your existing web application.
Advantages
- Automates the complex process of infrastructure definition and migration planning.
- Provides clear cost visibility before committing to a migration.
- Maintains security by using encrypted data handling and avoiding AI training on user code.
- Supports a wide range of modern web frameworks and platforms.
- Ensures a clean integration process through git-based migration branches.
Usage examples
- “Migrate my Replit web app to AWS.”
- The agent initiates the authentication flow, analyzes the Replit project structure, and prepares a migration plan for AWS Lambda and Aurora Serverless.
- “How much would it cost to host my Next.js site on AWS?”
- The agent analyzes the project, calculates the estimated monthly AWS costs, and presents a breakdown of the required services.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/aws/launch-with-aws/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip launch-with-aws.zip -d .agents/skills
Documentation by AWS
Launch with AWS
Drives an AWS migration end-to-end using CLI scripts. Takes a user's web application, analyzes it, generates a migration plan with cost estimate, and delivers deployable AWS Blocks infrastructure code.
The AWS MCP server is recommended but is not required. This skill works standalone via its CLI scripts in any agent environment.
Script Invocation
All commands are run via:
python3 scripts/launch_with_aws.py <command> [args...]where scripts/ is relative to this skill directory. The agent MUST set the working directory to the skill root before invoking commands.
Required files: launch_with_aws.py, launch_config.py, auth.py, auth_callback_server.py, launch_api_client.py, archive.py, service model. When loaded via MCP, fetch all and write to a temp directory preserving structure before invoking.
Each command outputs JSON to stdout on success, or exits non-zero with a JSON error on stderr.
Two global flags may be added to any command: --region <region> selects the region the service runs the migration in, and --aws-mcp-url <url> passes the URL of the AWS MCP server the agent is connected to. See step 2.
Dependencies: Python 3.10+ and boto3. The script checks both on startup and exits with a clear error if either is missing.
Supported Application Types
Full-stack apps built with vibe-coding platforms, and frontend web applications and websites (static sites, SPAs, and SSR frameworks with static export).
| Origin Platform | What it covers |
|---|---|
| Lovable | Lovable-generated full-stack apps (React + Supabase) |
| Bolt.new | Bolt.new-generated full-stack apps (React + Supabase) |
| Replit | Replit-hosted full-stack apps (React + Express.js + PostgreSQL) |
| Framework | Examples |
|---|---|
| React ecosystem | React, CRA, Vite + React, Gatsby, Docusaurus |
| Vue ecosystem | Vue, Nuxt (static export), VitePress |
| Angular | Angular |
| Svelte ecosystem | Svelte, SvelteKit (static export) |
| SSR with static export | Next.js, Nuxt, Astro, SvelteKit |
| Other modern frameworks | Astro, Solid, Preact, Lit, Eleventy |
| Vite (generic) | Any Vite-based app |
Other frameworks may also work. If the user's app doesn't match these, see Unsupported Application Handling below.
What Gets Migrated vs. What Stays
Lovable / Bolt.new apps (Supabase-backed):
| Component | What happens |
|---|---|
| Frontend & hosting | Migrated to AWS (S3 + CloudFront + Lambda) |
| Edge functions / server functions | Migrated to AWS Lambda |
| AI calls (e.g. Lovable AI Gateway) | Migrated to Amazon Bedrock |
| Database (Supabase DB) | Stays on Supabase — not migrated |
| Auth (Supabase Auth) | Stays on Supabase — not migrated |
| Storage & Realtime | Stays on Supabase — not migrated |
The app continues to call Supabase for database, auth, storage, and realtime from the AWS-hosted application.
Replit apps (Express.js + PostgreSQL):
| Component | What happens |
|---|---|
| Frontend & hosting | Migrated to AWS (S3 + CloudFront + Lambda) |
| Server logic (Express.js) | Migrated to AWS Lambda (API Gateway) |
| Database (PostgreSQL) | Schema and code migrated to AWS (Aurora Serverless / DynamoDB). Existing data is NOT migrated — customers must export and import their data separately. |
| Auth (Replit Auth) | Code migrated to AWS (Cognito). Existing user accounts are NOT migrated — customers must re-create or invite users in Cognito. |
| Realtime (WebSockets) | Migrated to AWS (API Gateway WebSocket) |
| File storage | Migrated to AWS (S3). Existing files are NOT migrated. |
Replit app infrastructure and code are migrated to AWS-native services, but existing data, user accounts, and files must be migrated separately by the customer.
Input Resolution
Resolve the user's input to a local directory path:
If the user provides a local path: pass that path directly.
If the user provides a GitHub URL: clone the repository locally and use that directory.
If neither is provided: use the current working directory. If it doesn't look like an app directory, ask the user for the path.
Flow
Run the script commands in order, surfacing results to the user at each step:
1. Authentication
python3 scripts/launch_with_aws.py auth-startAlways run first. Returns immediately with JSON:
If already authenticated:
{"authenticated": true, "reusedCachedSession": true, "baseUrl": "..."}If silent refresh succeeded:
{"authenticated": true, "reusedCachedSession": false, "baseUrl": "..."}If interactive sign-in is needed:
{"authenticated": false, "signInUrl": "https://...", "pid": 12345, "port": 54321, "baseUrl": "..."}
When authenticated is false, immediately display the signInUrl to the user (so they can open it in their browser) and call auth-wait in the same response:
python3 scripts/launch_with_aws.py auth-wait <pid>where <pid> is the pid value from the auth-start response. This blocks until the user completes browser sign-in (or times out after 600s). Returns {"authenticated": true, "baseUrl": "..."} on success.
Sessions are capped at 90 days even if the identity provider does not set an expiration; after that the interactive flow is required again.
To check the current session without authenticating, or to sign out:
python3 scripts/launch_with_aws.py session-status
python3 scripts/launch_with_aws.py sign-outsession-status reports whether a session exists and how long until the token and overall session expire. sign-out best-effort revokes the refresh token and deletes the local ~/.launch-with-aws/session.json. On shared or untrusted workstations, run sign-out when finished.
2. Create Launch
For a local directory, present this data-handling confirmation verbatim and wait for explicit approval:
Your source code will be uploaded to the Launch with AWS service in [regionName], [region] ([sourceLabel]) to analyze your application and generate a migration plan. If you later approve execution, an AWS-hosted agent will modify a copy of your source code according to the plan and produce a migrated snapshot for you to download. Your uploaded source code and associated launch data are encrypted in transit and at rest and retained for up to 48 hours for recovery. Your data is never used to train AI models. We exclude Git history, Git-ignored files, and files matching common sensitive-file patterns. Sensitive-file filtering is best effort; review your project for secrets. Continue? Or reply with [otherRegions regionName] to run the migration there instead.
Fill in the region fields from resolve-region, verbatim, and offer each otherRegions entry as an alternative. If the agent is connected to an AWS MCP server, pass that server's URL verbatim. The script reads the region from it and decides whether it counts as a signal.
python3 scripts/launch_with_aws.py resolve-region [--aws-mcp-url https://aws-mcp.<region>.api.aws/mcp]Returns {"region": ..., "regionName": ..., "source": ..., "sourceLabel": ..., "baseUrl": ..., "otherRegions": [{"region": ..., "regionName": ...}]}.
Do NOT call create-launch until the user explicitly confirms; a missing or ambiguous response means no. Naming one of otherRegions is both a region choice and approval — pass its region as --region and do not ask again.
python3 scripts/launch_with_aws.py create-launch <app-directory> [name] --region <region>Zips the directory, uploads it, then creates the launch. Returns the full launch object including launch.launchId and the region it was created in, which becomes the user's saved default. A launch exists only in its own region — pass its --region on every later command.
The launch starts in analyzing status and automatically progresses through analysis and planning.
3. Poll Launch Status
python3 scripts/launch_with_aws.py get-launch-status <launch-id> --region <region>Poll until status is planned (ready for execution) or failed; report any other status to the user. Key status progression:
analyzing→ detecting app type and dependenciesplanning→ generating migration planplanned→ ready for executionexecuting→ deployment in progresscompleted→ donefailed→ checkfailureReason
4. Get Full Launch Details & Confirm
python3 scripts/launch_with_aws.py get-launch <launch-id> plan,cost_estimate --region <region>Get full launch details. Optional second argument is a comma-separated include list: analysis, plan, execution, cost_estimate, download_url.
Present the cost estimate and plan to the user. The costEstimate field in the response contains estimatedMonthlyCost, region, and a services breakdown with per-service costs.
Confirmation Gate — present and wait for explicit approval:
Migration Summary
App type: [detected type from analysis]
Architecture: [target architecture from plan]
Estimated monthly cost: $X.XX/month ([costEstimate.region] pricing)
Migration runs in: [regionName], [region]
Ready to proceed? This will execute the migration in an AWS-managed environment (no cost to you) and produce the migrated snapshot for you to download.
Do NOT call start-launch-execution until the user explicitly confirms.
5. Start Execution
python3 scripts/launch_with_aws.py start-launch-execution <launch-id> --region <region>Starts deployment. Then poll with get-launch-status until status is completed or failed. Sleep at least 30 seconds between polls.
6. Download
python3 scripts/launch_with_aws.py get-launch-download-url <launch-id> --region <region>Always present the full download URL to the user — they may need it to download the migrated snapshot directly or for reference.
7. List or Delete Launches
python3 scripts/launch_with_aws.py list-launches
python3 scripts/launch_with_aws.py delete-launch <launch-id> --region <region>list-launches queries both regions and returns items with a region on each launch; add --region to query one. A region that cannot be reached is reported under errors instead of failing the command, unless every queried region fails, in which case the command exits with that error.
8. Post-Migration: Apply Migrated Code Locally
After obtaining the download URL (adapt commands for the user's platform if not POSIX):
Step A: Download and unpack
curl -L -o /tmp/migration-snapshot.zip "<download_url>"
mkdir -p /tmp/migration-output
unzip -o /tmp/migration-snapshot.zip -d /tmp/migration-outputStep B: Prepare the local workspace
Ensure the user's working directory is clean:
cd <user-app-directory>
git statusIf there are uncommitted changes, ask the user to commit or stash first. Do NOT proceed with a dirty working tree.
Step C: Apply migration (3-way merge)
Create a migration branch and overlay the migrated files:
cd <user-app-directory>
git checkout -b aws-migration
rsync -a /tmp/migration-output/ .
git status
git diff --statReview the changes with the user. Key additions to highlight:
aws-blocks/— AWS Blocks infrastructure definitionDEPLOY.md— deployment instructionsAny modified config files
If there are conflicts with the user's existing files, present them and ask how to resolve.
Step D: Follow DEPLOY.md
Read the DEPLOY.md file in the project root and follow its instructions to deploy the app to the user's AWS account. Typical steps:
AWS authentication (
aws login --profile aws-migrate --region us-east-1)CDK bootstrap (first-time only):
npm install && npx cdk bootstrapDeploy:
npx cdk deploy --all --progress eventsVerify the CloudFront URL that CDK prints on completion.
Important: Always read DEPLOY.md from the migrated output — it is generated specifically for this app and architecture. Do not assume deployment steps from memory.
Unsupported Application Handling
If a launch fails during analysis with a failureReason indicating an unsupported app type (or the user's stack doesn't match the supported list):
Tell the user: "This app type isn't directly supported by Launch with AWS yet. Let me search for other skills that can help deploy this kind of application."
Search for relevant skills based on the app type (e.g.
aws-serverless,aws-containers,aws-database,aws-deployment,aws-cdk,aws-ai-ml).
Files
- ATTRIBUTION.mdAdded by AutoGineer895 B
- LICENSELicence file added by AutoGineer9.9 KiB
- NOTICELicence file added by AutoGineer67 B
- SKILL.md13.4 KiB
- references/launchwithaws-2026-06-15.json34.9 KiB
- scripts/archive.pyScript16.2 KiB
- scripts/auth.pyScript16.1 KiB
- scripts/auth_callback_server.pyScript2.9 KiB
- scripts/launch_api_client.pyScript9.7 KiB
- scripts/launch_config.pyScript12.6 KiB
- scripts/launch_with_aws.pyScript12.7 KiB
scripts/archive.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Build the repo zip the backend expects."""
import io
import logging
import os
import re
import stat
import subprocess
import tempfile
import urllib.error
import urllib.request
import zipfile
from typing import Optional, Set, Tuple
from urllib.parse import urlparse
from launch_config import (
GITHUB_ZIPBALL_TIMEOUT_SECS,
MAX_ARCHIVE_BYTES,
MAX_ARCHIVE_ENTRIES,
MAX_COMPRESSION_RATIO,
MAX_ENTRY_UNCOMPRESSED_BYTES,
MAX_UNCOMPRESSED_BYTES,
)
logger = logging.getLogger(__name__)
# Chunk size for reading the GitHub download.
_DOWNLOAD_CHUNK_BYTES = 64 * 1024
# Hosts a GitHub zipball request may be redirected through: api.github.com
# redirects to codeload and then to its S3-backed object store.
_ALLOWED_REDIRECT_HOST_SUFFIXES = (
".github.com",
".githubusercontent.com",
".amazonaws.com",
)
_ALLOWED_REDIRECT_HOSTS = frozenset({"github.com"})
_SKIP_DIRS = frozenset(
{
".git",
".hg",
".svn",
"node_modules",
".next",
".nuxt",
".turbo",
".cache",
".parcel-cache",
"dist",
"build",
"out",
"coverage",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
".idea",
".vscode",
# Credential/secret directories
".aws",
".ssh",
".gnupg",
".gcp",
".azure",
".docker",
".kube",
}
)
_SKIP_FILES = frozenset(
{
".env",
".env.local",
".env.production",
".env.development",
".env.test",
".env.staging",
"credentials",
"credentials.json",
".git-credentials",
".netrc",
".pypirc",
".npmrc",
".htpasswd",
}
)
# Best-effort filtering of common credential files, not a security boundary.
# A filtered archive is not guaranteed to be secret-free.
_SKIP_SUFFIXES = (".pem", ".key", ".p12", ".pfx", ".jks", ".keystore")
_SKIP_NAME_PREFIXES = ("id_rsa", "id_ed25519", "id_ecdsa", "id_dsa")
_GITHUB_URL_RE = re.compile(
r"^https://github\.com/([\w.-]+)/([\w.-]+?)(?:\.git)?/?$",
)
class ArchiveError(Exception):
"""Raised when a repo archive cannot be produced."""
def _decode_git_paths(output: bytes) -> Set[str]:
"""Decode NUL-delimited Git paths using the filesystem encoding."""
return {os.fsdecode(path) for path in output.split(b"\0") if path}
def _run_git(root: str, args: list[str]) -> subprocess.CompletedProcess:
"""Run a Git file-enumeration command with consistent safeguards."""
return subprocess.run(
["git", *args],
cwd=root,
capture_output=True,
timeout=30,
)
def _has_gitignore(root: str) -> bool:
"""Return whether the source tree contains an applicable .gitignore file."""
for _, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
if ".gitignore" in filenames:
return True
return False
def _gitignore_files_without_repo(root: str) -> Set[str]:
"""Apply Git ignore rules to a directory that is not a Git worktree."""
with tempfile.TemporaryDirectory(prefix="launch-with-aws-git-") as git_dir:
initialized = _run_git(root, ["init", "--bare", "--quiet", git_dir])
if initialized.returncode != 0:
raise ArchiveError("Git could not initialize temporary metadata to apply ignore rules.")
result = _run_git(
root,
[
f"--git-dir={git_dir}",
f"--work-tree={root}",
"ls-files",
"--others",
"--exclude-standard",
"-z",
],
)
if result.returncode != 0:
raise ArchiveError(
"Git could not determine which files are safe to upload. "
"Resolve the Git error and try again."
)
return _decode_git_paths(result.stdout)
def _git_included_files(root: str) -> Optional[Set[str]]:
"""Return paths selected by Git ignore rules, or None when no rules apply.
NUL-delimited output preserves unusual filenames. Tracked files are combined
with untracked-but-not-ignored files so new local files are included.
"""
try:
result = _run_git(
root,
[
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"-z",
],
)
if result.returncode == 0:
ignored_tracked = _run_git(
root,
[
"ls-files",
"--cached",
"--ignored",
"--exclude-standard",
"-z",
],
)
if ignored_tracked.returncode != 0:
raise ArchiveError(
"Git could not determine which tracked files match ignore rules."
)
return _decode_git_paths(result.stdout) - _decode_git_paths(ignored_tracked.stdout)
if not _has_git_metadata(root):
if _has_gitignore(root):
return _gitignore_files_without_repo(root)
return None
raise ArchiveError(
"Git could not determine which files are safe to upload. "
"Resolve the Git error and try again."
)
except subprocess.TimeoutExpired as err:
raise ArchiveError(
"Git timed out while determining which files are safe to upload."
) from err
except OSError as err:
if _has_git_metadata(root) or _has_gitignore(root):
raise ArchiveError(
"Git is required to apply this source directory's ignore rules before upload."
) from err
return None
def _has_git_metadata(root: str) -> bool:
"""Return whether root is at or below a directory containing Git metadata."""
current = os.path.abspath(root)
while True:
if os.path.exists(os.path.join(current, ".git")):
return True
parent = os.path.dirname(current)
if parent == current:
return False
current = parent
def parse_github_url(url: str) -> Optional[Tuple[str, str]]:
"""Return (owner, repo) for a GitHub HTTPS URL, or None.
Only https://github.com/<owner>/<repo> URLs are accepted, and `.` / `..`
segments are rejected.
"""
match = _GITHUB_URL_RE.match(url.strip())
if not match:
return None
owner, repo = match.group(1), match.group(2)
if any(segment in (".", "..") for segment in (owner, repo)):
return None
return owner, repo
def sanitize_root_name(name: str) -> str:
segments = [s for s in re.split(r"[\\/]", name.strip()) if s]
base = (segments[-1] if segments else "").strip().lstrip(".")
cleaned = re.sub(r"[^\w.-]", "-", base).strip("-")
return cleaned or "app"
def _is_secret_file(filename: str) -> bool:
"""Return True if the filename matches a known secret pattern."""
lower = filename.lower()
if lower in _SKIP_FILES:
return True
if lower.startswith(".env."):
return True
if lower.endswith(_SKIP_SUFFIXES):
return True
if any(lower.startswith(p) for p in _SKIP_NAME_PREFIXES):
return True
return False
def _is_safe_entry_name(name: str) -> bool:
"""Return True if a ZIP entry name is a plain relative path.
Rejects absolute paths, Windows drive/UNC prefixes, `..` segments, and
embedded NUL bytes.
"""
if not name or "\x00" in name:
return False
if name.startswith("/") or name.startswith("\\"):
return False
# Windows drive letter (e.g. "C:\") or UNC ("\\host").
if re.match(r"^[A-Za-z]:", name) or name.startswith("\\\\"):
return False
normalized = name.replace("\\", "/")
return not any(segment == ".." for segment in normalized.split("/"))
def _is_symlink_entry(info: zipfile.ZipInfo) -> bool:
"""Return True if a ZIP entry encodes a Unix symlink."""
# The high 16 bits of external_attr hold the Unix mode for Unix-created
# entries.
mode = (info.external_attr >> 16) & 0xFFFF
return stat.S_ISLNK(mode)
def validate_archive_bytes(data: bytes) -> None:
"""Validate ZIP entry names, shape, and size limits.
All checks read the central-directory metadata only (nothing is
decompressed). Raises ArchiveError on the first violation.
"""
try:
zf = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile as err:
raise ArchiveError("Downloaded file is not a valid zip archive.") from err
with zf:
infos = zf.infolist()
if len(infos) > MAX_ARCHIVE_ENTRIES:
raise ArchiveError(
f"Archive has {len(infos)} entries, exceeding the "
f"{MAX_ARCHIVE_ENTRIES} entry limit."
)
total_uncompressed = 0
roots: Set[str] = set()
for info in infos:
name = info.filename
if not _is_safe_entry_name(name):
raise ArchiveError(f"Archive contains an unsafe entry path: {name!r}")
if _is_symlink_entry(info):
raise ArchiveError(f"Archive contains a symlink entry: {name!r}")
size = info.file_size
if size > MAX_ENTRY_UNCOMPRESSED_BYTES:
raise ArchiveError(
f"Archive entry {name!r} is {size // (1024 * 1024)} MiB "
f"uncompressed, exceeding the "
f"{MAX_ENTRY_UNCOMPRESSED_BYTES // (1024 * 1024)} MiB per-entry limit."
)
if info.compress_size > 0 and size / info.compress_size > MAX_COMPRESSION_RATIO:
raise ArchiveError(
f"Archive entry {name!r} exceeds the {MAX_COMPRESSION_RATIO}:1 "
"compression-ratio limit."
)
total_uncompressed += size
if total_uncompressed > MAX_UNCOMPRESSED_BYTES:
raise ArchiveError(
f"Archive decompresses to over "
f"{MAX_UNCOMPRESSED_BYTES // (1024 * 1024 * 1024)} GiB, "
"exceeding the decompressed-size limit."
)
top = info.filename.replace("\\", "/").split("/", 1)[0]
if top:
roots.add(top)
if len(roots) > 1:
raise ArchiveError(
f"Archive must contain a single top-level directory; found {len(roots)}."
)
def zip_local_repo(path: str, root_name: str) -> bytes:
"""Zip a local repo directory, skipping VCS/build artifacts and secrets.
When the directory is a git repository, .gitignore rules are respected
via `git ls-files`. The hardcoded secret exclusions still apply on top
as a defense-in-depth measure.
"""
root = os.path.abspath(os.path.expanduser(path))
if not os.path.isdir(root):
raise ArchiveError(f"Not a directory: {path}")
prefix = sanitize_root_name(root_name)
skipped: list[str] = []
git_files = _git_included_files(root)
if git_files is not None:
logger.info("Using git to determine included files (.gitignore respected)")
buffer = io.BytesIO()
file_count = 0
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
for filename in filenames:
abs_path = os.path.join(dirpath, filename)
if os.path.islink(abs_path):
continue
rel = os.path.relpath(abs_path, root).replace(os.sep, "/")
if git_files is not None and rel not in git_files:
continue
if _is_secret_file(filename):
skipped.append(rel)
continue
zf.write(abs_path, f"{prefix}/{rel}")
file_count += 1
if skipped:
logger.info(
"Excluded %d file(s) matching secret patterns: %s",
len(skipped),
", ".join(skipped),
)
if file_count == 0:
raise ArchiveError(f"No files found under {path}")
data = buffer.getvalue()
if len(data) > MAX_ARCHIVE_BYTES:
raise ArchiveError(
f"Archive is {len(data) // (1024 * 1024)} MiB, exceeding the "
f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit. Remove large "
"files or build artifacts and try again."
)
validate_archive_bytes(data)
return data
def _is_allowed_redirect_host(hostname: Optional[str]) -> bool:
"""Return True if a redirect target host is on the allowlist."""
if not hostname:
return False
hostname = hostname.lower()
if hostname in _ALLOWED_REDIRECT_HOSTS:
return True
return any(hostname.endswith(suffix) for suffix in _ALLOWED_REDIRECT_HOST_SUFFIXES)
class _AllowlistRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Redirect handler that only follows redirects to allowlisted hosts.
GitHub zipball downloads 302 from api.github.com to codeload and then to an
S3 object store; redirects to any other host are refused.
"""
def redirect_request(self, req, fp, code, msg, headers, newurl):
parsed = urlparse(newurl)
if parsed.scheme != "https":
raise ArchiveError(f"Refusing redirect to non-HTTPS URL during download: {newurl!r}")
if not _is_allowed_redirect_host(parsed.hostname):
raise ArchiveError(
f"Refusing redirect to disallowed host during download: {parsed.hostname!r}"
)
return super().redirect_request(req, fp, code, msg, headers, newurl)
def _read_capped(resp) -> bytes:
"""Read a response body, aborting if it exceeds MAX_ARCHIVE_BYTES.
Pre-checks the declared Content-Length when present, then reads in chunks
against a running total so bodies without a Content-Length are also bounded.
"""
declared = resp.headers.get("Content-Length")
if declared is not None:
try:
if int(declared) > MAX_ARCHIVE_BYTES:
raise ArchiveError(
f"Downloaded archive is {int(declared) // (1024 * 1024)} MiB, "
f"exceeding the {MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit."
)
except ValueError:
pass
chunks: list[bytes] = []
total = 0
while True:
chunk = resp.read(_DOWNLOAD_CHUNK_BYTES)
if not chunk:
break
total += len(chunk)
if total > MAX_ARCHIVE_BYTES:
raise ArchiveError(
f"Downloaded archive exceeds the "
f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit."
)
chunks.append(chunk)
return b"".join(chunks)
def download_github_zip(url: str) -> bytes:
"""Download and validate a public GitHub repo's default-branch archive.
Follows only allowlisted redirects, caps the body at MAX_ARCHIVE_BYTES, and
validates the archive before returning the bytes.
"""
parsed = parse_github_url(url)
if not parsed:
raise ArchiveError(f"Not a GitHub repository URL: {url}")
owner, repo = parsed
zipball = f"https://api.github.com/repos/{owner}/{repo}/zipball"
req = urllib.request.Request(zipball, headers={"Accept": "application/vnd.github+json"})
opener = urllib.request.build_opener(_AllowlistRedirectHandler())
try:
with opener.open(req, timeout=GITHUB_ZIPBALL_TIMEOUT_SECS) as resp:
data = _read_capped(resp)
except ArchiveError:
raise
except urllib.error.HTTPError as err:
if err.code in (401, 403, 404):
raise ArchiveError(
f"Could not download {owner}/{repo}. Only public repositories can "
"be fetched by URL — for a private repo, clone it locally and pass "
"the local path instead."
) from err
raise ArchiveError(f"GitHub returned {err.code} downloading {owner}/{repo}.") from err
except (urllib.error.URLError, OSError) as err:
raise ArchiveError(f"Failed to download {url}: {err}") from err
validate_archive_bytes(data)
return data
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server, Needs a command-line tool, Needs Python, Needs Node.js
Security
What our scanner noticed· Medium risk
- Phrasing that asks for actions without confirmation
- References to credential files
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- AWS
- Publisher
- AWS (Official)
- Source
- aws/agent-toolkit-for-aws · skills/core-skills/launch-with-aws · commit 0a218f8
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- NOTICE (added by AutoGineer from NOTICE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- aws/agent-toolkit-for-aws
AutoGineer is not affiliated with or endorsed by AWS or AWS.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download launch-with-aws.zip