SyncValsverifier → artifact → classifier → verdict
SyncVals · Trajectory

sfn-saga-compensation-orchestrator

claude-code claude-opus-4-7 ✓ resolved HARNESS_ERROR ↑ 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
HARNESS_ERRORInfrastructure failure, the agent never ran properly. Not a signal about agent or task.
SubtypeClassification Failed
EvidenceClaude Code classification failed: (classifier unavailable for this trial)
Root causeCould not analyze trial with Claude Code
RecommendationReview trial manually or check authentication
Trajectory
Tool-by-tool agent trajectory
34 tool calls · 4 tool types · 42 steps
so the order service keeps leaving garbage rows. inventory gets reserved, payment call times out, we retry, now inventory is double-reserved because the retry worked the second time. same deal when shipping fails after payment , we'd just charge the customer and leave them with no shipment. the ops slack is basically me apologizing at this point. we need a real saga. if any step fails, **undo** the earlier steps in reverse order. no half-states. step functions runs it, localstack , `http://localstack:4566`, creds already exported (`AWS_ACCESS_KEY_ID=test`, same for secret, region `us-east-1`). `aws`, `python3`, `boto3`, `jq`, `zip`. build from zero. shape of it: - a state machine runs three forward steps: **reserve inventory**, **charge payment**, **create shipment**. each step is its own lambda. each step writes to its own ddb table. - if any step fails, the saga fires compensations **in reverse order** of what actually ran. if reserve succeeded but charge failed, you refund (no-op, nothing was charged) and release. if charge succeeded but shipment failed, you refund payment and release inventory. if reserve failed, nothing to undo. - compensations are their own lambdas too. they must be idempotent , if state-function retries a compensation, running it twice should not corrupt the table. - each forward step gets an idempotency key from the input so a lambda retry inside a single step doesn't double-decrement inventory. - payment calls a "gateway" that needs an api key. store the key in secrets manager , customer-managed kms key encrypts the secret. the payment lambda reads the secret, nobody else should be able to. - on terminal state (completed or failed-compensated), the saga publishes an event to a custom event bus. a notifier lambda sits on that bus and writes one log line starting with `saga-terminal:`. to actually prove compensations work, the charge-payment lambda accepts a `force_failure` flag in its input , when true, it raises. the verifier uses this to drive a failure run and assert the state snaps back. done looks like this. two executions: **happy path** , `start-execution` with `{"order_id": "o-1", "sku": "x", "qty": 2, "amount": 100, "idempotency_key": "k-1"}`: - sm reaches `SUCCEEDED` within 60s - inventory row for sku `x` reflects `reserved_qty = 2` - payments row exists with status `CAPTURED` - shipments row exists - notifier log has `saga-terminal:OrderCompleted` **failure path** , same input but `"force_failure": true, "order_id": "o-2", "idempotency_key": "k-2"`: - sm still reaches `SUCCEEDED` (saga handles it gracefully, not a hard abort) - inventory reserved_qty back to 0 (compensation ran) - payments row is absent or `REFUNDED` (nothing was charged since charge failed, so a no-op refund is also ok) - shipments row does **not** exist - notifier log has `saga-terminal:OrderFailed` least privilege. the verifier reads policy docs directly. each lambda role only gets its own table. saga role can't wildcard on lambda invoke. payment role is the only one that reads the secret. notifier role has nothing but eventbridge + logs. no `*` on kms anywhere. names the verifier grabs by hand, use them exactly: | thing | name | |-|-| | kms alias | `alias/saga-cmk` | | secret | `payment/gateway-apikey` | | inventory table | `inventory` (pk `sku` / String) | | payments table | `payments` (pk `payment_id` / String) | | shipments table | `shipments` (pk `shipment_id` / String) | | idempotency table | `saga-idempotency` (pk `idempotency_key` / String, ttl enabled) | | reserve lambda | `reserve-inventory` | | charge lambda | `charge-payment` | | ship lambda | `create-shipment` | | release compensation | `release-inventory` | | refund compensation | `refund-payment` | | notifier lambda | `saga-notifier` | | reserve role | `reserve-inventory-role` | | charge role | `charge-payment-role` | | ship role | `create-shipment-role` | | release role | `release-inventory-role` | | refund role | `refund-payment-role` | | notifier role | `saga-notifier-role` | | state machine | `order-saga` | | state machine role | `order-saga-role` | | event bus | `saga-events` | | event rule | `on-saga-terminal` | eventbridge detail shape: source `order.saga`, detail-type is one of `OrderCompleted` or `OrderFailed`. the rule matches both and routes to the notifier lambda. python3.11 for all lambdas. localstack community 4.13.1. the "payment gateway" is fake , charge-payment reads the secret, logs that it did, and marks the payments row CAPTURED. no actual network call.

Check environment

shell
ls /app && echo "---" && which aws python3 jq zip && echo "---" && aws --version && python3 --version
output
---
/usr/local/bin/aws
/opt/venv/bin/python3
/usr/bin/jq
/usr/bin/zip
---
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24
Python 3.12.3

[stdout]
---
/usr/local/bin/aws
/opt/venv/bin/python3
/usr/bin/jq
/usr/bin/zip
---
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24
Python 3.12.3

Verify localstack connectivity

shell
export AWS_ENDPOINT_URL=http://localstack:4566 && aws --endpoint-url=http://localstack:4566 sts get-caller-identity && echo "---" && aws --endpoint-url=http://localstack:4566 dynamodb list-tables
output
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}
---
{
    "TableNames": []
}

[stdout]
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}
---
{
    "TableNames": []
}

Check existing resources

shell
aws --endpoint-url=http://localstack:4566 stepfunctions list-state-machines && echo "---" && aws --endpoint-url=http://localstack:4566 lambda list-functions --query 'Functions[].FunctionName'
output
{
    "stateMachines": []
}
---
[]

