SyncValsverifier → artifact → classifier → verdict
SyncVals · Trajectory

ddb-outbox-eventbridge-fanout

claude-code claude-opus-4-7 ✗ failed BAD_FAILURE ↑ View task
Solved from the instruction alone, tests/ and solution/ were withheld from the agent's workspace and restored only for grading.
Reward = tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.
Classification , post-hoc; cannot change the reward
BAD_FAILUREThe task is at fault, underspecified/contradictory instruction, brittle/flaky tests, or tests demanding undiscoverable behavior.
SubtypeUnderspecified Instruction
EvidenceTest `test_14_queue_policies_scope_events_with_source_arn` fails with: \"orders-created resource policy must include an Allow for events.amazonaws.com scoped by aws:SourceArn containing rule 'on-order-created' - confused-deputy protection\". The test checks for aws:SourceArn conditions in SQS queue resource policies that scope access by EventBridge rule ARN. However, the instruction.md does not mention queue resource policies, aws:SourceArn conditions, or the confused-deputy protection pattern at all. The instruction only states \"least privilege: every role scoped to its own resources\" and \"the verifier reads the policy docs directly\" but never specifies that queue resource policies need aws:SourceArn scoping.
Root causeThe instruction mentions least privilege and policy verification but does not explicitly require or document the specific AWS security pattern of scoping queue resource policies with aws:SourceArn conditions to EventBridge rule ARNs. This is a well-known AWS best practice (confused-deputy protection) but requires specialized security domain knowledge not provided by the instruction.
RecommendationUpdate instruction.md to explicitly state: \"Each SQS queue must have a resource policy that allows the events.amazonaws.com service principal to send messages ONLY from the specific EventBridge rule that targets that queue, using aws:SourceArn conditions (confused-deputy protection pattern). For example, the orders-created queue policy should restrict messages from the on-order-created rule only.\" Alternatively, if this level of detail is intentional, consider moving this test to an optional 'advanced security' category or marking it as a known-hard requirement that agents should research."
Trajectory
Tool-by-tool agent trajectory
32 tool calls · 4 tool types · 42 steps
ok the checkout service has been dropping events again. every time the orders table gets a new row, *something* downstream needs to know , pricing, fulfillment, the analytics folks, all of them. right now we're just hoping two sequential putitems stick. they don't. last week we had a row written and no event fired because the lambda crashed between the two writes. nobody noticed for 6 hours. so: outbox pattern. one transaction, two rows, and let the stream do the fanout. build it on localstack , `http://localstack:4566`, creds already exported (`AWS_ACCESS_KEY_ID=test`, same for secret, region `us-east-1`). you've got `aws`, `python3`, `boto3`, `jq`, `zip`. build the whole thing from zero. shape of it: - an "order api" lambda is the only thing that writes orders. it takes `{order_id, kind, ...}` where `kind` is either `OrderCreated` or `OrderCancelled`. it writes the business row **and** the outbox row in one atomic step. no half-states allowed. - the outbox table has a stream , view type `NEW_AND_OLD_IMAGES` (the relay needs to see both the new and old image of the row, not just keys, so downstream subscribers can react on diffs and not just inserts). a relay lambda reads the stream via an event source mapping and republishes onto a custom eventbridge bus. - the relay's event source mapping must use `ReportBatchItemFailures` so a single bad record can fail without retrying the whole batch , the relay returns `{"batchItemFailures": [{"itemIdentifier": "<seq#>"}, ...]}` for the records it couldn't republish. - two rules on that bus route by kind: `OrderCreated` goes to one sqs queue, `OrderCancelled` goes to another. each queue has its own dlq. no shared dlq. - the relay has to be idempotent , if the stream redelivers the same record, we don't want the downstream queue to see the message twice. keep a small ddb table for dedup, ttl it so it doesn't grow forever. - everything is encrypted with a customer-managed kms key. not `aws/dynamodb`, not `aws/sqs`. ours. done looks like this: invoke `order-api` with `{order_id: "o-1", kind: "OrderCreated", ...}`, and within 30s: - `orders-created` sqs queue has exactly 1 message - `orders-cancelled` sqs queue has 0 messages - `processed_events` table has a row for the corresponding outbox event_id then invoke again with `kind: "OrderCancelled"` and a different `order_id`: - `orders-cancelled` picks it up, `orders-created` stays quiet least privilege: every role scoped to its own resources. the outbox-relay role should not be able to touch the orders table's stream; the order-api role should not be able to touch sqs; nobody gets `*` on kms. the verifier reads the policy docs directly, not just "does it work end-to-end". names the verifier grabs by hand, use them exactly: | thing | name | |-|-| | kms alias | `alias/outbox-cmk` | | orders table | `orders` (pk `order_id` / String) | | outbox table | `outbox` (pk `event_id` / String) | | dedup table | `processed_events` (pk `event_id` / String) | | api lambda | `order-api` | | api lambda role | `order-api-role` | | relay lambda | `outbox-relay` | | relay lambda role | `outbox-relay-role` | | event bus | `order-events` | | rule for created | `on-order-created` | | rule for cancelled | `on-order-cancelled` | | created queue | `orders-created` | | cancelled queue | `orders-cancelled` | | created dlq | `orders-created-dlq` | | cancelled dlq | `orders-cancelled-dlq` | eventbridge detail shape: the relay republishes with `source = "outbox.relay"` and `detail-type` matching `kind` exactly , `OrderCreated` or `OrderCancelled`. rule patterns match by those. python3.11 for both lambdas. they run on the shared compose network so `http://localstack:4566` is reachable from inside them. localstack community 4.13.1. outbox rows need an `event_id` so the relay has a stable dedup key; generate one however you like, just make sure it's the same id when the row gets read off the stream.

Inspect working directory and tools

shell
ls -la /app && echo "---" && which aws python3 jq zip && python3 --version && aws --version
output
total 8
drwxr-xr-x 2 root root 4096 Apr 21 20:05 .
drwxr-xr-x 1 root root 4096 Apr 28 14:37 ..
---
/usr/local/bin/aws
/opt/venv/bin/python3
/usr/bin/jq
/usr/bin/zip
Python 3.12.3
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24

[stdout]
total 8
drwxr-xr-x 2 root root 4096 Apr 21 20:05 .
drwxr-xr-x 1 root root 4096 Apr 28 14:37 ..
---
/usr/local/bin/aws
/opt/venv/bin/python3
/usr/bin/jq
/usr/bin/zip
Python 3.12.3
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24

Check localstack availability

