refactor: agents and skills move to a client-neutral source that renders per client

`.claude/agents/` was the source of truth, which made every role Claude-Code
shaped. Adding a second client meant rewriting each role in that client's syntax
and maintaining both copies — the drift this scaffold exists to prevent, one layer
up.

Roles and skills now live under `.agents/` and render into each registered
client. `.claude/agents/`, `.claude/skills/` and `.codex/agents/` are generated;
`scripts/sync-agent-integrations.py --check` fails on drift and belongs in CI.

The role metadata is portable rather than vendor-named: `reasoning_tier`
(deep/balanced/fast/vision), `capabilities`, `mutation`, `invocation`, and an
optional `preload_skills`. A client manifest maps those to native syntax and must
declare what it cannot express — `codex.yaml` declares `tier_policy: unsupported`
and its adapters say so in the file, rather than the tier silently evaporating and
leaving the repository to believe it was enforced.

The port is behaviour-preserving where it should be and a fix where it should not.
Every instruction body is byte-identical — the whole diff to `.claude/agents/` is
18 added lines and zero deletions. What changed is frontmatter that was missing:

- four agents (`code-reviewer`, `tdd-guardian`, `dependency-audit`, `pr-creator`)
  declared no `tools:` and therefore inherited the ENTIRE tool pool, so three
  review-only agents could edit and write the code they were reviewing. All eight
  now declare capabilities explicitly.
- the six read-only roles gain a non-editing permission mode, so the constraint is
  enforced by the client rather than by the prompt asking nicely.
- `mutation` is now explicit, which records the two roles that genuinely need to
  write: `pr-creator` (external-write — it pushes a branch and opens a PR) and
  `dependency-audit` (workspace-write — package managers rewrite lockfiles).