[stdout]
{
    "stateMachines": []
}
---
[]
I'll build this from scratch. Let me plan the structure and then build all components.

Set up directory structure

shell
mkdir -p /app/lambdas /app/policies /app/build && ls /app
output
build
lambdas
policies

[stdout]
build
lambdas
policies
write · /app/lambdas/reserve_inventory.py
import time
import boto3
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")


def handler(event, context):
    sku = event["sku"]
    qty = int(event["qty"])
    key = event["idempotency_key"]
    op_key = f"{key}#reserve"

    try:
        ddb.put_item(
            TableName="saga-idempotency",
            Item={
                "idempotency_key": {"S": op_key},
                "ttl": {"N": str(int(time.time()) + 86400)},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"reserve-inventory: already done for {op_key}, skipping")
            return {**event, "reserved": True, "qty_reserved": qty}
        raise

    ddb.update_item(
        TableName="inventory",
        Key={"sku": {"S": sku}},
        UpdateExpression="ADD reserved_qty :q",
        ExpressionAttributeValues={":q": {"N": str(qty)}},
    )
    print(f"reserve-inventory: reserved {qty} of {sku}")
    return {**event, "reserved": True, "qty_reserved": qty}
write · /app/lambdas/charge_payment.py
import time
import boto3
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")
sm = boto3.client("secretsmanager")


def handler(event, context):
    if event.get("force_failure"):
        raise RuntimeError("forced failure for compensation test")

    order_id = event["order_id"]
    amount = event["amount"]
    key = event["idempotency_key"]
    op_key = f"{key}#charge"
    payment_id = f"pay-{order_id}"

    try:
        ddb.put_item(
            TableName="saga-idempotency",
            Item={
                "idempotency_key": {"S": op_key},
                "ttl": {"N": str(int(time.time()) + 86400)},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"charge-payment: already done for {op_key}, skipping")
            return {**event, "payment_id": payment_id, "charged": True}
        raise

    secret = sm.get_secret_value(SecretId="payment/gateway-apikey")
    api_key = secret["SecretString"]
    print(f"charge-payment: read gateway api key (len={len(api_key)}) and called fake gateway")

    ddb.put_item(
        TableName="payments",
        Item={
            "payment_id": {"S": payment_id},
            "order_id": {"S": order_id},
            "amount": {"N": str(amount)},
            "status": {"S": "CAPTURED"},
        },
    )
    print(f"charge-payment: captured {amount} for {payment_id}")
    return {**event, "payment_id": payment_id, "charged": True}
write · /app/lambdas/create_shipment.py
import time
import boto3
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")


def handler(event, context):
    order_id = event["order_id"]
    key = event["idempotency_key"]
    op_key = f"{key}#ship"
    shipment_id = f"ship-{order_id}"

    try:
        ddb.put_item(
            TableName="saga-idempotency",
            Item={
                "idempotency_key": {"S": op_key},
                "ttl": {"N": str(int(time.time()) + 86400)},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"create-shipment: already done for {op_key}, skipping")
            return {**event, "shipment_id": shipment_id, "shipped": True}
        raise

    ddb.put_item(
        TableName="shipments",
        Item={
            "shipment_id": {"S": shipment_id},
            "order_id": {"S": order_id},
            "status": {"S": "CREATED"},
        },
    )
    print(f"create-shipment: created {shipment_id}")
    return {**event, "shipment_id": shipment_id, "shipped": True}
write · /app/lambdas/release_inventory.py
import time
import boto3
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")


def handler(event, context):
    sku = event["sku"]
    qty = int(event["qty"])
    key = event["idempotency_key"]
    op_key = f"{key}#release"

    try:
        ddb.put_item(
            TableName="saga-idempotency",
            Item={
                "idempotency_key": {"S": op_key},
                "ttl": {"N": str(int(time.time()) + 86400)},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"release-inventory: already done for {op_key}, skipping")
            return {**event, "released": True}
        raise

    ddb.update_item(
        TableName="inventory",
        Key={"sku": {"S": sku}},
        UpdateExpression="ADD reserved_qty :q",
        ExpressionAttributeValues={":q": {"N": str(-qty)}},
    )
    print(f"release-inventory: released {qty} of {sku}")
    return {**event, "released": True}
write · /app/lambdas/refund_payment.py
import time
import boto3
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")


def handler(event, context):
    order_id = event["order_id"]
    key = event["idempotency_key"]
    op_key = f"{key}#refund"
    payment_id = f"pay-{order_id}"

    try:
        ddb.put_item(
            TableName="saga-idempotency",
            Item={
                "idempotency_key": {"S": op_key},
                "ttl": {"N": str(int(time.time()) + 86400)},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"refund-payment: already done for {op_key}, skipping")
            return {**event, "refunded": True}
        raise

    try:
        ddb.update_item(
            TableName="payments",
            Key={"payment_id": {"S": payment_id}},
            UpdateExpression="SET #s = :r",
            ExpressionAttributeNames={"#s": "status"},
            ExpressionAttributeValues={":r": {"S": "REFUNDED"}},
            ConditionExpression="attribute_exists(payment_id)",
        )
        print(f"refund-payment: refunded {payment_id}")
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            print(f"refund-payment: no payment row for {payment_id} (no-op, charge never wrote)")
        else:
            raise

    return {**event, "refunded": True}
ToolSearch
{
  "query": "select:TodoWrite",
  "max_results": 1
}

, output, 
{"type": "tool_reference", "tool_name": "TodoWrite"}
  • Write notifier lambda + state machine + IAM policies
  • Build deploy script (KMS, secret, DDB tables, IAM, lambdas, EventBridge, state machine)
  • Run deploy and verify happy path execution
  • Verify failure path with force_failure (compensations + OrderFailed event)
write · /app/lambdas/saga_notifier.py
def handler(event, context):
    detail_type = event.get("detail-type", "Unknown")
    detail = event.get("detail", {})
    print(f"saga-terminal:{detail_type} order_id={detail.get('order_id')} status={detail.get('status')}")
    return {"ok": True}
write · /app/state-machine.json
{
  "Comment": "Order saga: reserve inventory -> charge payment -> create shipment, with compensations in reverse order on failure.",
  "StartAt": "ReserveInventory",
  "States": {
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "reserve-inventory",
        "Payload.$": "$"
      },
      "ResultSelector": {"out.$": "$.Payload"},
      "ResultPath": "$.reserve",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "ResultPath": "$.error",
          "Next": "PublishFailedNoComp"
        }
      ],
      "Next": "ChargePayment"
    },
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "charge-payment",
        "Payload.$": "$"
      },
      "ResultSelector": {"out.$": "$.Payload"},
      "ResultPath": "$.charge",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "ResultPath": "$.error",
          "Next": "RefundAfterChargeFail"
        }
      ],
      "Next": "CreateShipment"
    },
    "CreateShipment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "create-shipment",
        "Payload.$": "$"
      },
      "ResultSelector": {"out.$": "$.Payload"},
      "ResultPath": "$.ship",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "ResultPath": "$.error",
          "Next": "RefundAfterShipFail"
        }
      ],
      "Next": "PublishCompleted"
    },
    "RefundAfterShipFail": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "refund-payment",
        "Payload.$": "$"
      },
      "ResultPath": "$.refundComp",
      "Next": "ReleaseAfterShipFail"
    },
    "ReleaseAfterShipFail": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "release-inventory",
        "Payload.$": "$"
      },
      "ResultPath": "$.releaseComp",
      "Next": "PublishFailed"
    },
    "RefundAfterChargeFail": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "refund-payment",
        "Payload.$": "$"
      },
      "ResultPath": "$.refundComp",
      "Next": "ReleaseAfterChargeFail"
    },
    "ReleaseAfterChargeFail": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "release-inventory",
        "Payload.$": "$"
      },
      "ResultPath": "$.releaseComp",
      "Next": "PublishFailed"
    },
    "PublishCompleted": {
      "Type": "Task",
      "Resource": "arn:aws:states:::events:putEvents",
      "Parameters": {
        "Entries": [
          {
            "Source": "order.saga",
            "DetailType": "OrderCompleted",
            "EventBusName": "saga-events",
            "Detail": {
              "order_id.$": "$.order_id",
              "status": "completed"
            }
          }
        ]
      },
      "End": true
    },
    "PublishFailed": {
      "Type": "Task",
      "Resource": "arn:aws:states:::events:putEvents",
      "Parameters": {
        "Entries": [
          {
            "Source": "order.saga",
            "DetailType": "OrderFailed",
            "EventBusName": "saga-events",
            "Detail": {
              "order_id.$": "$.order_id",
              "status": "failed-compensated"
            }
          }
        ]
      },
      "End": true
    },
    "PublishFailedNoComp": {
      "Type": "Task",
      "Resource": "arn:aws:states:::events:putEvents",
      "Parameters": {
        "Entries": [
          {
            "Source": "order.saga",
            "DetailType": "OrderFailed",
            "EventBusName": "saga-events",
            "Detail": {
              "order_id.$": "$.order_id",
              "status": "failed-no-comp"
            }
          }
        ]
      },
      "End": true
    }
  }
}
Now the IAM trust + per-role policy docs:
write · /app/policies/trust-lambda.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Service": "lambda.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }
  ]
}
write · /app/policies/trust-states.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Service": "states.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }
  ]
}
write · /app/policies/reserve-inventory-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InventoryWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/inventory"
    },
    {
      "Sid": "IdempotencyWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/release-inventory-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InventoryWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/inventory"
    },
    {
      "Sid": "IdempotencyWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/charge-payment-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PaymentsWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/payments"
    },
    {
      "Sid": "IdempotencyWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
    },
    {
      "Sid": "ReadGatewaySecret",
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "__SECRET_ARN__"
    },
    {
      "Sid": "DecryptWithCmk",
      "Effect": "Allow",
      "Action": ["kms:Decrypt"],
      "Resource": "__KMS_KEY_ARN__"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/refund-payment-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PaymentsUpdate",
      "Effect": "Allow",
      "Action": ["dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/payments"
    },
    {
      "Sid": "IdempotencyWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/create-shipment-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ShipmentsWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/shipments"
    },
    {
      "Sid": "IdempotencyWrite",
      "Effect": "Allow",
      "Action": ["dynamodb:PutItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
    },
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/saga-notifier-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:us-east-1:000000000000:*"
    }
  ]
}
write · /app/policies/order-saga-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InvokeSagaSteps",
      "Effect": "Allow",
      "Action": ["lambda:InvokeFunction"],
      "Resource": [
        "arn:aws:lambda:us-east-1:000000000000:function:reserve-inventory",
        "arn:aws:lambda:us-east-1:000000000000:function:charge-payment",
        "arn:aws:lambda:us-east-1:000000000000:function:create-shipment",
        "arn:aws:lambda:us-east-1:000000000000:function:release-inventory",
        "arn:aws:lambda:us-east-1:000000000000:function:refund-payment"
      ]
    },
    {
      "Sid": "PublishSagaEvents",
      "Effect": "Allow",
      "Action": ["events:PutEvents"],
      "Resource": "arn:aws:events:us-east-1:000000000000:event-bus/saga-events"
    }
  ]
}
write · /app/policies/secret-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "OnlyChargePaymentReadsSecret",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::000000000000:role/charge-payment-role"},
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*"
    }
  ]
}
write · /app/deploy.sh
#!/usr/bin/env bash
set -euo pipefail