shell
curl -s http://localstack:4566/_localstack/health | jq . 2>/dev/null | head -50
output
{
  "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",
    "sqs": "available",
    "ssm": "disabled",
    "stepfunctions": "disabled",
    "sts": "available",
    "support": "disabled",
    "swf": "disabled",
    "transcribe": "disabled"
  },
  "edition": "community",
  "version": "4.13.1"
}

[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",
    "sqs": "available",
    "ssm": "disabled",
    "stepfunctions": "disabled",
    "sts": "available",
    "support": "disabled",
    "swf": "disabled",
    "transcribe": "disabled"
  },
  "edition": "community",
  "version": "4.13.1"
}
Localstack is up. Let me design the build sequence and lay it out as scripts. I'll keep everything in `/app`.
ToolSearch
{
  "query": "select:TodoWrite",
  "max_results": 1
}

, output, 
{"type": "tool_reference", "tool_name": "TodoWrite"}
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled

Create build dirs

shell
mkdir -p /app/build /app/lambdas/order_api /app/lambdas/outbox_relay /app/iam && ls /app
output
build
iam
lambdas

[stdout]
build
iam
lambdas

Create env shim

shell
cat > /app/env.sh <<'EOF'
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_REGION=us-east-1
export AWS_PAGER=""
export EP=http://localstack:4566
alias awsl='aws --endpoint-url $EP'
EOF
cat /app/env.sh
output
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_REGION=us-east-1
export AWS_PAGER=""
export EP=http://localstack:4566
alias awsl='aws --endpoint-url $EP'

[stdout]
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_REGION=us-east-1
export AWS_PAGER=""
export EP=http://localstack:4566
alias awsl='aws --endpoint-url $EP'

Verify creds

shell
source /app/env.sh && aws --endpoint-url $EP sts get-caller-identity
output
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}

[stdout]
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}
Step 1: KMS CMK + alias.
write · /app/iam/kms-key-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableRoot",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::000000000000:root"},
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowAwsServices",
      "Effect": "Allow",
      "Principal": {"Service": ["dynamodb.amazonaws.com", "sqs.amazonaws.com", "events.amazonaws.com", "lambda.amazonaws.com"]},
      "Action": ["kms:Encrypt","kms:Decrypt","kms:ReEncrypt*","kms:GenerateDataKey*","kms:DescribeKey","kms:CreateGrant"],
      "Resource": "*"
    }
  ]
}

Create CMK + alias

shell
source /app/env.sh
KEY_ID=$(aws --endpoint-url $EP kms create-key \
  --description "outbox CMK" \
  --key-usage ENCRYPT_DECRYPT \
  --key-spec SYMMETRIC_DEFAULT \
  --policy file:///app/iam/kms-key-policy.json \
  | jq -r .KeyMetadata.KeyId)
echo "KEY_ID=$KEY_ID"
aws --endpoint-url $EP kms create-alias --alias-name alias/outbox-cmk --target-key-id "$KEY_ID"
KEY_ARN=$(aws --endpoint-url $EP kms describe-key --key-id alias/outbox-cmk | jq -r .KeyMetadata.Arn)
echo "KEY_ARN=$KEY_ARN"
echo "$KEY_ID" > /app/build/kms_key_id
echo "$KEY_ARN" > /app/build/kms_key_arn
output
KEY_ID=5ccc6de5-d379-458b-801f-baaeb4c83013
KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013

[stdout]
KEY_ID=5ccc6de5-d379-458b-801f-baaeb4c83013
KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled

Create DDB tables with CMK + stream + TTL

shell
source /app/env.sh
KEY_ARN=$(cat /app/build/kms_key_arn)

# orders table
aws --endpoint-url $EP 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_ARN" \
  > /dev/null && echo "orders created"

# outbox table with stream
aws --endpoint-url $EP 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 \
  --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=$KEY_ARN" \
  --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
  > /dev/null && echo "outbox created"

# processed_events with TTL
aws --endpoint-url $EP 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_ARN" \
  > /dev/null && echo "processed_events created"

# Wait for active state then enable TTL
for t in orders outbox processed_events; do
  aws --endpoint-url $EP dynamodb wait table-exists --table-name $t
done

aws --endpoint-url $EP dynamodb update-time-to-live \
  --table-name processed_events \
  --time-to-live-specification "Enabled=true,AttributeName=ttl" \
  > /dev/null && echo "TTL enabled"

OUTBOX_STREAM=$(aws --endpoint-url $EP dynamodb describe-table --table-name outbox | jq -r .Table.LatestStreamArn)
echo "OUTBOX_STREAM=$OUTBOX_STREAM"
echo "$OUTBOX_STREAM" > /app/build/outbox_stream_arn

aws --endpoint-url $EP dynamodb list-tables
output
orders created
outbox created
processed_events created
TTL enabled
OUTBOX_STREAM=arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987
{
    "TableNames": [
        "orders",
        "outbox",
        "processed_events"
    ]
}

[stdout]
orders created
outbox created
processed_events created
TTL enabled
OUTBOX_STREAM=arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987
{
    "TableNames": [
        "orders",
        "outbox",
        "processed_events"
    ]
}
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled
Step 3 & 4: SQS queues first (so the rule targets resolve), then EventBridge bus + rules.

Create SQS DLQs + main queues with redrive + CMK

shell
source /app/env.sh
KEY_ID=$(cat /app/build/kms_key_id)
KEY_ARN=$(cat /app/build/kms_key_arn)

# DLQs first
for q in orders-created-dlq orders-cancelled-dlq; do
  aws --endpoint-url $EP sqs create-queue \
    --queue-name $q \
    --attributes "{\"KmsMasterKeyId\":\"$KEY_ID\"}" > /dev/null
  echo "$q created"
done

CREATED_DLQ_URL=$(aws --endpoint-url $EP sqs get-queue-url --queue-name orders-created-dlq | jq -r .QueueUrl)
CANCELLED_DLQ_URL=$(aws --endpoint-url $EP sqs get-queue-url --queue-name orders-cancelled-dlq | jq -r .QueueUrl)
CREATED_DLQ_ARN=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_DLQ_URL --attribute-names QueueArn | jq -r .Attributes.QueueArn)
CANCELLED_DLQ_ARN=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CANCELLED_DLQ_URL --attribute-names QueueArn | jq -r .Attributes.QueueArn)
echo "CREATED_DLQ_ARN=$CREATED_DLQ_ARN"
echo "CANCELLED_DLQ_ARN=$CANCELLED_DLQ_ARN"

