tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check working directory and tools
ls -la && echo "---" && which aws python3 zip jq && python3 --version
total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 15:18 .. --- /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq Python 3.12.3 [stdout] total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 28 15:18 .. --- /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq Python 3.12.3
Check localstack health
curl -s http://localstack:4566/_localstack/health | jq . | head -30
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "disabled",
"ses": "disabled",
"sns": "disabled",
[stdout]
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "disabled",
"ses": "disabled",
"sns": "disabled",Check AWS env and account
echo "AWS_ENDPOINT_URL=$AWS_ENDPOINT_URL"; echo "AWS_REGION=$AWS_REGION"; echo "AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION"; aws --endpoint-url http://localstack:4566 sts get-caller-identity --region us-east-1
AWS_ENDPOINT_URL=http://localstack:4566
AWS_REGION=
AWS_DEFAULT_REGION=us-east-1
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
[stdout]
AWS_ENDPOINT_URL=http://localstack:4566
AWS_REGION=
AWS_DEFAULT_REGION=us-east-1
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}Verify env-based endpoint works
aws sts get-caller-identity 2>&1 | head -5
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}Create working dirs
mkdir -p /app/lambdas/order_api /app/lambdas/outbox_relay /app/policies /app/build && ls /app
build lambdas policies [stdout] build lambdas policies
import json
import os
import time
import uuid
import boto3
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
REGION = os.environ.get("AWS_REGION", "us-east-1")
ORDERS_TABLE = os.environ["ORDERS_TABLE"]
OUTBOX_TABLE = os.environ["OUTBOX_TABLE"]
ALLOWED_KINDS = {"OrderCreated", "OrderCancelled"}
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name=REGION)
def _to_attr(value):
if isinstance(value, bool):
return {"BOOL": value}
if isinstance(value, (int, float)):
return {"N": str(value)}
if value is None:
return {"NULL": True}
if isinstance(value, list):
return {"L": [_to_attr(v) for v in value]}
if isinstance(value, dict):
return {"M": {k: _to_attr(v) for k, v in value.items()}}
return {"S": str(value)}
def handler(event, context):
body = event
if isinstance(event.get("body"), str):
body = json.loads(event["body"])
order_id = body.get("order_id")
kind = body.get("kind")
if not order_id or kind not in ALLOWED_KINDS:
return {"statusCode": 400, "body": json.dumps({"error": "order_id and kind=OrderCreated|OrderCancelled required"})}
event_id = str(uuid.uuid4())
now_ms = int(time.time() * 1000)
order_payload = {k: v for k, v in body.items() if k not in ("order_id", "kind")}
order_item = {
"order_id": {"S": order_id},
"last_kind": {"S": kind},
"updated_at": {"N": str(now_ms)},
}
for k, v in order_payload.items():
order_item[k] = _to_attr(v)
detail = {"order_id": order_id, "kind": kind, **order_payload}
outbox_item = {
"event_id": {"S": event_id},
"order_id": {"S": order_id},
"kind": {"S": kind},
"created_at": {"N": str(now_ms)},
"detail": {"S": json.dumps(detail)},
}
ddb.transact_write_items(
TransactItems=[
{"Put": {"TableName": ORDERS_TABLE, "Item": order_item}},
{"Put": {"TableName": OUTBOX_TABLE, "Item": outbox_item}},
]
)
return {
"statusCode": 200,
"body": json.dumps({"order_id": order_id, "kind": kind, "event_id": event_id}),
}
import json
import os
import time
import boto3
from botocore.exceptions import ClientError
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
REGION = os.environ.get("AWS_REGION", "us-east-1")
DEDUP_TABLE = os.environ["DEDUP_TABLE"]
EVENT_BUS = os.environ["EVENT_BUS"]
DEDUP_TTL_SECONDS = int(os.environ.get("DEDUP_TTL_SECONDS", "86400"))
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name=REGION)
events = boto3.client("events", endpoint_url=ENDPOINT, region_name=REGION)
def _extract(image, key):
v = image.get(key)
if not v:
return None
return v.get("S") or v.get("N")
def _claim(event_id):
try:
ddb.put_item(
TableName=DEDUP_TABLE,
Item={
"event_id": {"S": event_id},
"claimed_at": {"N": str(int(time.time()))},
"ttl": {"N": str(int(time.time()) + DEDUP_TTL_SECONDS)},
},
ConditionExpression="attribute_not_exists(event_id)",
)
return True
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
raise
def handler(event, context):
failures = []
for record in event.get("Records", []):
seq = record.get("dynamodb", {}).get("SequenceNumber")
try:
ev = record.get("eventName")
if ev not in ("INSERT", "MODIFY"):
continue
new_image = record.get("dynamodb", {}).get("NewImage") or {}
event_id = _extract(new_image, "event_id")
kind = _extract(new_image, "kind")
detail_str = _extract(new_image, "detail")
if not event_id or kind not in ("OrderCreated", "OrderCancelled") or not detail_str:
continue
if not _claim(event_id):
continue
events.put_events(
Entries=[
{
"EventBusName": EVENT_BUS,
"Source": "outbox.relay",
"DetailType": kind,
"Detail": detail_str,
}
]
)
except Exception as exc:
print(f"relay failed seq={seq}: {exc}")
if seq:
failures.append({"itemIdentifier": seq})
return {"batchItemFailures": failures}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
{
"Version": "2012-10-17",
"Id": "outbox-cmk-policy",
"Statement": [
{
"Sid": "EnableRootAdmin",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:root"},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "AllowEventBridgeUseOfKey",
"Effect": "Allow",
"Principal": {"Service": "events.amazonaws.com"},
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "AllowAwsServicesUseOfKey",
"Effect": "Allow",
"Principal": {"Service": ["dynamodb.amazonaws.com", "sqs.amazonaws.com"]},
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "*"
}
]
}
Create CMK and alias
KEY_OUT=$(aws kms create-key --description "outbox CMK" --key-usage ENCRYPT_DECRYPT --policy file:///app/policies/kms-key-policy.json) && echo "$KEY_OUT" | jq . && KEY_ID=$(echo "$KEY_OUT" | jq -r .KeyMetadata.KeyId) && KEY_ARN=$(echo "$KEY_OUT" | jq -r .KeyMetadata.Arn) && echo "$KEY_ID" > /app/build/key_id && echo "$KEY_ARN" > /app/build/key_arn && aws kms create-alias --alias-name alias/outbox-cmk --target-key-id "$KEY_ID" && aws kms list-aliases --query 'Aliases[?AliasName==`alias/outbox-cmk`]'
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "a844925e-7666-4a77-931f-1b25a390ebf9",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/a844925e-7666-4a77-931f-1b25a390ebf9",
"CreationDate": "2026-04-28T15:21:22.621356+00:00",
"Enabled": true,
"Description": "outbox CMK",
"KeyUsage": "ENCRYPT_DECRYPT",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER",
"CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
"KeySpec": "SYMMETRIC_DEFAULT",
"EncryptionAlgorithms": [
"SYMMETRIC_DEFAULT"
],
"MultiRegion": false,
"CurrentKeyMaterialId": "7df5537c3c3253a79d8419e8fe8a0f357df5537c3c3253a79d8419e8fe8a0f35"
}
}
[
{
"AliasName": "alias/outbox-cmk",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/outbox-cmk",
"TargetKeyId": "a844925e-7666-4a77-931f-1b25a390ebf9",
"CreationDate": "2026-04-28T15:21:23.082150+00:00"
}
]
[stdout]
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "a844925e-7666-4a77-931f-1b25a390ebf9",
"Arn": "arn:aws:kms:us-east-1:000000000000:key/a844925e-7666-4a77-931f-1b25a390ebf9",
"CreationDate": "2026-04-28T15:21:22.621356+00:00",
"Enabled": true,
"Description": "outbox CMK",
"KeyUsage": "ENCRYPT_DECRYPT",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER",
"CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
"KeySpec": "SYMMETRIC_DEFAULT",
"EncryptionAlgorithms": [
"SYMMETRIC_DEFAULT"
],
"MultiRegion": false,
"CurrentKeyMaterialId": "7df5537c3c3253a79d8419e8fe8a0f357df5537c3c3253a79d8419e8fe8a0f35"
}
}
[
{
"AliasName": "alias/outbox-cmk",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/outbox-cmk",
"TargetKeyId": "a844925e-7666-4a77-931f-1b25a390ebf9",
"CreationDate": "2026-04-28T15:21:23.082150+00:00"
}
]Create three DDB tables w/ CMK
KEY_ID=$(cat /app/build/key_id) && \ aws dynamodb create-table \ --table-name orders \ --attribute-definitions AttributeName=order_id,AttributeType=S \ --key-schema AttributeName=order_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ID" \ --query 'TableDescription.TableName' && \ aws dynamodb create-table \ --table-name outbox \ --attribute-definitions AttributeName=event_id,AttributeType=S \ --key-schema AttributeName=event_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification "StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES" \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ID" \ --query 'TableDescription.TableName' && \ aws dynamodb create-table \ --table-name processed_events \ --attribute-definitions AttributeName=event_id,AttributeType=S \ --key-schema AttributeName=event_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ID" \ --query 'TableDescription.TableName'
"orders" "outbox" "processed_events" [stdout] "orders" "outbox" "processed_events"
Enable TTL on dedup table; check outbox stream
aws dynamodb update-time-to-live --table-name processed_events --time-to-live-specification "Enabled=true,AttributeName=ttl" && aws dynamodb describe-table --table-name outbox --query 'Table.{Stream:StreamSpecification,SSE:SSEDescription,Status:TableStatus,LatestStreamArn:LatestStreamArn}'{
"TimeToLiveSpecification": {
"Enabled": true,
"AttributeName": "ttl"
}
}
{
"Stream": {
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"SSE": {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/a844925e-7666-4a77-931f-1b25a390ebf9"
},
"Status": "ACTIVE",
"LatestStreamArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T15:21:38.606"
}
[stdout]
{
"TimeToLiveSpecification": {
"Enabled": true,
"AttributeName": "ttl"
}
}
{
"Stream": {
"StreamEnabled": true,
"StreamViewType": "NEW_AND_OLD_IMAGES"
},
"SSE": {
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/a844925e-7666-4a77-931f-1b25a390ebf9"
},
"Status": "ACTIVE",
"LatestStreamArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T15:21:38.606"
}Save outbox stream ARN
aws dynamodb describe-table --table-name outbox --query 'Table.LatestStreamArn' --output text > /app/build/outbox_stream_arn && cat /app/build/outbox_stream_arn
arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T15:21:38.606 [stdout] arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T15:21:38.606
Create DLQs encrypted with CMK
KEY_ID=$(cat /app/build/key_id) && \
for q in orders-created-dlq orders-cancelled-dlq; do
aws sqs create-queue --queue-name "$q" --attributes "{\"KmsMasterKeyId\":\"$KEY_ID\"}" --query QueueUrl --output text
donehttp://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq [stdout] http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq
Get DLQ ARNs
CREATED_DLQ_ARN=$(aws sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created-dlq --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) && \ CANCELLED_DLQ_ARN=$(aws sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled-dlq --attribute-names QueueArn --query 'Attributes.QueueArn' --output text) && \ echo "$CREATED_DLQ_ARN" > /app/build/orders_created_dlq_arn && \ echo "$CANCELLED_DLQ_ARN" > /app/build/orders_cancelled_dlq_arn && \ echo "$CREATED_DLQ_ARN" && echo "$CANCELLED_DLQ_ARN"
arn:aws:sqs:us-east-1:000000000000:orders-created-dlq arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq [stdout] arn:aws:sqs:us-east-1:000000000000:orders-created-dlq arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq
Create main queues with redrive + CMK
KEY_ID=$(cat /app/build/key_id)
CREATED_DLQ_ARN=$(cat /app/build/orders_created_dlq_arn)
CANCELLED_DLQ_ARN=$(cat /app/build/orders_cancelled_dlq_arn)
CREATED_REDRIVE=$(jq -nc --arg arn "$CREATED_DLQ_ARN" '{deadLetterTargetArn:$arn,maxReceiveCount:"5"}')
CANCELLED_REDRIVE=$(jq -nc --arg arn "$CANCELLED_DLQ_ARN" '{deadLetterTargetArn:$arn,maxReceiveCount:"5"}')
CREATED_ATTRS=$(jq -nc --arg key "$KEY_ID" --arg rd "$CREATED_REDRIVE" '{KmsMasterKeyId:$key,RedrivePolicy:$rd}')
CANCELLED_ATTRS=$(jq -nc --arg key "$KEY_ID" --arg rd "$CANCELLED_REDRIVE" '{KmsMasterKeyId:$key,RedrivePolicy:$rd}')
aws sqs create-queue --queue-name orders-created --attributes "$CREATED_ATTRS" --query QueueUrl --output text
aws sqs create-queue --queue-name orders-cancelled --attributes "$CANCELLED_ATTRS" --query QueueUrl --output texthttp://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled [stdout] http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
Save queue ARNs and grant EventBridge SendMessage
CREATED_ARN=$(aws sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
CANCELLED_ARN=$(aws sqs get-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo "$CREATED_ARN" > /app/build/orders_created_arn
echo "$CANCELLED_ARN" > /app/build/orders_cancelled_arn
echo "$CREATED_ARN" && echo "$CANCELLED_ARN"
POLICY_CREATED=$(jq -nc --arg arn "$CREATED_ARN" '{Version:"2012-10-17",Statement:[{Sid:"AllowEventBridge",Effect:"Allow",Principal:{Service:"events.amazonaws.com"},Action:"sqs:SendMessage",Resource:$arn}]}')
POLICY_CANCELLED=$(jq -nc --arg arn "$CANCELLED_ARN" '{Version:"2012-10-17",Statement:[{Sid:"AllowEventBridge",Effect:"Allow",Principal:{Service:"events.amazonaws.com"},Action:"sqs:SendMessage",Resource:$arn}]}')
aws sqs set-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created --attributes "$(jq -nc --arg p "$POLICY_CREATED" '{Policy:$p}')"
aws sqs set-queue-attributes --queue-url http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled --attributes "$(jq -nc --arg p "$POLICY_CANCELLED" '{Policy:$p}')"arn:aws:sqs:us-east-1:000000000000:orders-created arn:aws:sqs:us-east-1:000000000000:orders-cancelled [stdout] arn:aws:sqs:us-east-1:000000000000:orders-created arn:aws:sqs:us-east-1:000000000000:orders-cancelled
Create custom event bus
aws events create-event-bus --name order-events --query EventBusArn --output text > /app/build/event_bus_arn cat /app/build/event_bus_arn
arn:aws:events:us-east-1:000000000000:event-bus/order-events [stdout] arn:aws:events:us-east-1:000000000000:event-bus/order-events
Create EB rules + targets
CREATED_PATTERN='{"source":["outbox.relay"],"detail-type":["OrderCreated"]}'
CANCELLED_PATTERN='{"source":["outbox.relay"],"detail-type":["OrderCancelled"]}'
aws events put-rule --name on-order-created --event-bus-name order-events --event-pattern "$CREATED_PATTERN" --state ENABLED --query RuleArn --output text
aws events put-rule --name on-order-cancelled --event-bus-name order-events --event-pattern "$CANCELLED_PATTERN" --state ENABLED --query RuleArn --output text
CREATED_ARN=$(cat /app/build/orders_created_arn)
CANCELLED_ARN=$(cat /app/build/orders_cancelled_arn)
aws events put-targets --rule on-order-created --event-bus-name order-events --targets "[{\"Id\":\"created-q\",\"Arn\":\"$CREATED_ARN\"}]"
aws events put-targets --rule on-order-cancelled --event-bus-name order-events --targets "[{\"Id\":\"cancelled-q\",\"Arn\":\"$CANCELLED_ARN\"}]"arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled
{
"FailedEntryCount": 0,
"FailedEntries": []
}
{
"FailedEntryCount": 0,
"FailedEntries": []
}
[stdout]
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-created
arn:aws:events:us-east-1:000000000000:rule/order-events/on-order-cancelled
{
"FailedEntryCount": 0,
"FailedEntries": []
}
{
"FailedEntryCount": 0,
"FailedEntries": []
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/order-api*"
},
{
"Sid": "WriteOrdersAndOutbox",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "AtomicWrite",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:000000000000:table/orders",
"arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
]
},
{
"Sid": "EncryptViaCmk",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "__KEY_ARN__",
"Condition": {
"StringEquals": {
"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
}
}
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/outbox-relay*"
},
{
"Sid": "ReadOutboxStream",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeStream",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:ListStreams",
"dynamodb:ListShards"
],
"Resource": "__OUTBOX_STREAM_ARN__"
},
{
"Sid": "DedupTableWrite",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
},
{
"Sid": "PutEventsOnBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/order-events"
},
{
"Sid": "DecryptViaCmk",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "__KEY_ARN__",
"Condition": {
"StringEquals": {
"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
}
}
}
]
}
Materialize and attach inline policies to roles
KEY_ARN=$(cat /app/build/key_arn) STREAM_ARN=$(cat /app/build/outbox_stream_arn) sed "s|__KEY_ARN__|$KEY_ARN|g" /app/policies/order-api-policy.json > /app/build/order-api-policy.json sed -e "s|__KEY_ARN__|$KEY_ARN|g" -e "s|__OUTBOX_STREAM_ARN__|$STREAM_ARN|g" /app/policies/outbox-relay-policy.json > /app/build/outbox-relay-policy.json aws iam create-role --role-name order-api-role --assume-role-policy-document file:///app/policies/lambda-trust.json --query Role.Arn --output text > /app/build/order_api_role_arn aws iam create-role --role-name outbox-relay-role --assume-role-policy-document file:///app/policies/lambda-trust.json --query Role.Arn --output text > /app/build/outbox_relay_role_arn aws iam put-role-policy --role-name order-api-role --policy-name order-api-policy --policy-document file:///app/build/order-api-policy.json aws iam put-role-policy --role-name outbox-relay-role --policy-name outbox-relay-policy --policy-document file:///app/build/outbox-relay-policy.json echo "Roles:"; cat /app/build/order_api_role_arn; echo; cat /app/build/outbox_relay_role_arn
Roles: arn:aws:iam::000000000000:role/order-api-role arn:aws:iam::000000000000:role/outbox-relay-role [stdout] Roles: arn:aws:iam::000000000000:role/order-api-role arn:aws:iam::000000000000:role/outbox-relay-role
Zip both lambdas
cd /app/lambdas/order_api && zip -q /app/build/order_api.zip handler.py cd /app/lambdas/outbox_relay && zip -q /app/build/outbox_relay.zip handler.py ls -la /app/build/*.zip
-rw-r--r-- 1 root root 993 Apr 28 15:23 /app/build/order_api.zip -rw-r--r-- 1 root root 1093 Apr 28 15:23 /app/build/outbox_relay.zip [stdout] -rw-r--r-- 1 root root 993 Apr 28 15:23 /app/build/order_api.zip -rw-r--r-- 1 root root 1093 Apr 28 15:23 /app/build/outbox_relay.zip
Deploy both lambdas
ORDER_API_ROLE=$(cat /app/build/order_api_role_arn)
RELAY_ROLE=$(cat /app/build/outbox_relay_role_arn)
aws lambda create-function \
--function-name order-api \
--runtime python3.11 \
--role "$ORDER_API_ROLE" \
--handler handler.handler \
--zip-file fileb:///app/build/order_api.zip \
--timeout 15 \
--environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566,ORDERS_TABLE=orders,OUTBOX_TABLE=outbox}" \
--query 'FunctionArn' --output text
aws lambda create-function \
--function-name outbox-relay \
--runtime python3.11 \
--role "$RELAY_ROLE" \
--handler handler.handler \
--zip-file fileb:///app/build/outbox_relay.zip \
--timeout 30 \
--environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566,DEDUP_TABLE=processed_events,EVENT_BUS=order-events,DEDUP_TTL_SECONDS=86400}" \
--query 'FunctionArn' --output textarn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay [stdout] arn:aws:lambda:us-east-1:000000000000:function:order-api arn:aws:lambda:us-east-1:000000000000:function:outbox-relay
Wait for lambdas active
aws lambda wait function-active --function-name order-api && aws lambda wait function-active --function-name outbox-relay && echo OK
OK [stdout] OK
Wire event source mapping
STREAM_ARN=$(cat /app/build/outbox_stream_arn)
aws lambda create-event-source-mapping \
--function-name outbox-relay \
--event-source-arn "$STREAM_ARN" \
--starting-position LATEST \
--batch-size 10 \
--maximum-batching-window-in-seconds 1 \
--function-response-types ReportBatchItemFailures \
--query '{UUID:UUID,State:State,FunctionResponseTypes:FunctionResponseTypes}'{
"UUID": "f15a615d-3375-4a10-8a0d-69b0a5029c46",
"State": "Creating",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}
[stdout]
{
"UUID": "f15a615d-3375-4a10-8a0d-69b0a5029c46",
"State": "Creating",
"FunctionResponseTypes": [
"ReportBatchItemFailures"
]
}Wait until ESM is Enabled
UUID=f15a615d-3375-4a10-8a0d-69b0a5029c46 for i in 1 2 3 4 5 6 7 8 9 10; do STATE=$(aws lambda get-event-source-mapping --uuid "$UUID" --query State --output text) echo "state=$STATE" if [ "$STATE" = "Enabled" ]; then break; fi sleep 2 done
state=Enabled [stdout] state=Enabled
Invoke order-api with OrderCreated
PAYLOAD1=$(printf '{"order_id":"o-1","kind":"OrderCreated","amount":42}' | base64 -w0)
aws lambda invoke --function-name order-api --payload "$PAYLOAD1" --cli-binary-format base64 /tmp/out1.json >/dev/null
cat /tmp/out1.json; echo{"statusCode": 200, "body": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"event_id\": \"c46683da-311f-4523-b7ec-2aebe902443d\"}"}
[stdout]
{"statusCode": 200, "body": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"event_id\": \"c46683da-311f-4523-b7ec-2aebe902443d\"}"}Poll queues for first event
CREATED_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created CANCELLED_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do CC=$(aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text) CN=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text) DD=$(aws dynamodb scan --table-name processed_events --select COUNT --query Count --output text) echo "tick=$i created=$CC cancelled=$CN dedup=$DD" if [ "$CC" = "1" ] && [ "$CN" = "0" ] && [ "$DD" = "1" ]; then break; fi sleep 2 done
tick=1 created=1 cancelled=0 dedup=1 [stdout] tick=1 created=1 cancelled=0 dedup=1
Verify OrderCancelled fans only to cancelled queue
CREATED_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created
CANCELLED_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
# drain orders-created so we can verify it stays at 0 after cancel
RECV=$(aws sqs receive-message --queue-url "$CREATED_URL" --max-number-of-messages 10 --wait-time-seconds 1 --visibility-timeout 30)
echo "received pre-test:"; echo "$RECV" | jq '.Messages[].Body | fromjson | {source,"detail-type",detail: (.detail|fromjson)}'
RH=$(echo "$RECV" | jq -r '.Messages[0].ReceiptHandle')
[ -n "$RH" ] && [ "$RH" != "null" ] && aws sqs delete-message --queue-url "$CREATED_URL" --receipt-handle "$RH"
PAYLOAD2=$(printf '{"order_id":"o-2","kind":"OrderCancelled","reason":"customer"}' | base64 -w0)
aws lambda invoke --function-name order-api --payload "$PAYLOAD2" --cli-binary-format base64 /tmp/out2.json >/dev/null
cat /tmp/out2.json; echo
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
CC=$(aws sqs get-queue-attributes --queue-url "$CREATED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
CN=$(aws sqs get-queue-attributes --queue-url "$CANCELLED_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text)
DD=$(aws dynamodb scan --table-name processed_events --select COUNT --query Count --output text)
echo "tick=$i created=$CC cancelled=$CN dedup=$DD"
if [ "$CC" = "0" ] && [ "$CN" = "1" ] && [ "$DD" = "2" ]; then break; fi
sleep 2
donereceived pre-test:
jq: error (at <stdin>:10): object ({"order_id"...) only strings can be parsed
{"statusCode": 200, "body": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"event_id\": \"6620de34-bc9a-429f-a430-310f8f9a0f54\"}"}
tick=1 created=0 cancelled=0 dedup=1
tick=2 created=0 cancelled=1 dedup=2
[stdout]
received pre-test:
jq: error (at <stdin>:10): object ({"order_id"...) only strings can be parsed
{"statusCode": 200, "body": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"event_id\": \"6620de34-bc9a-429f-a430-310f8f9a0f54\"}"}
tick=1 created=0 cancelled=0 dedup=1
tick=2 created=0 cancelled=1 dedup=2Inspect cancelled message body
CANCELLED_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
aws sqs receive-message --queue-url "$CANCELLED_URL" --max-number-of-messages 1 --visibility-timeout 1 --wait-time-seconds 1 | jq '.Messages[0].Body | fromjson | {source, "detail-type", detail}'{
"source": "outbox.relay",
"detail-type": "OrderCancelled",
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer"
}
}
[stdout]
{
"source": "outbox.relay",
"detail-type": "OrderCancelled",
"detail": {
"order_id": "o-2",
"kind": "OrderCancelled",
"reason": "customer"
}
}No file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/bpB8WaC46DETmDaaXXbZa/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 20 items
../tests/test_outputs.py::test_01_cmk_alias_exists_and_enabled PASSED [ 5%]
../tests/test_outputs.py::test_02_orders_table_exists PASSED [ 10%]
../tests/test_outputs.py::test_03_outbox_table_exists PASSED [ 15%]
../tests/test_outputs.py::test_04_processed_events_table_exists PASSED [ 20%]
../tests/test_outputs.py::test_05_custom_event_bus_exists PASSED [ 25%]
../tests/test_outputs.py::test_06_orders_table_encrypted_with_cmk PASSED [ 30%]
../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk PASSED [ 35%]
../tests/test_outputs.py::test_08_outbox_stream_is_new_and_old_images PASSED [ 40%]
../tests/test_outputs.py::test_09_processed_events_ttl_enabled PASSED [ 45%]
../tests/test_outputs.py::test_10_both_rule_patterns_use_array_wrap_and_disjoint_types PASSED [ 50%]
../tests/test_outputs.py::test_11_each_rule_targets_only_its_intended_queue PASSED [ 55%]
../tests/test_outputs.py::test_12_both_main_queues_encrypted_with_cmk PASSED [ 60%]
../tests/test_outputs.py::test_13_main_queues_have_redrive_to_distinct_dlqs PASSED [ 65%]
../tests/test_outputs.py::test_14_queue_policies_scope_events_with_source_arn FAILED [ 70%]
../tests/test_outputs.py::test_15_relay_esm_has_batch_item_failures_on_outbox_stream PASSED [ 75%]
../tests/test_outputs.py::test_16_relay_role_is_scoped_not_wildcard PASSED [ 80%]
../tests/test_outputs.py::test_17_order_api_role_scoped_to_orders_and_outbox_only PASSED [ 85%]
../tests/test_outputs.py::test_18_e2e_order_created_routes_to_created_queue_only PASSED [ 90%]
../tests/test_outputs.py::test_19_e2e_order_cancelled_routes_to_cancelled_queue_only PASSED [ 95%]
../tests/test_outputs.py::test_20_e2e_idempotent_duplicate_does_not_double_fanout PASSED [100%]
=================================== FAILURES ===================================
_____________ test_14_queue_policies_scope_events_with_source_arn ______________
def test_14_queue_policies_scope_events_with_source_arn():
"""Each main queue's resource policy must allow events.amazonaws.com
principal ONLY when aws:SourceArn matches the rule ARN for that queue.
This is the classic 'confused-deputy' protection that LLMs typically
either omit or wildcard."""
sqs = _client("sqs")
mapping = [
(QUEUE_CREATED, RULE_CREATED),
(QUEUE_CANCELLED, RULE_CANCELLED),
]
for qname, rule_name in mapping:
url = _queue_url(qname)
attrs = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["Policy"])[
"Attributes"
]
policy_str = attrs.get("Policy")
assert policy_str, f"{qname} missing resource policy"
policy = json.loads(policy_str)
stmts = policy.get("Statement", [])
if isinstance(stmts, dict):
stmts = [stmts]
matched = False
for s in stmts:
if s.get("Effect") != "Allow":
continue
principal = s.get("Principal", {})
svc = principal.get("Service") if isinstance(principal, dict) else None
svc_list = _as_list(svc)
if "events.amazonaws.com" not in svc_list:
continue
actions = _as_list(s.get("Action"))
if not any("SendMessage" in a or a == "sqs:*" for a in actions):
continue
cond = s.get("Condition", {})
arn_like = cond.get("ArnEquals") or cond.get("ArnLike") or {}
src_arn = arn_like.get("aws:SourceArn")
src_list = _as_list(src_arn)
if any(rule_name in v for v in src_list):
matched = True
break
> assert matched, (
f"{qname} resource policy must include an Allow for "
f"events.amazonaws.com scoped by aws:SourceArn containing "
f"rule '{rule_name}' - confused-deputy protection"
)
E AssertionError: orders-created resource policy must include an Allow for events.amazonaws.com scoped by aws:SourceArn containing rule 'on-order-created' - confused-deputy protection
E assert False
/tests/test_outputs.py:395: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 88 warnings
/root/.cache/uv/archive-v0/bpB8WaC46DETmDaaXXbZa/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_01_cmk_alias_exists_and_enabled
PASSED ../tests/test_outputs.py::test_02_orders_table_exists
PASSED ../tests/test_outputs.py::test_03_outbox_table_exists
PASSED ../tests/test_outputs.py::test_04_processed_events_table_exists
PASSED ../tests/test_outputs.py::test_05_custom_event_bus_exists
PASSED ../tests/test_outputs.py::test_06_orders_table_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_07_outbox_table_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_08_outbox_stream_is_new_and_old_images
PASSED ../tests/test_outputs.py::test_09_processed_events_ttl_enabled
PASSED ../tests/test_outputs.py::test_10_both_rule_patterns_use_array_wrap_and_disjoint_types
PASSED ../tests/test_outputs.py::test_11_each_rule_targets_only_its_intended_queue
PASSED ../tests/test_outputs.py::test_12_both_main_queues_encrypted_with_cmk
PASSED ../tests/test_outputs.py::test_13_main_queues_have_redrive_to_distinct_dlqs
PASSED ../tests/test_outputs.py::test_15_relay_esm_has_batch_item_failures_on_outbox_stream
PASSED ../tests/test_outputs.py::test_16_relay_role_is_scoped_not_wildcard
PASSED ../tests/test_outputs.py::test_17_order_api_role_scoped_to_orders_and_outbox_only
PASSED ../tests/test_outputs.py::test_18_e2e_order_created_routes_to_created_queue_only
PASSED ../tests/test_outputs.py::test_19_e2e_order_cancelled_routes_to_cancelled_queue_only
PASSED ../tests/test_outputs.py::test_20_e2e_idempotent_duplicate_does_not_double_fanout
FAILED ../tests/test_outputs.py::test_14_queue_policies_scope_events_with_source_arn
================== 1 failed, 19 passed, 88 warnings in 21.51s ==================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_f50e42f2e8904a1c. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_f50e42f2e8904a1c · verifier authoritative; classifier explanatory.