ENDPOINT="http://localstack:4566"
AWS="aws --endpoint-url=$ENDPOINT --region us-east-1"
ACCOUNT="000000000000"
REGION="us-east-1"

cd /app

echo "==> 1. KMS customer-managed key + alias"
KEY_META=$($AWS kms list-aliases --query "Aliases[?AliasName=='alias/saga-cmk'].TargetKeyId" --output text)
if [ -z "$KEY_META" ] || [ "$KEY_META" = "None" ]; then
  KEY_ID=$($AWS kms create-key --description "Saga CMK for payment gateway secret" --query 'KeyMetadata.KeyId' --output text)
  $AWS kms create-alias --alias-name alias/saga-cmk --target-key-id "$KEY_ID"
else
  KEY_ID="$KEY_META"
fi
KEY_ARN="arn:aws:kms:${REGION}:${ACCOUNT}:key/${KEY_ID}"
echo "    key_arn=$KEY_ARN"

echo "==> 2. Secret encrypted with CMK"
if ! $AWS secretsmanager describe-secret --secret-id payment/gateway-apikey >/dev/null 2>&1; then
  $AWS secretsmanager create-secret \
    --name payment/gateway-apikey \
    --secret-string "fake-gateway-key-$(date +%s)" \
    --kms-key-id "alias/saga-cmk" >/dev/null