# Main queues with redrive
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"}')

aws --endpoint-url $EP sqs create-queue \
  --queue-name orders-created \
  --attributes "{\"KmsMasterKeyId\":\"$KEY_ID\",\"RedrivePolicy\":$(echo "$CREATED_REDRIVE" | jq -Rs .)}" > /dev/null
aws --endpoint-url $EP sqs create-queue \
  --queue-name orders-cancelled \
  --attributes "{\"KmsMasterKeyId\":\"$KEY_ID\",\"RedrivePolicy\":$(echo "$CANCELLED_REDRIVE" | jq -Rs .)}" > /dev/null

CREATED_URL=$(aws --endpoint-url $EP sqs get-queue-url --queue-name orders-created | jq -r .QueueUrl)
CANCELLED_URL=$(aws --endpoint-url $EP sqs get-queue-url --queue-name orders-cancelled | jq -r .QueueUrl)
CREATED_ARN=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names QueueArn | jq -r .Attributes.QueueArn)
CANCELLED_ARN=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CANCELLED_URL --attribute-names QueueArn | jq -r .Attributes.QueueArn)

echo "CREATED_ARN=$CREATED_ARN"
echo "CANCELLED_ARN=$CANCELLED_ARN"

echo "$CREATED_URL" > /app/build/orders_created_url
echo "$CANCELLED_URL" > /app/build/orders_cancelled_url
echo "$CREATED_ARN" > /app/build/orders_created_arn
echo "$CANCELLED_ARN" > /app/build/orders_cancelled_arn
echo "$CREATED_DLQ_ARN" > /app/build/orders_created_dlq_arn
echo "$CANCELLED_DLQ_ARN" > /app/build/orders_cancelled_dlq_arn
output
orders-created-dlq created
orders-cancelled-dlq created
CREATED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created-dlq
CANCELLED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq
CREATED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created
CANCELLED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled

[stdout]
orders-created-dlq created
orders-cancelled-dlq created
CREATED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created-dlq
CANCELLED_DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq
CREATED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-created
CANCELLED_ARN=arn:aws:sqs:us-east-1:000000000000:orders-cancelled

Verify queue attributes

shell
source /app/env.sh
# verify redrive + KMS attributes
for q in orders-created orders-cancelled; do
  url=$(aws --endpoint-url $EP sqs get-queue-url --queue-name $q | jq -r .QueueUrl)
  echo "=== $q ==="
  aws --endpoint-url $EP sqs get-queue-attributes --queue-url $url --attribute-names KmsMasterKeyId RedrivePolicy
done
output
=== orders-created ===
{
    "Attributes": {
        "KmsMasterKeyId": "5ccc6de5-d379-458b-801f-baaeb4c83013",
        "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-created-dlq\",\"maxReceiveCount\":\"5\"}\n"
    }
}
=== orders-cancelled ===
{
    "Attributes": {
        "KmsMasterKeyId": "5ccc6de5-d379-458b-801f-baaeb4c83013",
        "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq\",\"maxReceiveCount\":\"5\"}\n"
    }
}

[stdout]
=== orders-created ===
{
    "Attributes": {
        "KmsMasterKeyId": "5ccc6de5-d379-458b-801f-baaeb4c83013",
        "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-created-dlq\",\"maxReceiveCount\":\"5\"}\n"
    }
}
=== orders-cancelled ===
{
    "Attributes": {
        "KmsMasterKeyId": "5ccc6de5-d379-458b-801f-baaeb4c83013",
        "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-cancelled-dlq\",\"maxReceiveCount\":\"5\"}\n"
    }
}
Now the EventBridge bus + rules + queue policies so EventBridge can deliver.

Create bus, rules, targets, queue resource policies

shell
source /app/env.sh
KEY_ID=$(cat /app/build/kms_key_id)

aws --endpoint-url $EP events create-event-bus \
  --name order-events \
  --kms-key-identifier "$KEY_ID" > /dev/null && echo "bus created"

BUS_ARN=$(aws --endpoint-url $EP events describe-event-bus --name order-events | jq -r .Arn)
echo "BUS_ARN=$BUS_ARN"
echo "$BUS_ARN" > /app/build/bus_arn

# Rules
aws --endpoint-url $EP events put-rule \
  --name on-order-created \
  --event-bus-name order-events \
  --event-pattern '{"source":["outbox.relay"],"detail-type":["OrderCreated"]}' \
  > /dev/null && echo "rule on-order-created"

aws --endpoint-url $EP events put-rule \
  --name on-order-cancelled \
  --event-bus-name order-events \
  --event-pattern '{"source":["outbox.relay"],"detail-type":["OrderCancelled"]}' \
  > /dev/null && echo "rule on-order-cancelled"

CREATED_ARN=$(cat /app/build/orders_created_arn)
CANCELLED_ARN=$(cat /app/build/orders_cancelled_arn)
CREATED_URL=$(cat /app/build/orders_created_url)
CANCELLED_URL=$(cat /app/build/orders_cancelled_url)

# Targets
aws --endpoint-url $EP events put-targets \
  --rule on-order-created --event-bus-name order-events \
  --targets "Id=created-q,Arn=$CREATED_ARN" > /dev/null && echo "target -> created"

aws --endpoint-url $EP events put-targets \
  --rule on-order-cancelled --event-bus-name order-events \
  --targets "Id=cancelled-q,Arn=$CANCELLED_ARN" > /dev/null && echo "target -> cancelled"

# Allow EventBridge to send to each queue
for pair in "$CREATED_URL|$CREATED_ARN" "$CANCELLED_URL|$CANCELLED_ARN"; do
  url=${pair%%|*}; arn=${pair##*|}
  pol=$(jq -nc --arg arn "$arn" '{Version:"2012-10-17",Statement:[{Sid:"AllowEB",Effect:"Allow",Principal:{Service:"events.amazonaws.com"},Action:"sqs:SendMessage",Resource:$arn}]}')
  aws --endpoint-url $EP sqs set-queue-attributes \
    --queue-url $url \
    --attributes "{\"Policy\":$(echo "$pol" | jq -Rs .)}" > /dev/null
  echo "policy set on $url"
done
output
bus created
BUS_ARN=arn:aws:events:us-east-1:000000000000:event-bus/order-events
rule on-order-created
rule on-order-cancelled
target -> created
target -> cancelled
policy set on http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created
policy set on http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled

[stdout]
bus created
BUS_ARN=arn:aws:events:us-east-1:000000000000:event-bus/order-events
rule on-order-created
rule on-order-cancelled
target -> created
target -> cancelled
policy set on http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-created
policy set on http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/orders-cancelled
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled
write · /app/iam/lambda-trust.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }
  ]
}