`pr-creator` keeps `shell` because opening a PR needs it, but it is now the only
agent here with a write mutation and a declared reason for it, instead of one of
four with unlimited access by omission.
This commit is contained in:
2026-09-19 17:27:26 -04:00
parent e111e61832
commit 3f0786f1e7
53 changed files with 13801 additions and 2 deletions
+423
View File
@@ -0,0 +1,423 @@
---
name: aws-services
description: AWS service patterns, IAM best practices, and common architectures. Use when designing or implementing AWS infrastructure.
---
# AWS Services Skill
## Common Architecture Patterns
### Web Application (ECS + RDS)
```
┌─────────────────────────────────────────────────────────────┐
│ VPC │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Public Subnets ││
│ │ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ ALB │ │ NAT GW │ ││
│ │ └──────┬──────┘ └──────┬──────┘ ││
│ └─────────┼───────────────────────────────────┼───────────┘│
│ │ │ │
│ ┌─────────┼───────────────────────────────────┼───────────┐│
│ │ │ Private Subnets │ ││
│ │ ┌──────▼──────┐ ┌───────▼─────┐ ││
│ │ │ ECS Fargate │ │ RDS │ ││
│ │ │ (Tasks) │───────────────────▶│ PostgreSQL │ ││
│ │ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```
### Serverless (Lambda + API Gateway)
```
┌────────────┐ ┌─────────────┐ ┌─────────────┐
│ Route53 │────▶│ API Gateway │────▶│ Lambda │
└────────────┘ └─────────────┘ └──────┬──────┘
┌──────────────────────────┼──────────────┐
│ │ │
┌──────▼─────┐ ┌─────────┐ ┌────▼────┐
│ DynamoDB │ │ S3 │ │ Secrets │
└────────────┘ └─────────┘ │ Manager │
└─────────┘
```
## IAM Best Practices
### Least Privilege Policy
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3ReadSpecificBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-app-data-bucket",
"arn:aws:s3:::my-app-data-bucket/*"
]
},
{
"Sid": "AllowSecretsAccess",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:eu-west-2:123456789:secret:my-app/*"
]
}
]
}
```
### Trust Policy (for ECS Tasks)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
```
### Cross-Account Access
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_ID:role/CrossAccountRole"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id"
}
}
}
]
}
```
## Secrets Management
### Using Secrets Manager
```python
# Python - boto3
import boto3
import json
def get_secret(secret_name: str, region: str = "eu-west-2") -> dict:
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# Usage
db_creds = get_secret("myapp/prod/database")
connection_string = f"postgresql://{db_creds['username']}:{db_creds['password']}@{db_creds['host']}/{db_creds['database']}"
```
```typescript
// TypeScript - AWS SDK v3
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
async function getSecret(secretName: string): Promise<Record<string, string>> {
const client = new SecretsManagerClient({ region: "eu-west-2" });
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
if (!response.SecretString) {
throw new Error("Secret not found");
}
return JSON.parse(response.SecretString);
}
```
### ECS Task with Secrets
```json
// Task definition
{
"containerDefinitions": [
{
"name": "app",
"secrets": [
{
"name": "DATABASE_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:eu-west-2:123456789:secret:myapp/database:password::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:eu-west-2:123456789:secret:myapp/api-key"
}
]
}
]
}
```
## S3 Patterns
### Bucket Policy (Least Privilege)
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowECSTaskAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789:role/ecs-task-role"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
},
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
}
]
}
```
### Presigned URLs
```python
import boto3
from botocore.config import Config
def generate_presigned_url(bucket: str, key: str, expiration: int = 3600) -> str:
"""Generate a presigned URL for S3 object access."""
s3_client = boto3.client(
"s3",
config=Config(signature_version="s3v4"),
region_name="eu-west-2"
)
return s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": key},
ExpiresIn=expiration
)
```
## DynamoDB Patterns
### Single Table Design
```python
# Entity types in same table
ENTITY_TYPES = {
"USER": {"PK": "USER#", "SK": "PROFILE"},
"ORDER": {"PK": "USER#", "SK": "ORDER#"},
"PRODUCT": {"PK": "PRODUCT#", "SK": "DETAILS"},
}
# Access patterns
def get_user(user_id: str) -> dict:
return table.get_item(
Key={"PK": f"USER#{user_id}", "SK": "PROFILE"}
)["Item"]
def get_user_orders(user_id: str) -> list:
response = table.query(
KeyConditionExpression="PK = :pk AND begins_with(SK, :sk)",
ExpressionAttributeValues={
":pk": f"USER#{user_id}",
":sk": "ORDER#"
}
)
return response["Items"]
```
## Lambda Patterns
### Handler with Error Handling
```python
import json
import logging
from typing import Any
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event: dict, context: Any) -> dict:
"""Lambda handler with proper error handling."""
try:
logger.info("Processing event", extra={"event": event})
# Process request
body = json.loads(event.get("body", "{}"))
result = process_request(body)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(result)
}
except ValidationError as e:
logger.warning("Validation error", extra={"error": str(e)})
return {
"statusCode": 400,
"body": json.dumps({"error": str(e)})
}
except Exception as e:
logger.exception("Unexpected error")
return {
"statusCode": 500,
"body": json.dumps({"error": "Internal server error"})
}
```
### Cold Start Optimization
```python
# Initialize outside handler (runs once per container)
import boto3
# These persist across invocations
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("my-table")
secrets_client = boto3.client("secretsmanager")
# Cache secrets
_cached_secrets = {}
def get_cached_secret(name: str) -> dict:
if name not in _cached_secrets:
response = secrets_client.get_secret_value(SecretId=name)
_cached_secrets[name] = json.loads(response["SecretString"])
return _cached_secrets[name]
def handler(event, context):
# Use cached resources
secret = get_cached_secret("my-secret")
# ...
```
## CloudWatch Patterns
### Structured Logging
```python
import json
import logging
class JsonFormatter(logging.Formatter):
def format(self, record):
log_record = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name,
}
# Add extra fields
if hasattr(record, "extra"):
log_record.update(record.extra)
return json.dumps(log_record)
# Setup
logger = logging.getLogger()
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
# Usage
logger.info("User created", extra={"user_id": "123", "email": "[email protected]"})
```
### Custom Metrics
```python
import boto3
cloudwatch = boto3.client("cloudwatch")
def publish_metric(name: str, value: float, unit: str = "Count"):
cloudwatch.put_metric_data(
Namespace="MyApp",
MetricData=[
{
"MetricName": name,
"Value": value,
"Unit": unit,
"Dimensions": [
{"Name": "Environment", "Value": "prod"},
{"Name": "Service", "Value": "api"},
]
}
]
)
# Usage
publish_metric("OrdersProcessed", 1)
publish_metric("ProcessingTime", 150, "Milliseconds")
```
## CLI Commands
```bash
# IAM
aws iam get-role --role-name MyRole
aws iam list-attached-role-policies --role-name MyRole
aws sts get-caller-identity
# S3
aws s3 ls s3://my-bucket/
aws s3 cp file.txt s3://my-bucket/
aws s3 presign s3://my-bucket/file.txt --expires-in 3600
# Secrets Manager
aws secretsmanager get-secret-value --secret-id my-secret
aws secretsmanager list-secrets
# ECS
aws ecs list-clusters
aws ecs describe-services --cluster my-cluster --services my-service
aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment
# Lambda
aws lambda invoke --function-name my-function output.json
aws lambda list-functions
aws logs tail /aws/lambda/my-function --follow
# CloudWatch
aws logs filter-log-events --log-group-name /aws/lambda/my-function --filter-pattern "ERROR"
```
## Security Checklist
- [ ] All S3 buckets have versioning enabled
- [ ] All S3 buckets block public access (unless explicitly needed)
- [ ] Encryption at rest enabled for all data stores
- [ ] Encryption in transit (TLS) for all connections
- [ ] IAM roles use least privilege
- [ ] No long-term credentials (use IAM roles/instance profiles)
- [ ] Secrets in Secrets Manager (not env vars or code)
- [ ] VPC endpoints for AWS services (avoid public internet)
- [ ] Security groups follow principle of least privilege
- [ ] CloudTrail enabled for auditing
- [ ] GuardDuty enabled for threat detection