else
  $AWS secretsmanager update-secret \
    --secret-id payment/gateway-apikey \
    --kms-key-id "alias/saga-cmk" >/dev/null
fi
SECRET_ARN=$($AWS secretsmanager describe-secret --secret-id payment/gateway-apikey --query ARN --output text)
echo "    secret_arn=$SECRET_ARN"

echo "==> 3. DynamoDB tables"
create_table() {
  local name="$1" pk="$2"
  if ! $AWS dynamodb describe-table --table-name "$name" >/dev/null 2>&1; then
    $AWS dynamodb create-table \
      --table-name "$name" \
      --attribute-definitions AttributeName=$pk,AttributeType=S \
      --key-schema AttributeName=$pk,KeyType=HASH \
      --billing-mode PAY_PER_REQUEST >/dev/null
    echo "    created $name"
  else
    echo "    $name already exists"
  fi
}
create_table inventory sku
create_table payments payment_id
create_table shipments shipment_id
create_table saga-idempotency idempotency_key

# Enable TTL on idempotency
TTL_STATUS=$($AWS dynamodb describe-time-to-live --table-name saga-idempotency --query 'TimeToLiveDescription.TimeToLiveStatus' --output text 2>/dev/null || echo "DISABLED")
if [ "$TTL_STATUS" != "ENABLED" ]; then
  $AWS dynamodb update-time-to-live \
    --table-name saga-idempotency \
    --time-to-live-specification "Enabled=true,AttributeName=ttl" >/dev/null
  echo "    enabled TTL on saga-idempotency"
fi

echo "==> 4. IAM roles + inline policies"
ensure_role() {
  local role="$1" trust_file="$2"
  if ! $AWS iam get-role --role-name "$role" >/dev/null 2>&1; then
    $AWS iam create-role --role-name "$role" --assume-role-policy-document "file://$trust_file" >/dev/null
    echo "    created role $role"
  fi
}
attach_inline() {
  local role="$1" policy_name="$2" policy_file="$3"
  $AWS iam put-role-policy --role-name "$role" --policy-name "$policy_name" --policy-document "file://$policy_file" >/dev/null
}

# Render charge-payment policy with secret + kms ARNs
mkdir -p /app/build
sed -e "s|__SECRET_ARN__|${SECRET_ARN}|g" \
    -e "s|__KMS_KEY_ARN__|${KEY_ARN}|g" \
    /app/policies/charge-payment-policy.json > /app/build/charge-payment-policy.rendered.json

ensure_role reserve-inventory-role  /app/policies/trust-lambda.json
ensure_role charge-payment-role     /app/policies/trust-lambda.json
ensure_role create-shipment-role    /app/policies/trust-lambda.json
ensure_role release-inventory-role  /app/policies/trust-lambda.json
ensure_role refund-payment-role     /app/policies/trust-lambda.json
ensure_role saga-notifier-role      /app/policies/trust-lambda.json
ensure_role order-saga-role         /app/policies/trust-states.json

attach_inline reserve-inventory-role  reserve-inventory-policy  /app/policies/reserve-inventory-policy.json
attach_inline charge-payment-role     charge-payment-policy     /app/build/charge-payment-policy.rendered.json
attach_inline create-shipment-role    create-shipment-policy    /app/policies/create-shipment-policy.json
attach_inline release-inventory-role  release-inventory-policy  /app/policies/release-inventory-policy.json
attach_inline refund-payment-role     refund-payment-policy     /app/policies/refund-payment-policy.json
attach_inline saga-notifier-role      saga-notifier-policy      /app/policies/saga-notifier-policy.json
attach_inline order-saga-role         order-saga-policy         /app/policies/order-saga-policy.json

echo "==> 5. Resource policy on secret (only charge-payment-role can read)"
$AWS secretsmanager put-resource-policy \
  --secret-id payment/gateway-apikey \
  --resource-policy "file:///app/policies/secret-policy.json" >/dev/null

echo "==> 6. Package + create lambdas"
package_lambda() {
  local src="$1" zip="$2"
  ( cd /app/lambdas && zip -q -j "/app/build/${zip}" "${src}" )
}
package_lambda reserve_inventory.py reserve_inventory.zip
package_lambda charge_payment.py    charge_payment.zip
package_lambda create_shipment.py   create_shipment.zip
package_lambda release_inventory.py release_inventory.zip
package_lambda refund_payment.py    refund_payment.zip
package_lambda saga_notifier.py     saga_notifier.zip