Create IAM roles + scoped policies

shell
source /app/env.sh
KEY_ARN=$(cat /app/build/kms_key_arn)
OUTBOX_STREAM=$(cat /app/build/outbox_stream_arn)
BUS_ARN=$(cat /app/build/bus_arn)

# order-api-role policy
cat > /app/iam/order-api-policy.json <<EOF
{
  "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": "TxnWriteOrdersAndOutbox",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem","dynamodb:UpdateItem","dynamodb:TransactWriteItems"],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:000000000000:table/orders",
        "arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
      ]
    },
    {
      "Sid": "DescribeForCmk",
      "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": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": ["kms:Encrypt","kms:Decrypt","kms:GenerateDataKey","kms:DescribeKey"],
      "Resource": "$KEY_ARN",
      "Condition": {"StringEquals": {"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"}}
    }
  ]
}
EOF

# outbox-relay-role policy
cat > /app/iam/outbox-relay-policy.json <<EOF
{
  "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"
      ],
      "Resource": "$OUTBOX_STREAM"
    },
    {
      "Sid": "DedupTableWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem","dynamodb:GetItem","dynamodb:DescribeTable"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/processed_events"
    },
    {
      "Sid": "PutEventsOnBus",
      "Effect": "Allow",
      "Action": "events:PutEvents",
      "Resource": "$BUS_ARN"
    },
    {
      "Sid": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": ["kms:Decrypt","kms:Encrypt","kms:GenerateDataKey","kms:DescribeKey"],
      "Resource": "$KEY_ARN",
      "Condition": {"StringEquals": {"kms:ViaService": "dynamodb.us-east-1.amazonaws.com"}}
    },
    {
      "Sid": "UseCmkForEvents",
      "Effect": "Allow",
      "Action": ["kms:GenerateDataKey","kms:Decrypt"],
      "Resource": "$KEY_ARN",
      "Condition": {"StringEquals": {"kms:ViaService": "events.us-east-1.amazonaws.com"}}
    }
  ]
}
EOF

# Create roles
aws --endpoint-url $EP iam create-role \
  --role-name order-api-role \
  --assume-role-policy-document file:///app/iam/lambda-trust.json > /dev/null && echo "order-api-role created"

aws --endpoint-url $EP iam create-role \
  --role-name outbox-relay-role \
  --assume-role-policy-document file:///app/iam/lambda-trust.json > /dev/null && echo "outbox-relay-role created"

aws --endpoint-url $EP iam put-role-policy \
  --role-name order-api-role \
  --policy-name order-api-inline \
  --policy-document file:///app/iam/order-api-policy.json && echo "order-api policy attached"

aws --endpoint-url $EP iam put-role-policy \
  --role-name outbox-relay-role \
  --policy-name outbox-relay-inline \
  --policy-document file:///app/iam/outbox-relay-policy.json && echo "outbox-relay policy attached"

ORDER_API_ROLE_ARN=$(aws --endpoint-url $EP iam get-role --role-name order-api-role | jq -r .Role.Arn)
RELAY_ROLE_ARN=$(aws --endpoint-url $EP iam get-role --role-name outbox-relay-role | jq -r .Role.Arn)
echo "$ORDER_API_ROLE_ARN" > /app/build/order_api_role_arn
echo "$RELAY_ROLE_ARN" > /app/build/relay_role_arn
echo "ORDER_API_ROLE_ARN=$ORDER_API_ROLE_ARN"
echo "RELAY_ROLE_ARN=$RELAY_ROLE_ARN"
output
order-api-role created
outbox-relay-role created
order-api policy attached
outbox-relay policy attached
ORDER_API_ROLE_ARN=arn:aws:iam::000000000000:role/order-api-role
RELAY_ROLE_ARN=arn:aws:iam::000000000000:role/outbox-relay-role

[stdout]
order-api-role created
outbox-relay-role created
order-api policy attached
outbox-relay policy attached
ORDER_API_ROLE_ARN=arn:aws:iam::000000000000:role/order-api-role
RELAY_ROLE_ARN=arn:aws:iam::000000000000:role/outbox-relay-role
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled
write · /app/lambdas/order_api/handler.py
import json
import os
import time
import uuid

import boto3

ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name="us-east-1")

ALLOWED_KINDS = {"OrderCreated", "OrderCancelled"}


def handler(event, _context):
    body = event if isinstance(event, dict) else json.loads(event)
    order_id = body["order_id"]
    kind = body["kind"]
    if kind not in ALLOWED_KINDS:
        raise ValueError(f"unsupported kind: {kind}")

    event_id = str(uuid.uuid4())
    now = int(time.time())

    order_item = {
        "order_id": {"S": order_id},
        "kind": {"S": kind},
        "updated_at": {"N": str(now)},
        "payload": {"S": json.dumps(body)},
    }

    outbox_item = {
        "event_id": {"S": event_id},
        "order_id": {"S": order_id},
        "kind": {"S": kind},
        "created_at": {"N": str(now)},
        "payload": {"S": json.dumps(body)},
    }

    ddb.transact_write_items(
        TransactItems=[
            {"Put": {"TableName": "orders", "Item": order_item}},
            {"Put": {"TableName": "outbox", "Item": outbox_item}},
        ]
    )

    return {"order_id": order_id, "event_id": event_id, "kind": kind}
write · /app/lambdas/outbox_relay/handler.py
import json
import os
import time

import boto3
from botocore.exceptions import ClientError

ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
BUS_NAME = os.environ.get("BUS_NAME", "order-events")
DEDUP_TABLE = os.environ.get("DEDUP_TABLE", "processed_events")
DEDUP_TTL_SECONDS = int(os.environ.get("DEDUP_TTL_SECONDS", "604800"))  # 7d

events = boto3.client("events", endpoint_url=ENDPOINT, region_name="us-east-1")
ddb = boto3.client("dynamodb", endpoint_url=ENDPOINT, region_name="us-east-1")


def _img_to_plain(image):
    out = {}
    for k, v in image.items():
        if "S" in v:
            out[k] = v["S"]
        elif "N" in v:
            out[k] = v["N"]
        elif "BOOL" in v:
            out[k] = v["BOOL"]
        else:
            out[k] = v
    return out


def _claim(event_id):
    """Reserve event_id in dedup table. Returns True if we won the claim."""
    try:
        ddb.put_item(
            TableName=DEDUP_TABLE,
            Item={
                "event_id": {"S": event_id},
                "processed_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 rec in event.get("Records", []):
        seq = rec.get("dynamodb", {}).get("SequenceNumber")
        try:
            new_image = rec.get("dynamodb", {}).get("NewImage")
            if rec.get("eventName") not in ("INSERT", "MODIFY") or not new_image:
                # nothing to publish (e.g. REMOVE) , ack and skip
                continue

            row = _img_to_plain(new_image)
            event_id = row.get("event_id")
            kind = row.get("kind")
            if not event_id or kind not in ("OrderCreated", "OrderCancelled"):
                # malformed , skip rather than poison the stream
                continue

            if not _claim(event_id):
                # already processed; skip silently
                continue

            detail = {
                "event_id": event_id,
                "order_id": row.get("order_id"),
                "kind": kind,
                "payload": row.get("payload"),
            }

            resp = events.put_events(
                Entries=[
                    {
                        "EventBusName": BUS_NAME,
                        "Source": "outbox.relay",
                        "DetailType": kind,
                        "Detail": json.dumps(detail),
                    }
                ]
            )
            if resp.get("FailedEntryCount", 0) > 0:
                raise RuntimeError(f"PutEvents failed: {resp}")

        except Exception as exc:  # noqa: BLE001
            print(f"record {seq} failed: {exc!r}")
            if seq:
                failures.append({"itemIdentifier": seq})

    return {"batchItemFailures": failures}

Zip lambda packages

shell
cd /app/lambdas/order_api && zip -q -r /app/build/order-api.zip handler.py
cd /app/lambdas/outbox_relay && zip -q -r /app/build/outbox-relay.zip handler.py
ls -la /app/build/
output
total 64
drwxr-xr-x 2 root root 4096 Apr 28 14:42 .
drwxr-xr-x 1 root root 4096 Apr 28 14:39 ..
-rw-r--r-- 1 root root   61 Apr 28 14:41 bus_arn
-rw-r--r-- 1 root root   76 Apr 28 14:39 kms_key_arn
-rw-r--r-- 1 root root   37 Apr 28 14:39 kms_key_id
-rw-r--r-- 1 root root  705 Apr 28 14:42 order-api.zip
-rw-r--r-- 1 root root   46 Apr 28 14:41 order_api_role_arn
-rw-r--r-- 1 root root   52 Apr 28 14:40 orders_cancelled_arn
-rw-r--r-- 1 root root   56 Apr 28 14:40 orders_cancelled_dlq_arn
-rw-r--r-- 1 root root   83 Apr 28 14:40 orders_cancelled_url
-rw-r--r-- 1 root root   50 Apr 28 14:40 orders_created_arn
-rw-r--r-- 1 root root   54 Apr 28 14:40 orders_created_dlq_arn
-rw-r--r-- 1 root root   81 Apr 28 14:40 orders_created_url
-rw-r--r-- 1 root root 1381 Apr 28 14:42 outbox-relay.zip
-rw-r--r-- 1 root root   84 Apr 28 14:40 outbox_stream_arn
-rw-r--r-- 1 root root   49 Apr 28 14:41 relay_role_arn

[stdout]
total 64
drwxr-xr-x 2 root root 4096 Apr 28 14:42 .
drwxr-xr-x 1 root root 4096 Apr 28 14:39 ..
-rw-r--r-- 1 root root   61 Apr 28 14:41 bus_arn
-rw-r--r-- 1 root root   76 Apr 28 14:39 kms_key_arn
-rw-r--r-- 1 root root   37 Apr 28 14:39 kms_key_id
-rw-r--r-- 1 root root  705 Apr 28 14:42 order-api.zip
-rw-r--r-- 1 root root   46 Apr 28 14:41 order_api_role_arn
-rw-r--r-- 1 root root   52 Apr 28 14:40 orders_cancelled_arn
-rw-r--r-- 1 root root   56 Apr 28 14:40 orders_cancelled_dlq_arn
-rw-r--r-- 1 root root   83 Apr 28 14:40 orders_cancelled_url
-rw-r--r-- 1 root root   50 Apr 28 14:40 orders_created_arn
-rw-r--r-- 1 root root   54 Apr 28 14:40 orders_created_dlq_arn
-rw-r--r-- 1 root root   81 Apr 28 14:40 orders_created_url
-rw-r--r-- 1 root root 1381 Apr 28 14:42 outbox-relay.zip
-rw-r--r-- 1 root root   84 Apr 28 14:40 outbox_stream_arn
-rw-r--r-- 1 root root   49 Apr 28 14:41 relay_role_arn

Create both lambdas

shell
source /app/env.sh
ORDER_API_ROLE_ARN=$(cat /app/build/order_api_role_arn)
RELAY_ROLE_ARN=$(cat /app/build/relay_role_arn)
KEY_ARN=$(cat /app/build/kms_key_arn)

aws --endpoint-url $EP lambda create-function \
  --function-name order-api \
  --runtime python3.11 \
  --role "$ORDER_API_ROLE_ARN" \
  --handler handler.handler \
  --zip-file fileb:///app/build/order-api.zip \
  --timeout 30 \
  --kms-key-arn "$KEY_ARN" \
  --environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566}" > /dev/null && echo "order-api created"

aws --endpoint-url $EP lambda create-function \
  --function-name outbox-relay \
  --runtime python3.11 \
  --role "$RELAY_ROLE_ARN" \
  --handler handler.handler \
  --zip-file fileb:///app/build/outbox-relay.zip \
  --timeout 30 \
  --kms-key-arn "$KEY_ARN" \
  --environment "Variables={AWS_ENDPOINT_URL=http://localstack:4566,BUS_NAME=order-events,DEDUP_TABLE=processed_events}" > /dev/null && echo "outbox-relay created"

aws --endpoint-url $EP lambda wait function-active-v2 --function-name order-api
aws --endpoint-url $EP lambda wait function-active-v2 --function-name outbox-relay
echo "both active"
output
order-api created
outbox-relay created
both active

[stdout]
order-api created
outbox-relay created
both active

Create ESM with ReportBatchItemFailures

shell
source /app/env.sh
OUTBOX_STREAM=$(cat /app/build/outbox_stream_arn)

aws --endpoint-url $EP lambda create-event-source-mapping \
  --function-name outbox-relay \
  --event-source-arn "$OUTBOX_STREAM" \
  --starting-position TRIM_HORIZON \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 1 \
  --function-response-types ReportBatchItemFailures \
  > /tmp/esm.json
cat /tmp/esm.json
ESM_UUID=$(jq -r .UUID /tmp/esm.json)
echo "ESM_UUID=$ESM_UUID"
echo "$ESM_UUID" > /app/build/esm_uuid

# wait until enabled
for i in $(seq 1 20); do
  state=$(aws --endpoint-url $EP lambda get-event-source-mapping --uuid $ESM_UUID | jq -r .State)
  echo "ESM state: $state"
  [ "$state" = "Enabled" ] && break
  sleep 1
done
output
{
    "UUID": "7346a8b0-4769-4cfc-a85f-d19510c48511",
    "StartingPosition": "TRIM_HORIZON",
    "BatchSize": 10,
    "MaximumBatchingWindowInSeconds": 1,
    "ParallelizationFactor": 1,
    "EventSourceArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:outbox-relay",
    "LastModified": "2026-04-28T14:42:56.240664+00:00",
    "LastProcessingResult": "No records processed",
    "State": "Creating",
    "StateTransitionReason": "User action",
    "DestinationConfig": {
        "OnFailure": {}
    },
    "MaximumRecordAgeInSeconds": -1,
    "BisectBatchOnFunctionError": false,
    "MaximumRetryAttempts": -1,
    "TumblingWindowInSeconds": 0,
    "FunctionResponseTypes": [
        "ReportBatchItemFailures"
    ],
    "EventSourceMappingArn": "arn:aws:lambda:us-east-1:000000000000:event-source-mapping:7346a8b0-4769-4cfc-a85f-d19510c48511"
}
ESM_UUID=7346a8b0-4769-4cfc-a85f-d19510c48511
ESM state: Enabled

[stdout]
{
    "UUID": "7346a8b0-4769-4cfc-a85f-d19510c48511",
    "StartingPosition": "TRIM_HORIZON",
    "BatchSize": 10,
    "MaximumBatchingWindowInSeconds": 1,
    "ParallelizationFactor": 1,
    "EventSourceArn": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:outbox-relay",
    "LastModified": "2026-04-28T14:42:56.240664+00:00",
    "LastProcessingResult": "No records processed",
    "State": "Creating",
    "StateTransitionReason": "User action",
    "DestinationConfig": {
        "OnFailure": {}
    },
    "MaximumRecordAgeInSeconds": -1,
    "BisectBatchOnFunctionError": false,
    "MaximumRetryAttempts": -1,
    "TumblingWindowInSeconds": 0,
    "FunctionResponseTypes": [
        "ReportBatchItemFailures"
    ],
    "EventSourceMappingArn": "arn:aws:lambda:us-east-1:000000000000:event-source-mapping:7346a8b0-4769-4cfc-a85f-d19510c48511"
}
ESM_UUID=7346a8b0-4769-4cfc-a85f-d19510c48511
ESM state: Enabled
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled

Invoke order-api twice and watch queues

shell
source /app/env.sh
CREATED_URL=$(cat /app/build/orders_created_url)
CANCELLED_URL=$(cat /app/build/orders_cancelled_url)

# Drain any leftover messages
aws --endpoint-url $EP sqs purge-queue --queue-url $CREATED_URL || true
aws --endpoint-url $EP sqs purge-queue --queue-url $CANCELLED_URL || true
sleep 2

# 1) OrderCreated for o-1
PAYLOAD1=$(echo -n '{"order_id":"o-1","kind":"OrderCreated","amount_cents":4999,"sku":"abc"}' | base64 -w0)
aws --endpoint-url $EP lambda invoke \
  --function-name order-api \
  --payload "$PAYLOAD1" \
  --cli-binary-format base64 \
  /tmp/order-api-1.json
echo "--- order-api response 1:"
cat /tmp/order-api-1.json; echo

# 2) OrderCancelled for o-2
PAYLOAD2=$(echo -n '{"order_id":"o-2","kind":"OrderCancelled","reason":"buyer changed mind"}' | base64 -w0)
aws --endpoint-url $EP lambda invoke \
  --function-name order-api \
  --payload "$PAYLOAD2" \
  --cli-binary-format base64 \
  /tmp/order-api-2.json
echo "--- order-api response 2:"
cat /tmp/order-api-2.json; echo

# Wait for stream to propagate through relay
echo "waiting up to 30s for relay to process..."
for i in $(seq 1 30); do
  c=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names ApproximateNumberOfMessages | jq -r .Attributes.ApproximateNumberOfMessages)
  x=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CANCELLED_URL --attribute-names ApproximateNumberOfMessages | jq -r .Attributes.ApproximateNumberOfMessages)
  echo "t=${i}s created=$c cancelled=$x"
  if [ "$c" = "1" ] && [ "$x" = "1" ]; then break; fi
  sleep 1