create_or_update_lambda() {
  local name="$1" handler="$2" role="$3" zip="$4"
  local role_arn="arn:aws:iam::${ACCOUNT}:role/${role}"
  if $AWS lambda get-function --function-name "$name" >/dev/null 2>&1; then
    $AWS lambda update-function-code --function-name "$name" --zip-file "fileb:///app/build/${zip}" >/dev/null
  else
    $AWS lambda create-function \
      --function-name "$name" \
      --runtime python3.11 \
      --handler "$handler" \
      --role "$role_arn" \
      --zip-file "fileb:///app/build/${zip}" \
      --timeout 30 >/dev/null
    echo "    created lambda $name"
  fi
  # wait for active
  for _ in $(seq 1 30); do
    state=$($AWS lambda get-function --function-name "$name" --query 'Configuration.State' --output text 2>/dev/null || echo "Pending")
    [ "$state" = "Active" ] && break
    sleep 1
  done
}

create_or_update_lambda reserve-inventory  reserve_inventory.handler  reserve-inventory-role  reserve_inventory.zip
create_or_update_lambda charge-payment     charge_payment.handler     charge-payment-role     charge_payment.zip
create_or_update_lambda create-shipment    create_shipment.handler    create-shipment-role    create_shipment.zip
create_or_update_lambda release-inventory  release_inventory.handler  release-inventory-role  release_inventory.zip
create_or_update_lambda refund-payment     refund_payment.handler     refund-payment-role     refund_payment.zip
create_or_update_lambda saga-notifier      saga_notifier.handler      saga-notifier-role      saga_notifier.zip

echo "==> 7. EventBridge custom bus + rule + target"
if ! $AWS events describe-event-bus --name saga-events >/dev/null 2>&1; then
  $AWS events create-event-bus --name saga-events >/dev/null
  echo "    created event bus saga-events"
fi

$AWS events put-rule \
  --name on-saga-terminal \
  --event-bus-name saga-events \
  --event-pattern '{"source":["order.saga"],"detail-type":["OrderCompleted","OrderFailed"]}' \
  --state ENABLED >/dev/null

RULE_ARN=$($AWS events describe-rule --name on-saga-terminal --event-bus-name saga-events --query Arn --output text)
NOTIFIER_ARN="arn:aws:lambda:${REGION}:${ACCOUNT}:function:saga-notifier"

# Allow EventBridge to invoke the notifier
$AWS lambda remove-permission --function-name saga-notifier --statement-id allow-eventbridge >/dev/null 2>&1 || true
$AWS lambda add-permission \
  --function-name saga-notifier \
  --statement-id allow-eventbridge \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn "$RULE_ARN" >/dev/null

$AWS events put-targets \
  --rule on-saga-terminal \
  --event-bus-name saga-events \
  --targets "Id=1,Arn=${NOTIFIER_ARN}" >/dev/null

echo "==> 8. State machine"
SM_ROLE_ARN="arn:aws:iam::${ACCOUNT}:role/order-saga-role"
SM_ARN="arn:aws:states:${REGION}:${ACCOUNT}:stateMachine:order-saga"
if $AWS stepfunctions describe-state-machine --state-machine-arn "$SM_ARN" >/dev/null 2>&1; then
  $AWS stepfunctions update-state-machine \
    --state-machine-arn "$SM_ARN" \
    --definition "file:///app/state-machine.json" \
    --role-arn "$SM_ROLE_ARN" >/dev/null
else
  $AWS stepfunctions create-state-machine \
    --name order-saga \
    --type STANDARD \
    --definition "file:///app/state-machine.json" \
    --role-arn "$SM_ROLE_ARN" >/dev/null
  echo "    created state machine order-saga"
fi

echo "==> deploy complete"

Run deploy script

shell
chmod +x /app/deploy.sh && bash /app/deploy.sh
output
==> 1. KMS customer-managed key + alias
    key_arn=arn:aws:kms:us-east-1:000000000000:key/b8faf67c-f83a-494a-b022-93f6e5812215
==> 2. Secret encrypted with CMK
    secret_arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:payment/gateway-apikey-Aquutq
==> 3. DynamoDB tables
    created inventory
    created payments
    created shipments
    created saga-idempotency
    enabled TTL on saga-idempotency
==> 4. IAM roles + inline policies
    created role reserve-inventory-role
    created role charge-payment-role
    created role create-shipment-role
    created role release-inventory-role
    created role refund-payment-role
    created role saga-notifier-role
    created role order-saga-role
==> 5. Resource policy on secret (only charge-payment-role can read)
==> 6. Package + create lambdas
    created lambda reserve-inventory
    created lambda charge-payment
    created lambda create-shipment
    created lambda release-inventory
    created lambda refund-payment
    created lambda saga-notifier
==> 7. EventBridge custom bus + rule + target
    created event bus saga-events
==> 8. State machine
    created state machine order-saga
==> deploy complete

[stdout]
==> 1. KMS customer-managed key + alias
    key_arn=arn:aws:kms:us-east-1:000000000000:key/b8faf67c-f83a-494a-b022-93f6e5812215
==> 2. Secret encrypted with CMK
    secret_arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:payment/gateway-apikey-Aquutq
==> 3. DynamoDB tables
    created inventory
    created payments
    created shipments
    created saga-idempotency
    enabled TTL on saga-idempotency
==> 4. IAM roles + inline policies
    created role reserve-inventory-role
    created role charge-payment-role
    created role create-shipment-role
    created role release-inventory-role
    created role refund-payment-role
    created role saga-notifier-role
    created role order-saga-role
==> 5. Resource policy on secret (only charge-payment-role can read)
==> 6. Package + create lambdas
    created lambda reserve-inventory
    created lambda charge-payment
    created lambda create-shipment
    created lambda release-inventory
    created lambda refund-payment
    created lambda saga-notifier
==> 7. EventBridge custom bus + rule + target
    created event bus saga-events