done
output
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- order-api response 1:
{"order_id": "o-1", "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a", "kind": "OrderCreated"}
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- order-api response 2:
{"order_id": "o-2", "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086", "kind": "OrderCancelled"}
waiting up to 30s for relay to process...
t=1s created=1 cancelled=0
t=2s created=1 cancelled=1

[stdout]
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- order-api response 1:
{"order_id": "o-1", "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a", "kind": "OrderCreated"}
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- order-api response 2:
{"order_id": "o-2", "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086", "kind": "OrderCancelled"}
waiting up to 30s for relay to process...
t=1s created=1 cancelled=0
t=2s created=1 cancelled=1
Both lambdas worked end-to-end in 2 seconds. Let me verify the dedup table and the message contents.

Verify msgs, dedup, tables, DLQs

shell
source /app/env.sh
CREATED_URL=$(cat /app/build/orders_created_url)
CANCELLED_URL=$(cat /app/build/orders_cancelled_url)

echo "=== orders-created message ==="
aws --endpoint-url $EP sqs receive-message --queue-url $CREATED_URL --max-number-of-messages 10 --wait-time-seconds 1 --visibility-timeout 1 | jq '.Messages[].Body | fromjson | {source, "detail-type", detail: (.detail|fromjson)}'

echo "=== orders-cancelled message ==="
aws --endpoint-url $EP sqs receive-message --queue-url $CANCELLED_URL --max-number-of-messages 10 --wait-time-seconds 1 --visibility-timeout 1 | jq '.Messages[].Body | fromjson | {source, "detail-type", detail: (.detail|fromjson)}'

echo "=== processed_events scan ==="
aws --endpoint-url $EP dynamodb scan --table-name processed_events | jq '.Items[] | {event_id: .event_id.S, ttl: .ttl.N}'