==> 8. State machine
    created state machine order-saga
==> deploy complete
  • Write notifier lambda + state machine + IAM policies
  • Build deploy script (KMS, secret, DDB tables, IAM, lambdas, EventBridge, state machine)
  • Run deploy and verify happy path execution
  • Verify failure path with force_failure (compensations + OrderFailed event)

Start happy path execution and wait

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
EXEC_ARN=$($AWS stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:000000000000:stateMachine:order-saga \
  --name happy-1-$(date +%s) \
  --input '{"order_id":"o-1","sku":"x","qty":2,"amount":100,"idempotency_key":"k-1"}' \
  --query executionArn --output text) && echo "exec=$EXEC_ARN" && \
for i in $(seq 1 30); do
  STATUS=$($AWS stepfunctions describe-execution --execution-arn "$EXEC_ARN" --query status --output text)
  echo "  iter=$i status=$STATUS"
  [ "$STATUS" != "RUNNING" ] && break
  sleep 2
done && \
$AWS stepfunctions describe-execution --execution-arn "$EXEC_ARN" --query 'status' --output text
output
exec=arn:aws:states:us-east-1:000000000000:execution:order-saga:happy-1-1776914590
  iter=1 status=RUNNING
  iter=2 status=RUNNING
  iter=3 status=SUCCEEDED
SUCCEEDED

[stdout]
exec=arn:aws:states:us-east-1:000000000000:execution:order-saga:happy-1-1776914590
  iter=1 status=RUNNING
  iter=2 status=RUNNING
  iter=3 status=SUCCEEDED
SUCCEEDED

Inspect DDB rows after happy path

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
echo "--- inventory[x]" && $AWS dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}' && \
echo "--- payments[pay-o-1]" && $AWS dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-1"}}' && \
echo "--- shipments[ship-o-1]" && $AWS dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-1"}}'
output
--- inventory[x]
{
    "Item": {
        "sku": {
            "S": "x"
        },
        "reserved_qty": {
            "N": "2"
        }
    }
}
--- payments[pay-o-1]
{
    "Item": {
        "amount": {
            "N": "100"
        },
        "order_id": {
            "S": "o-1"
        },
        "payment_id": {
            "S": "pay-o-1"
        },
        "status": {
            "S": "CAPTURED"
        }
    }
}
--- shipments[ship-o-1]
{
    "Item": {
        "shipment_id": {
            "S": "ship-o-1"
        },
        "order_id": {
            "S": "o-1"
        },
        "status": {
            "S": "CREATED"
        }
    }
}

[stdout]
--- inventory[x]
{
    "Item": {
        "sku": {
            "S": "x"
        },
        "reserved_qty": {
            "N": "2"
        }
    }
}
--- payments[pay-o-1]
{
    "Item": {
        "amount": {
            "N": "100"
        },
        "order_id": {
            "S": "o-1"
        },
        "payment_id": {
            "S": "pay-o-1"
        },
        "status": {
            "S": "CAPTURED"
        }
    }
}
--- shipments[ship-o-1]
{
    "Item": {
        "shipment_id": {
            "S": "ship-o-1"
        },
        "order_id": {
            "S": "o-1"
        },
        "status": {
            "S": "CREATED"
        }
    }
}

Check notifier logs for saga-terminal

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
LOG_GROUP="/aws/lambda/saga-notifier" && \
sleep 3 && \
STREAMS=$($AWS logs describe-log-streams --log-group-name "$LOG_GROUP" --order-by LastEventTime --descending --max-items 5 --query 'logStreams[].logStreamName' --output text 2>/dev/null) && \
echo "streams: $STREAMS" && \
for s in $STREAMS; do
  echo "--- $s"
  $AWS logs get-log-events --log-group-name "$LOG_GROUP" --log-stream-name "$s" --query 'events[].message' --output text
done
output
streams: 2026/04/23/[$LATEST]64187fb6544ebe61c0d957bf3e8e17f8
--- 2026/04/23/[$LATEST]64187fb6544ebe61c0d957bf3e8e17f8
START RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b Version: $LATEST
	saga-terminal:OrderCompleted order_id=o-1 status=completed
	END RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b
	REPORT RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b	Duration: 9.75 ms	Billed Duration: 10 ms	Memory Size: 128 MB	Max Memory Used: 128 MB

[stdout]
streams: 2026/04/23/[$LATEST]64187fb6544ebe61c0d957bf3e8e17f8
--- 2026/04/23/[$LATEST]64187fb6544ebe61c0d957bf3e8e17f8
START RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b Version: $LATEST
	saga-terminal:OrderCompleted order_id=o-1 status=completed
	END RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b
	REPORT RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b	Duration: 9.75 ms	Billed Duration: 10 ms	Memory Size: 128 MB	Max Memory Used: 128 MB
Happy path works end-to-end. Now the failure path.
  • Write notifier lambda + state machine + IAM policies
  • Build deploy script (KMS, secret, DDB tables, IAM, lambdas, EventBridge, state machine)
  • Run deploy and verify happy path execution
  • Verify failure path with force_failure (compensations + OrderFailed event)

Failure path execution + DDB inspection

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
EXEC_ARN=$($AWS stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:000000000000:stateMachine:order-saga \
  --name fail-1-$(date +%s) \
  --input '{"order_id":"o-2","sku":"x","qty":2,"amount":100,"idempotency_key":"k-2","force_failure":true}' \
  --query executionArn --output text) && echo "exec=$EXEC_ARN" && \
for i in $(seq 1 30); do
  STATUS=$($AWS stepfunctions describe-execution --execution-arn "$EXEC_ARN" --query status --output text)
  echo "  iter=$i status=$STATUS"
  [ "$STATUS" != "RUNNING" ] && break
  sleep 2
done && \
echo "--- final status:" && \
$AWS stepfunctions describe-execution --execution-arn "$EXEC_ARN" --query 'status' --output text && \
echo "--- inventory[x]" && $AWS dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}' && \
echo "--- payments[pay-o-2]" && $AWS dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-2"}}' && \
echo "--- shipments[ship-o-2]" && $AWS dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-2"}}'
output
exec=arn:aws:states:us-east-1:000000000000:execution:order-saga:fail-1-1776914635
  iter=1 status=RUNNING
  iter=2 status=RUNNING
  iter=3 status=SUCCEEDED
--- final status:
SUCCEEDED
--- inventory[x]
{
    "Item": {
        "sku": {
            "S": "x"
        },
        "reserved_qty": {
            "N": "2"
        }
    }
}
--- payments[pay-o-2]
--- shipments[ship-o-2]

[stdout]
exec=arn:aws:states:us-east-1:000000000000:execution:order-saga:fail-1-1776914635
  iter=1 status=RUNNING
  iter=2 status=RUNNING
  iter=3 status=SUCCEEDED
--- final status:
SUCCEEDED
--- inventory[x]
{
    "Item": {
        "sku": {
            "S": "x"
        },
        "reserved_qty": {
            "N": "2"
        }
    }
}
--- payments[pay-o-2]
--- shipments[ship-o-2]
Failure path: state machine `SUCCEEDED`, no payment row, no shipment row, inventory rolled back its delta (reserved 2 then released 2; remaining 2 is from the prior happy-path run on same sku). Let me confirm the compensations actually fired and check the notifier log.

Confirm compensations + notifier OrderFailed

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
echo "--- saga-idempotency rows (compensation evidence)" && \
$AWS dynamodb scan --table-name saga-idempotency --query 'Items[].idempotency_key.S' --output text | tr '\t' '\n' | sort && \
echo "--- notifier log (most recent stream)" && \
sleep 2 && \
LATEST=$($AWS logs describe-log-streams --log-group-name /aws/lambda/saga-notifier --order-by LastEventTime --descending --max-items 1 --query 'logStreams[0].logStreamName' --output text) && \
echo "stream=$LATEST" && \
$AWS logs get-log-events --log-group-name /aws/lambda/saga-notifier --log-stream-name "$LATEST" --query 'events[].message' --output text
output
Exit code 254
--- saga-idempotency rows (compensation evidence)
k-1#charge
k-1#reserve
k-1#ship
k-2#refund
k-2#release
k-2#reserve
--- notifier log (most recent stream)
stream=2026/04/23/[$LATEST]08c5a4a80de9e3c75f4bbf1c9bba74e1
None

aws: [ERROR]: An error occurred (ResourceNotFoundException) when calling the GetLogEvents operation: The specified log stream does not exist.

[error] tool reported failure
Compensation evidence confirmed via idempotency table. The log stream lookup tripped over the `$LATEST` shell expansion.

Filter all saga-terminal log lines

shell
AWS="aws --endpoint-url=http://localstack:4566 --region us-east-1" && \
$AWS logs filter-log-events --log-group-name /aws/lambda/saga-notifier --filter-pattern '"saga-terminal"' --query 'events[].message' --output text
output
START RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b Version: $LATEST
	saga-terminal:OrderCompleted order_id=o-1 status=completed
	END RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b
	REPORT RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b	Duration: 9.75 ms	Billed Duration: 10 ms	Memory Size: 128 MB	Max Memory Used: 128 MB	
	START RequestId: 36adb276-3bf0-4338-8640-a004fbba6548 Version: $LATEST
	saga-terminal:OrderFailed order_id=o-2 status=failed-compensated
	END RequestId: 36adb276-3bf0-4338-8640-a004fbba6548
	REPORT RequestId: 36adb276-3bf0-4338-8640-a004fbba6548	Duration: 8.23 ms	Billed Duration: 9 ms	Memory Size: 128 MB	Max Memory Used: 128 MB

[stdout]
START RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b Version: $LATEST
	saga-terminal:OrderCompleted order_id=o-1 status=completed
	END RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b
	REPORT RequestId: 2186f5c2-d223-4fd5-a647-339407aca54b	Duration: 9.75 ms	Billed Duration: 10 ms	Memory Size: 128 MB	Max Memory Used: 128 MB	
	START RequestId: 36adb276-3bf0-4338-8640-a004fbba6548 Version: $LATEST
	saga-terminal:OrderFailed order_id=o-2 status=failed-compensated
	END RequestId: 36adb276-3bf0-4338-8640-a004fbba6548
	REPORT RequestId: 36adb276-3bf0-4338-8640-a004fbba6548	Duration: 8.23 ms	Billed Duration: 9 ms	Memory Size: 128 MB	Max Memory Used: 128 MB
Both terminal events landed in the notifier log: - `saga-terminal:OrderCompleted order_id=o-1 status=completed` - `saga-terminal:OrderFailed order_id=o-2 status=failed-compensated`
  • Write notifier lambda + state machine + IAM policies
  • Build deploy script (KMS, secret, DDB tables, IAM, lambdas, EventBridge, state machine)
  • Run deploy and verify happy path execution
  • Verify failure path with force_failure (compensations + OrderFailed event)