echo "=== outbox scan ==="
aws --endpoint-url $EP dynamodb scan --table-name outbox | jq '.Items[] | {event_id: .event_id.S, order_id: .order_id.S, kind: .kind.S}'

echo "=== orders scan ==="
aws --endpoint-url $EP dynamodb scan --table-name orders | jq '.Items[] | {order_id: .order_id.S, kind: .kind.S}'

echo "=== DLQ depths ==="
for q in orders-created-dlq orders-cancelled-dlq; do
  url=$(aws --endpoint-url $EP sqs get-queue-url --queue-name $q | jq -r .QueueUrl)
  d=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $url --attribute-names ApproximateNumberOfMessages | jq -r .Attributes.ApproximateNumberOfMessages)
  echo "$q = $d"
done
output
=== orders-created message ===
jq: error (at <stdin>:10): object ({"event_id"...) only strings can be parsed
=== orders-cancelled message ===
jq: error (at <stdin>:10): object ({"event_id"...) only strings can be parsed
=== processed_events scan ===
{
  "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
  "ttl": "1777992201"
}
{
  "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
  "ttl": "1777992204"
}
=== outbox scan ===
{
  "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
  "order_id": "o-1",
  "kind": "OrderCreated"
}
{
  "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
  "order_id": "o-2",
  "kind": "OrderCancelled"
}
=== orders scan ===
{
  "order_id": "o-1",
  "kind": "OrderCreated"
}
{
  "order_id": "o-2",
  "kind": "OrderCancelled"
}
=== DLQ depths ===
orders-created-dlq = 0
orders-cancelled-dlq = 0

[stdout]
=== orders-created message ===
jq: error (at <stdin>:10): object ({"event_id"...) only strings can be parsed
=== orders-cancelled message ===
jq: error (at <stdin>:10): object ({"event_id"...) only strings can be parsed
=== processed_events scan ===
{
  "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
  "ttl": "1777992201"
}
{
  "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
  "ttl": "1777992204"
}
=== outbox scan ===
{
  "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
  "order_id": "o-1",
  "kind": "OrderCreated"
}
{
  "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
  "order_id": "o-2",
  "kind": "OrderCancelled"
}
=== orders scan ===
{
  "order_id": "o-1",
  "kind": "OrderCreated"
}
{
  "order_id": "o-2",
  "kind": "OrderCancelled"
}
=== DLQ depths ===
orders-created-dlq = 0
orders-cancelled-dlq = 0
The detail came through as an object already. Let me re-look without the extra parse and also test idempotency.

Peek SQS message envelopes

shell
source /app/env.sh
CREATED_URL=$(cat /app/build/orders_created_url)
CANCELLED_URL=$(cat /app/build/orders_cancelled_url)

# wait for visibility to expire
sleep 2

echo "=== orders-created message (raw envelope) ==="
aws --endpoint-url $EP sqs receive-message --queue-url $CREATED_URL --max-number-of-messages 10 --wait-time-seconds 1 --visibility-timeout 1 | jq '.Messages[] | .Body | fromjson | {source, "detail-type", detail}'

echo "=== orders-cancelled message (raw envelope) ==="
aws --endpoint-url $EP sqs receive-message --queue-url $CANCELLED_URL --max-number-of-messages 10 --wait-time-seconds 1 --visibility-timeout 1 | jq '.Messages[] | .Body | fromjson | {source, "detail-type", detail}'
output
=== orders-created message (raw envelope) ===
{
  "source": "outbox.relay",
  "detail-type": "OrderCreated",
  "detail": {
    "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
    "order_id": "o-1",
    "kind": "OrderCreated",
    "payload": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"amount_cents\": 4999, \"sku\": \"abc\"}"
  }
}
=== orders-cancelled message (raw envelope) ===
{
  "source": "outbox.relay",
  "detail-type": "OrderCancelled",
  "detail": {
    "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
    "order_id": "o-2",
    "kind": "OrderCancelled",
    "payload": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"reason\": \"buyer changed mind\"}"
  }
}

[stdout]
=== orders-created message (raw envelope) ===
{
  "source": "outbox.relay",
  "detail-type": "OrderCreated",
  "detail": {
    "event_id": "3c08151f-62a9-47a8-a18e-ce67ce17db0a",
    "order_id": "o-1",
    "kind": "OrderCreated",
    "payload": "{\"order_id\": \"o-1\", \"kind\": \"OrderCreated\", \"amount_cents\": 4999, \"sku\": \"abc\"}"
  }
}
=== orders-cancelled message (raw envelope) ===
{
  "source": "outbox.relay",
  "detail-type": "OrderCancelled",
  "detail": {
    "event_id": "d9a9428b-60a7-4509-bafe-543d0d43e086",
    "order_id": "o-2",
    "kind": "OrderCancelled",
    "payload": "{\"order_id\": \"o-2\", \"kind\": \"OrderCancelled\", \"reason\": \"buyer changed mind\"}"
  }
}
Let me also confirm idempotency: invoke the relay with a synthesized re-delivery of an already-processed record and make sure it doesn't double-publish.

Test idempotency on re-delivery

shell
source /app/env.sh
CREATED_URL=$(cat /app/build/orders_created_url)

# get current depth
before=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names ApproximateNumberOfMessages | jq -r .Attributes.ApproximateNumberOfMessages)
echo "before: $before"

# Build a synthetic stream event re-delivering event_id 3c08151f-62a9-47a8-a18e-ce67ce17db0a
cat > /tmp/replay.json <<'EOF'
{
  "Records": [
    {
      "eventName": "INSERT",
      "dynamodb": {
        "SequenceNumber": "999999999",
        "NewImage": {
          "event_id": {"S": "3c08151f-62a9-47a8-a18e-ce67ce17db0a"},
          "order_id": {"S": "o-1"},
          "kind": {"S": "OrderCreated"},
          "created_at": {"N": "1"},
          "payload": {"S": "{\"order_id\":\"o-1\",\"kind\":\"OrderCreated\"}"}
        }
      }
    }
  ]
}
EOF

aws --endpoint-url $EP lambda invoke \
  --function-name outbox-relay \
  --payload fileb:///tmp/replay.json \
  /tmp/replay-resp.json
echo "--- relay response:"
cat /tmp/replay-resp.json; echo

sleep 2
after=$(aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names ApproximateNumberOfMessages,ApproximateNumberOfMessagesNotVisible | jq .Attributes)
echo "after: $after"
output
before: 1
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- relay response:
{"batchItemFailures": []}

aws: [ERROR]: An error occurred (InvalidAttributeName) when calling the GetQueueAttributes operation: Unknown Attribute ApproximateNumberOfMessages,ApproximateNumberOfMessagesNotVisible.
after:

[stdout]
before: 1
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
--- relay response:
{"batchItemFailures": []}

aws: [ERROR]: An error occurred (InvalidAttributeName) when calling the GetQueueAttributes operation: Unknown Attribute ApproximateNumberOfMessages,ApproximateNumberOfMessagesNotVisible.
after:

Check queue depth after replay

shell
source /app/env.sh
CREATED_URL=$(cat /app/build/orders_created_url)
aws --endpoint-url $EP sqs get-queue-attributes --queue-url $CREATED_URL --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible | jq .Attributes
output
{
  "ApproximateNumberOfMessages": "1",
  "ApproximateNumberOfMessagesNotVisible": "0"
}

[stdout]
{
  "ApproximateNumberOfMessages": "1",
  "ApproximateNumberOfMessagesNotVisible": "0"
}
Idempotency confirmed (depth stayed at 1 after replay). Final sanity check on the IAM policies the verifier will inspect.

Dump IAM policy docs

shell
source /app/env.sh
echo "=== order-api-role policy ==="
aws --endpoint-url $EP iam get-role-policy --role-name order-api-role --policy-name order-api-inline | jq .PolicyDocument
echo
echo "=== outbox-relay-role policy ==="
aws --endpoint-url $EP iam get-role-policy --role-name outbox-relay-role --policy-name outbox-relay-inline | jq .PolicyDocument
output
=== order-api-role policy ===
{
  "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": "TxnWriteOrdersAndOutbox",
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:TransactWriteItems"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:000000000000:table/orders",
        "arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
      ]
    },
    {
      "Sid": "DescribeForCmk",
      "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": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}

=== outbox-relay-role policy ===
{
  "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"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987"
    },
    {
      "Sid": "DedupTableWrite",
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:DescribeTable"
      ],
      "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": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:Encrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
        }
      }
    },
    {
      "Sid": "UseCmkForEvents",
      "Effect": "Allow",
      "Action": [
        "kms:GenerateDataKey",
        "kms:Decrypt"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "events.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}

[stdout]
=== order-api-role policy ===
{
  "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": "TxnWriteOrdersAndOutbox",
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:TransactWriteItems"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:000000000000:table/orders",
        "arn:aws:dynamodb:us-east-1:000000000000:table/outbox"
      ]
    },
    {
      "Sid": "DescribeForCmk",
      "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": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}

=== outbox-relay-role policy ===
{
  "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"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/outbox/stream/2026-04-28T14:40:17.987"
    },
    {
      "Sid": "DedupTableWrite",
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:DescribeTable"
      ],
      "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": "UseCmkForDdb",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "kms:Encrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "dynamodb.us-east-1.amazonaws.com"
        }
      }
    },
    {
      "Sid": "UseCmkForEvents",
      "Effect": "Allow",
      "Action": [
        "kms:GenerateDataKey",
        "kms:Decrypt"
      ],
      "Resource": "arn:aws:kms:us-east-1:000000000000:key/5ccc6de5-d379-458b-801f-baaeb4c83013",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "events.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}
  • Create CMK + alias/outbox-cmk
  • Create DDB tables (orders, outbox w/ stream NEW_AND_OLD_IMAGES, processed_events w/ TTL)
  • Create EventBridge bus + rules
  • Create SQS queues + DLQs (CMK encrypted)
  • Create IAM roles with least-privilege policies
  • Build & deploy order-api lambda
  • Build & deploy outbox-relay lambda + ESM with ReportBatchItemFailures
  • End-to-end test: OrderCreated then OrderCancelled
All built and verified. Summary of what's live on `http://localstack:4566`: **Atomicity** , `order-api` (`/app/lambdas/order_api/handler.py`) writes the `orders` row and the `outbox` row in a single `TransactWriteItems`. No half-state. **Stream → fanout** , `outbox` table has a stream view `NEW_AND_OLD_IMAGES`. The ESM on `outbox-relay` is created with `--function-response-types ReportBatchItemFailures`, and the relay returns `{"batchItemFailures": [{"itemIdentifier": <seq>}]}` for any record it fails to publish (`/app/lambdas/outbox_relay/handler.py:91`). **Routing** , `outbox-relay` calls `events.put_events` with `Source = "outbox.relay"` and `DetailType = kind`. The two rules on `order-events` match `detail-type: ["OrderCreated"]` → `orders-created` queue, and `["OrderCancelled"]` → `orders-cancelled` queue. **DLQs** , separate per queue: `orders-created-dlq`, `orders-cancelled-dlq`, each wired via `RedrivePolicy` with `maxReceiveCount=5`. **Idempotency** , `processed_events` (pk `event_id`, TTL on `ttl` attr) is reserved with a `attribute_not_exists(event_id)` conditional put before publishing. Verified with a manual replay , depth on `orders-created` stayed at 1. **KMS** , single CMK `alias/outbox-cmk` (id `5ccc6de5-…`) wraps `orders`, `outbox`, `processed_events`, both queues + both DLQs, the `order-events` bus, and both lambdas. Role policies reference the key by ARN with `kms:ViaService` conditions, never `*`. **Least privilege**: - `order-api-role`: `TransactWriteItems` on `orders`+`outbox` only, no SQS, no streams, no events. - `outbox-relay-role`: stream actions on the `outbox/stream/...` ARN only (cannot read `orders`' stream because `orders` has no stream and the ARN is specific anyway), `events:PutEvents` on the `order-events` bus ARN only, dedup table only, no SQS. **End-to-end test result**: - Invoke `order-api {order_id:"o-1", kind:"OrderCreated"}` → `orders-created` = 1, `orders-cancelled` = 0, `processed_events` has `3c08151f-…`. - Invoke `order-api {order_id:"o-2", kind:"OrderCancelled"}` → `orders-cancelled` = 1, both DLQs = 0, `processed_events` has `d9a9428b-…`. - Both lambdas processed in ~2s.
Final diff , what the agent changed

No file changes, the agent left the workspace untouched.

Verifier output , the proof behind the reward
tests/test.sh
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/zF6ciT2hIJpLuGz5p4N3B/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: 89 warnings
  /root/.cache/uv/archive-v0/zF6ciT2hIJpLuGz5p4N3B/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, 89 warnings in 22.05s ==================

Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_67cb63173a1c4651. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.

Trial trial_67cb63173a1c4651 · verifier authoritative; classifier explanatory.