Done. Saga is wired end-to-end on LocalStack and both proof executions match the spec. **What got built** - `/app/lambdas/` , 6 Python 3.11 lambdas (3 forward steps, 2 compensations, 1 notifier). All forward and compensation lambdas use a conditional-write idempotency pattern against `saga-idempotency` keyed by `${idempotency_key}#${step}` so retries never double-apply. - `/app/state-machine.json` , Step Functions definition. Forward chain: `ReserveInventory → ChargePayment → CreateShipment → PublishCompleted`. Each step's `Catch` routes to compensations in reverse: shipment-fail → refund + release; charge-fail → refund (no-op) + release; reserve-fail → no compensation. All terminal paths publish to EventBridge and reach `SUCCEEDED` (saga handles failure gracefully). - `/app/policies/` , per-role inline policies, each scoped to its own table ARN. No `*` resources anywhere on KMS/Lambda. Charge role is the only one with `secretsmanager:GetSecretValue` + `kms:Decrypt` for the CMK ARN. Saga role enumerates the 5 lambda ARNs explicitly. - `/app/deploy.sh` , idempotent end-to-end deploy. **Verified behavior** - Happy path (`o-1`, `k-1`): SM `SUCCEEDED`; `inventory[x].reserved_qty=2`; `payments[pay-o-1].status=CAPTURED`; `shipments[ship-o-1]` exists; notifier logged `saga-terminal:OrderCompleted`. - Failure path (`o-2`, `k-2`, `force_failure=true`): SM `SUCCEEDED`; payments/shipments rows for `o-2` absent; idempotency table shows `k-2#reserve`, `k-2#refund`, `k-2#release` (reserve forward, then both compensations in reverse , no `k-2#charge` or `k-2#ship`); notifier logged `saga-terminal:OrderFailed`. The leftover `reserved_qty=2` on sku `x` is the happy-path's reservation; the failure run added 2 then released 2, net zero delta. Re-run safely with `bash /app/deploy.sh`.
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/i98WC4T_QG83EuHR8KJzn/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_and_secret_exist PASSED      [  5%]
../tests/test_outputs.py::test_02_all_four_tables_exist PASSED           [ 10%]
../tests/test_outputs.py::test_03_all_six_lambdas_exist PASSED           [ 15%]
../tests/test_outputs.py::test_04_state_machine_and_bus_exist PASSED     [ 20%]
../tests/test_outputs.py::test_05_terminal_rule_exists_on_saga_bus PASSED [ 25%]
../tests/test_outputs.py::test_06_every_forward_state_has_catch_to_compensation PASSED [ 30%]
../tests/test_outputs.py::test_07_compensations_run_in_lifo_order PASSED [ 35%]
../tests/test_outputs.py::test_08_compensation_states_preserve_error_via_result_path PASSED [ 40%]
../tests/test_outputs.py::test_09_idempotency_table_has_ttl_and_correct_pk PASSED [ 45%]
../tests/test_outputs.py::test_10_each_lambda_role_scoped_to_its_own_table PASSED [ 50%]
../tests/test_outputs.py::test_11_only_charge_role_reads_the_payment_secret PASSED [ 55%]
../tests/test_outputs.py::test_12_saga_role_lambda_invoke_not_wildcarded PASSED [ 60%]
../tests/test_outputs.py::test_13_rule_pattern_matches_both_terminal_types_with_array_wrap PASSED [ 65%]
../tests/test_outputs.py::test_14_charge_role_kms_decrypt_scoped_not_wildcard PASSED [ 70%]
../tests/test_outputs.py::test_15_rule_targets_notifier_lambda PASSED    [ 75%]
../tests/test_outputs.py::test_16_notifier_role_is_least_privilege_no_ddb_kms_secrets PASSED [ 80%]
../tests/test_outputs.py::test_17_notifier_lambda_resource_policy_allows_events_with_source_arn PASSED [ 85%]
../tests/test_outputs.py::test_18_happy_path_completes_and_writes_all_three_tables PASSED [ 90%]
../tests/test_outputs.py::test_19_failure_path_triggers_compensation_and_rolls_back_inventory PASSED [ 95%]
../tests/test_outputs.py::test_20_idempotency_prevents_double_reservation PASSED [100%]

=============================== warnings summary ===============================
test_outputs.py: 132 warnings
  /root/.cache/uv/archive-v0/i98WC4T_QG83EuHR8KJzn/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_and_secret_exist
PASSED ../tests/test_outputs.py::test_02_all_four_tables_exist
PASSED ../tests/test_outputs.py::test_03_all_six_lambdas_exist
PASSED ../tests/test_outputs.py::test_04_state_machine_and_bus_exist
PASSED ../tests/test_outputs.py::test_05_terminal_rule_exists_on_saga_bus
PASSED ../tests/test_outputs.py::test_06_every_forward_state_has_catch_to_compensation
PASSED ../tests/test_outputs.py::test_07_compensations_run_in_lifo_order
PASSED ../tests/test_outputs.py::test_08_compensation_states_preserve_error_via_result_path
PASSED ../tests/test_outputs.py::test_09_idempotency_table_has_ttl_and_correct_pk
PASSED ../tests/test_outputs.py::test_10_each_lambda_role_scoped_to_its_own_table
PASSED ../tests/test_outputs.py::test_11_only_charge_role_reads_the_payment_secret
PASSED ../tests/test_outputs.py::test_12_saga_role_lambda_invoke_not_wildcarded
PASSED ../tests/test_outputs.py::test_13_rule_pattern_matches_both_terminal_types_with_array_wrap
PASSED ../tests/test_outputs.py::test_14_charge_role_kms_decrypt_scoped_not_wildcard
PASSED ../tests/test_outputs.py::test_15_rule_targets_notifier_lambda
PASSED ../tests/test_outputs.py::test_16_notifier_role_is_least_privilege_no_ddb_kms_secrets
PASSED ../tests/test_outputs.py::test_17_notifier_lambda_resource_policy_allows_events_with_source_arn
PASSED ../tests/test_outputs.py::test_18_happy_path_completes_and_writes_all_three_tables
PASSED ../tests/test_outputs.py::test_19_failure_path_triggers_compensation_and_rolls_back_inventory
PASSED ../tests/test_outputs.py::test_20_idempotency_prevents_double_reservation
====================== 20 passed, 132 warnings in 21.82s =======================

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

Trial trial_c00e2ff3ace84860 · verifier authoritative; classifier explanatory.