tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Verify localstack and tools
aws --endpoint-url=http://localstack:4566 sts get-caller-identity && python3 --version && which zip jq
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
Python 3.12.3
/usr/bin/zip
/usr/bin/jq
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
Python 3.12.3
/usr/bin/zip
/usr/bin/jqConfirm box is empty
aws --endpoint-url=http://localstack:4566 secretsmanager list-secrets && aws --endpoint-url=http://localstack:4566 lambda list-functions --query 'Functions[].FunctionName' && aws --endpoint-url=http://localstack:4566 stepfunctions list-state-machines && aws --endpoint-url=http://localstack:4566 events list-event-buses
{
"SecretList": []
}
[]
{
"stateMachines": []
}
{
"EventBuses": [
{
"Name": "default",
"Arn": "arn:aws:events:us-east-1:000000000000:event-bus/default",
"CreationTime": "2026-04-22T06:03:15.434436+00:00",
"LastModifiedTime": "2026-04-22T06:03:15.434440+00:00"
}
]
}
[stdout]
{
"SecretList": []
}
[]
{
"stateMachines": []
}
{
"EventBuses": [
{
"Name": "default",
"Arn": "arn:aws:events:us-east-1:000000000000:event-bus/default",
"CreationTime": "2026-04-22T06:03:15.434436+00:00",
"LastModifiedTime": "2026-04-22T06:03:15.434440+00:00"
}
]
}Set up build dir
mkdir -p /app/build && ls
build [stdout] build
Create CMK
aws --endpoint-url=http://localstack:4566 kms create-key --description "app rotation key" --key-usage ENCRYPT_DECRYPT --query 'KeyMetadata.KeyId' --output text > /tmp/kms_key_id && cat /tmp/kms_key_id
c48dd23c-f587-4c90-8e27-e1ed82f08626 [stdout] c48dd23c-f587-4c90-8e27-e1ed82f08626
Create KMS alias
KEY_ID=$(cat /tmp/kms_key_id); aws --endpoint-url=http://localstack:4566 kms create-alias --alias-name alias/app-rotation-key --target-key-id "$KEY_ID" && aws --endpoint-url=http://localstack:4566 kms list-aliases --query "Aliases[?AliasName=='alias/app-rotation-key']"
[
{
"AliasName": "alias/app-rotation-key",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/app-rotation-key",
"TargetKeyId": "c48dd23c-f587-4c90-8e27-e1ed82f08626",
"CreationDate": "2026-04-22T06:03:36.293421+00:00"
}
]
[stdout]
[
{
"AliasName": "alias/app-rotation-key",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/app-rotation-key",
"TargetKeyId": "c48dd23c-f587-4c90-8e27-e1ed82f08626",
"CreationDate": "2026-04-22T06:03:36.293421+00:00"
}
]Create custom event bus
aws --endpoint-url=http://localstack:4566 events create-event-bus --name rotation-events --query 'EventBusArn' --output text
arn:aws:events:us-east-1:000000000000:event-bus/rotation-events [stdout] arn:aws:events:us-east-1:000000000000:event-bus/rotation-events
{"source":["rotation.pipeline"],"detail-type":["RotationComplete"]}
Create EventBridge rule
aws --endpoint-url=http://localstack:4566 events put-rule --name on-rotation-success --event-bus-name rotation-events --event-pattern file:///app/build/event-pattern.json --state ENABLED --query 'RuleArn' --output text
arn:aws:events:us-east-1:000000000000:rule/rotation-events/on-rotation-success [stdout] arn:aws:events:us-east-1:000000000000:rule/rotation-events/on-rotation-success
import json
import logging
log = logging.getLogger()
log.setLevel(logging.INFO)
def handler(event, context):
detail = event.get("detail", {}) if isinstance(event, dict) else {}
secret_id = detail.get("secretId", "<unknown>")
version = detail.get("newVersion", "<unknown>")
print(f"notified: rotation complete for secret={secret_id} version={version}")
log.info("notified: rotation complete for secret=%s version=%s", secret_id, version)
return {"ok": True, "event": event}
Zip notification lambda
cd /app/build/notification && zip -q ../notification.zip handler.py && ls -la /app/build/notification.zip
-rw-r--r-- 1 root root 432 Apr 22 06:04 /app/build/notification.zip [stdout] -rw-r--r-- 1 root root 432 Apr 22 06:04 /app/build/notification.zip
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/notification-lambda:*"
}
]
}
Create notification lambda role
aws --endpoint-url=http://localstack:4566 iam create-role --role-name notification-lambda-role --assume-role-policy-document file:///app/build/trust-lambda.json --query 'Role.Arn' --output text
arn:aws:iam::000000000000:role/notification-lambda-role [stdout] arn:aws:iam::000000000000:role/notification-lambda-role
Attach policy + create notification lambda
aws --endpoint-url=http://localstack:4566 iam put-role-policy --role-name notification-lambda-role --policy-name notification-lambda-logs --policy-document file:///app/build/notification-lambda-policy.json && aws --endpoint-url=http://localstack:4566 lambda create-function --function-name notification-lambda --runtime python3.11 --role arn:aws:iam::000000000000:role/notification-lambda-role --handler handler.handler --zip-file fileb:///app/build/notification.zip --timeout 15 --query 'FunctionArn' --output text
arn:aws:lambda:us-east-1:000000000000:function:notification-lambda [stdout] arn:aws:lambda:us-east-1:000000000000:function:notification-lambda
Wait for notification lambda active
aws --endpoint-url=http://localstack:4566 lambda wait function-active --function-name notification-lambda && aws --endpoint-url=http://localstack:4566 lambda get-function --function-name notification-lambda --query 'Configuration.[State,LastUpdateStatus,Runtime]'
[
"Active",
"Successful",
"python3.11"
]
[stdout]
[
"Active",
"Successful",
"python3.11"
]Wire rule target + lambda permission
aws --endpoint-url=http://localstack:4566 events put-targets --event-bus-name rotation-events --rule on-rotation-success --targets 'Id=notify1,Arn=arn:aws:lambda:us-east-1:000000000000:function:notification-lambda' && aws --endpoint-url=http://localstack:4566 lambda add-permission --function-name notification-lambda --statement-id allow-rotation-events-bus --action lambda:InvokeFunction --principal events.amazonaws.com --source-arn arn:aws:events:us-east-1:000000000000:rule/rotation-events/on-rotation-success
{
"FailedEntryCount": 0,
"FailedEntries": []
}
{
"Statement": "{\"Sid\": \"allow-rotation-events-bus\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:notification-lambda\", \"Principal\": {\"Service\": \"events.amazonaws.com\"}, \"Condition\": {\"ArnLike\": {\"AWS:SourceArn\": \"arn:aws:events:us-east-1:000000000000:rule/rotation-events/on-rotation-success\"}}}"
}
[stdout]
{
"FailedEntryCount": 0,
"FailedEntries": []
}
{
"Statement": "{\"Sid\": \"allow-rotation-events-bus\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:notification-lambda\", \"Principal\": {\"Service\": \"events.amazonaws.com\"}, \"Condition\": {\"ArnLike\": {\"AWS:SourceArn\": \"arn:aws:events:us-east-1:000000000000:rule/rotation-events/on-rotation-success\"}}}"
}import logging
import os
import secrets
import string
import boto3
log = logging.getLogger()
log.setLevel(logging.INFO)
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localstack:4566")
client = boto3.client("secretsmanager", endpoint_url=ENDPOINT, region_name=os.environ.get("AWS_REGION", "us-east-1"))
def _generate_password(length: int = 32) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
def handler(event, context):
secret_id = event["SecretId"]
token = event["ClientRequestToken"]
step = event["Step"]
log.info("rotation step=%s secret=%s token=%s", step, secret_id, token)
meta = client.describe_secret(SecretId=secret_id)
versions = meta.get("VersionIdsToStages", {})
if token not in versions:
raise ValueError(f"version {token} has no stage for secret {secret_id}")
if "AWSCURRENT" in versions[token]:
log.info("version %s is already AWSCURRENT; nothing to do", token)
return
if "AWSPENDING" not in versions[token]:
raise ValueError(f"version {token} not staged AWSPENDING for secret {secret_id}")
if step == "createSecret":
create_secret(secret_id, token)
elif step == "setSecret":
set_secret(secret_id, token)
elif step == "testSecret":
test_secret(secret_id, token)
elif step == "finishSecret":
finish_secret(secret_id, token)
else:
raise ValueError(f"unknown step {step}")
def create_secret(secret_id: str, token: str) -> None:
try:
client.get_secret_value(SecretId=secret_id, VersionId=token, VersionStage="AWSPENDING")
log.info("AWSPENDING already exists for %s", token)
return
except client.exceptions.ResourceNotFoundException:
pass
new_password = _generate_password()
client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=new_password,
VersionStages=["AWSPENDING"],
)
log.info("staged AWSPENDING value for %s", token)
def set_secret(secret_id: str, token: str) -> None:
log.info("setSecret no-op: no external db to update for %s", secret_id)
def test_secret(secret_id: str, token: str) -> None:
value = client.get_secret_value(SecretId=secret_id, VersionId=token, VersionStage="AWSPENDING")["SecretString"]
if not value or len(value) < 16:
raise ValueError("pending secret failed validation")
log.info("testSecret ok for %s", token)
def finish_secret(secret_id: str, token: str) -> None:
meta = client.describe_secret(SecretId=secret_id)
current_version = None
for version_id, stages in meta.get("VersionIdsToStages", {}).items():
if "AWSCURRENT" in stages:
current_version = version_id
break
if current_version == token:
log.info("finishSecret: already current")
return
client.update_secret_version_stage(
SecretId=secret_id,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current_version,
)
log.info("finishSecret moved AWSCURRENT to %s (was %s)", token, current_version)
Zip rotator lambda
cd /app/build/rotator && zip -q ../rotator.zip handler.py && ls -la /app/build/rotator.zip
-rw-r--r-- 1 root root 1237 Apr 22 06:05 /app/build/rotator.zip [stdout] -rw-r--r-- 1 root root 1237 Apr 22 06:05 /app/build/rotator.zip
Get KMS key ARN
KEY_ID=$(cat /tmp/kms_key_id); aws --endpoint-url=http://localstack:4566 kms describe-key --key-id "$KEY_ID" --query 'KeyMetadata.Arn' --output text > /tmp/kms_arn && cat /tmp/kms_arn
arn:aws:kms:us-east-1:000000000000:key/c48dd23c-f587-4c90-8e27-e1ed82f08626 [stdout] arn:aws:kms:us-east-1:000000000000:key/c48dd23c-f587-4c90-8e27-e1ed82f08626
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/secrets-rotator:*"
},
{
"Sid": "RotateSecret",
"Effect": "Allow",
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage"
],
"Resource": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password*"
},
{
"Sid": "RandomTokenForCreateSecret",
"Effect": "Allow",
"Action": "secretsmanager:GetRandomPassword",
"Resource": "*"
},
{
"Sid": "UseRotationKey",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:000000000000:key/c48dd23c-f587-4c90-8e27-e1ed82f08626",
"Condition": {
"StringEquals": {
"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
}
}
}
]
}
Create rotator role + attach policy
aws --endpoint-url=http://localstack:4566 iam create-role --role-name secrets-rotator-role --assume-role-policy-document file:///app/build/trust-lambda.json --query 'Role.Arn' --output text && aws --endpoint-url=http://localstack:4566 iam put-role-policy --role-name secrets-rotator-role --policy-name secrets-rotator-policy --policy-document file:///app/build/secrets-rotator-policy.json
arn:aws:iam::000000000000:role/secrets-rotator-role [stdout] arn:aws:iam::000000000000:role/secrets-rotator-role
Create rotator lambda
aws --endpoint-url=http://localstack:4566 lambda create-function --function-name secrets-rotator --runtime python3.11 --role arn:aws:iam::000000000000:role/secrets-rotator-role --handler handler.handler --zip-file fileb:///app/build/rotator.zip --timeout 30 --environment 'Variables={AWS_ENDPOINT_URL=http://localstack:4566}' --query 'FunctionArn' --output text && aws --endpoint-url=http://localstack:4566 lambda wait function-active --function-name secrets-rotatorarn:aws:lambda:us-east-1:000000000000:function:secrets-rotator [stdout] arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator
Allow secretsmanager to invoke rotator
aws --endpoint-url=http://localstack:4566 lambda add-permission --function-name secrets-rotator --statement-id allow-secretsmanager-invoke --action lambda:InvokeFunction --principal secretsmanager.amazonaws.com
{
"Statement": "{\"Sid\": \"allow-secretsmanager-invoke\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator\", \"Principal\": {\"Service\": \"secretsmanager.amazonaws.com\"}}"
}
[stdout]
{
"Statement": "{\"Sid\": \"allow-secretsmanager-invoke\", \"Effect\": \"Allow\", \"Action\": \"lambda:InvokeFunction\", \"Resource\": \"arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator\", \"Principal\": {\"Service\": \"secretsmanager.amazonaws.com\"}}"
}Create secret w/ CMK
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name app/db/password --description "App DB password (rotated)" --kms-key-id alias/app-rotation-key --secret-string "initial-placeholder-do-not-use" --query 'ARN' --output text
arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy [stdout] arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy
Confirm secret uses CMK
aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query '[KmsKeyId,VersionIdsToStages]'
[
"alias/app-rotation-key",
{
"d3a5ce64-fa9b-401d-a56d-23b24a34b417": [
"AWSCURRENT"
]
}
]
[stdout]
[
"alias/app-rotation-key",
{
"d3a5ce64-fa9b-401d-a56d-23b24a34b417": [
"AWSCURRENT"
]
}
]Attach rotation lambda + trigger initial rotation
aws --endpoint-url=http://localstack:4566 secretsmanager rotate-secret --secret-id app/db/password --rotation-lambda-arn arn:aws:lambda:us-east-1:000000000000:function:secrets-rotator --rotation-rules ScheduleExpression='rate(30 days)' --rotate-immediately 2>&1 | head -50
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy",
"Name": "app/db/password",
"VersionId": "c4ee174b-6e14-4f07-91a2-47d53936f3cb"
}
[stdout]
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy",
"Name": "app/db/password",
"VersionId": "c4ee174b-6e14-4f07-91a2-47d53936f3cb"
}Check initial rotation state
sleep 3 && aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query '[KmsKeyId,RotationEnabled,RotationLambdaARN,VersionIdsToStages]'
[
"alias/app-rotation-key",
null,
null,
{
"d3a5ce64-fa9b-401d-a56d-23b24a34b417": [
"AWSPREVIOUS"
],
"c4ee174b-6e14-4f07-91a2-47d53936f3cb": [
"AWSPENDING",
"AWSCURRENT"
]
}
]
[stdout]
[
"alias/app-rotation-key",
null,
null,
{
"d3a5ce64-fa9b-401d-a56d-23b24a34b417": [
"AWSPREVIOUS"
],
"c4ee174b-6e14-4f07-91a2-47d53936f3cb": [
"AWSPENDING",
"AWSCURRENT"
]
}
]Test re-rotation w/o lambda arn
aws --endpoint-url=http://localstack:4566 secretsmanager rotate-secret --secret-id app/db/password 2>&1 | head -20
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy",
"Name": "app/db/password",
"VersionId": "d4131316-3b48-4eda-bba9-cfe0b7711df5"
}
[stdout]
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password-KFdhYy",
"Name": "app/db/password",
"VersionId": "d4131316-3b48-4eda-bba9-cfe0b7711df5"
}Verify rotation produces different strings
sleep 3 && aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages' && aws --endpoint-url=http://localstack:4566 secretsmanager get-secret-value --secret-id app/db/password --query 'SecretString' --output text > /tmp/cur1.txt && aws --endpoint-url=http://localstack:4566 secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSPREVIOUS --query 'SecretString' --output text > /tmp/prev1.txt && echo "current=$(cat /tmp/cur1.txt)" && echo "previous=$(cat /tmp/prev1.txt)"
{
"c4ee174b-6e14-4f07-91a2-47d53936f3cb": [
"AWSPREVIOUS"
],
"d4131316-3b48-4eda-bba9-cfe0b7711df5": [
"AWSPENDING",
"AWSCURRENT"
]
}
current=fHoYkIyfppAsJ0otlUs1GqNeRFxsqAuH
previous=toodLoCkAoijYEoDrXPsvvZ9ASFWTJy3
[stdout]
{
"c4ee174b-6e14-4f07-91a2-47d53936f3cb": [
"AWSPREVIOUS"
],
"d4131316-3b48-4eda-bba9-cfe0b7711df5": [
"AWSPENDING",
"AWSCURRENT"
]
}
current=fHoYkIyfppAsJ0otlUs1GqNeRFxsqAuH
previous=toodLoCkAoijYEoDrXPsvvZ9ASFWTJy3import logging
log = logging.getLogger()
log.setLevel(logging.INFO)
def handler(event, context):
detail = event.get("detail", {}) if isinstance(event, dict) else {}
secret_id = detail.get("ARN") or detail.get("secretId") or "<unknown>"
version = detail.get("VersionId") or detail.get("newVersion") or "<unknown>"
print(f"notified: rotation complete for secret={secret_id} version={version}")
log.info("notified: rotation complete for secret=%s version=%s", secret_id, version)
return {"ok": True}
Update notification lambda code
cd /app/build/notification && zip -q -f ../notification.zip handler.py 2>/dev/null; cd /app/build/notification && rm -f ../notification.zip && zip -q ../notification.zip handler.py && aws --endpoint-url=http://localstack:4566 lambda update-function-code --function-name notification-lambda --zip-file fileb:///app/build/notification.zip --query '[State,LastUpdateStatus]' && aws --endpoint-url=http://localstack:4566 lambda wait function-updated --function-name notification-lambda
[
"Active",
"InProgress"
]
[stdout]
[
"Active",
"InProgress"
]{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "states.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RotateAppSecret",
"Effect": "Allow",
"Action": "secretsmanager:RotateSecret",
"Resource": "arn:aws:secretsmanager:us-east-1:000000000000:secret:app/db/password*"
},
{
"Sid": "PublishRotationEvent",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/rotation-events"
}
]
}
{
"Comment": "Rotate app/db/password and emit a RotationComplete event onto the rotation-events bus.",
"StartAt": "RotateSecret",
"States": {
"RotateSecret": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:secretsmanager:rotateSecret",
"Parameters": {
"SecretId": "app/db/password"
},
"ResultPath": "$.rotation",
"Next": "EmitRotationComplete"
},
"EmitRotationComplete": {
"Type": "Task",
"Resource": "arn:aws:states:::events:putEvents",
"Parameters": {
"Entries": [
{
"EventBusName": "rotation-events",
"Source": "rotation.pipeline",
"DetailType": "RotationComplete",
"Detail.$": "States.JsonToString($.rotation)"
}
]
},
"End": true
}
}
}
Create child SM + role
aws --endpoint-url=http://localstack:4566 iam create-role --role-name rotation-child-role --assume-role-policy-document file:///app/build/trust-states.json --query 'Role.Arn' --output text && aws --endpoint-url=http://localstack:4566 iam put-role-policy --role-name rotation-child-role --policy-name rotation-child-policy --policy-document file:///app/build/rotation-child-policy.json && aws --endpoint-url=http://localstack:4566 stepfunctions create-state-machine --name rotation-child --definition file:///app/build/rotation-child.asl.json --role-arn arn:aws:iam::000000000000:role/rotation-child-role --type STANDARD --query 'stateMachineArn' --output text
arn:aws:iam::000000000000:role/rotation-child-role arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child [stdout] arn:aws:iam::000000000000:role/rotation-child-role arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child
Smoke test child SM
CHILD_EXEC=$(aws --endpoint-url=http://localstack:4566 stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child --query 'executionArn' --output text) && echo "started=$CHILD_EXEC" && for i in 1 2 3 4 5 6 7 8 9 10; do STATUS=$(aws --endpoint-url=http://localstack:4566 stepfunctions describe-execution --execution-arn "$CHILD_EXEC" --query 'status' --output text); echo "t=${i}s status=$STATUS"; if [ "$STATUS" != "RUNNING" ]; then break; fi; sleep 1; done && aws --endpoint-url=http://localstack:4566 stepfunctions describe-execution --execution-arn "$CHILD_EXEC" --query '[status,output,error,cause]'started=arn:aws:states:us-east-1:000000000000:execution:rotation-child:a85700e6-aaa0-4233-9717-328178cb4ecc
t=1s status=RUNNING
t=2s status=RUNNING
t=3s status=RUNNING
t=4s status=RUNNING
t=5s status=SUCCEEDED
[
"SUCCEEDED",
"{\"FailedEntryCount\":0,\"Entries\":[{\"EventId\":\"05512606-722a-4543-af1a-1b972bb3400a\"}]}",
null,
null
]
[stdout]
started=arn:aws:states:us-east-1:000000000000:execution:rotation-child:a85700e6-aaa0-4233-9717-328178cb4ecc
t=1s status=RUNNING
t=2s status=RUNNING
t=3s status=RUNNING
t=4s status=RUNNING
t=5s status=SUCCEEDED
[
"SUCCEEDED",
"{\"FailedEntryCount\":0,\"Entries\":[{\"EventId\":\"05512606-722a-4543-af1a-1b972bb3400a\"}]}",
null,
null
]Check notification log
sleep 2 && aws --endpoint-url=http://localstack:4566 logs describe-log-streams --log-group-name /aws/lambda/notification-lambda --order-by LastEventTime --descending --max-items 3 --query 'logStreams[].logStreamName' && aws --endpoint-url=http://localstack:4566 logs filter-log-events --log-group-name /aws/lambda/notification-lambda --filter-pattern 'notified' --query 'events[].message'
[
"2026/04/22/[$LATEST]9904b813092f6949a5420d2b76872a94"
]
[
"START RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3 Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"[INFO]\t2026-04-22T06:08:28.220Z\t8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tnotified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"END RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\n",
"REPORT RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tDuration: 10.24 ms\tBilled Duration: 11 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n"
]
[stdout]
[
"2026/04/22/[$LATEST]9904b813092f6949a5420d2b76872a94"
]
[
"START RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3 Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"[INFO]\t2026-04-22T06:08:28.220Z\t8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tnotified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"END RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\n",
"REPORT RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tDuration: 10.24 ms\tBilled Duration: 11 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n"
]{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeChildRotation",
"Effect": "Allow",
"Action": "states:StartExecution",
"Resource": "arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child"
},
{
"Sid": "PollAndCancelChildExecution",
"Effect": "Allow",
"Action": [
"states:DescribeExecution",
"states:StopExecution"
],
"Resource": "arn:aws:states:us-east-1:000000000000:execution:rotation-child:*"
},
{
"Sid": "ManagedRuleForSyncCallback",
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:PutTargets",
"events:DescribeRule"
],
"Resource": "arn:aws:events:us-east-1:000000000000:rule/StepFunctionsGetEventsForStepFunctionsExecutionRule"
}
]
}
{
"Comment": "Parent orchestrator: invokes the child rotation SM synchronously (.sync:2) and waits.",
"StartAt": "RunChildRotation",
"States": {
"RunChildRotation": {
"Type": "Task",
"Resource": "arn:aws:states:::states:startExecution.sync:2",
"Parameters": {
"StateMachineArn": "arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child"
},
"End": true
}
}
}
Create parent SM + role
aws --endpoint-url=http://localstack:4566 iam create-role --role-name rotation-orchestrator-role --assume-role-policy-document file:///app/build/trust-states.json --query 'Role.Arn' --output text && aws --endpoint-url=http://localstack:4566 iam put-role-policy --role-name rotation-orchestrator-role --policy-name rotation-orchestrator-policy --policy-document file:///app/build/rotation-orchestrator-policy.json && aws --endpoint-url=http://localstack:4566 stepfunctions create-state-machine --name rotation-orchestrator --definition file:///app/build/rotation-orchestrator.asl.json --role-arn arn:aws:iam::000000000000:role/rotation-orchestrator-role --type STANDARD --query 'stateMachineArn' --output text
arn:aws:iam::000000000000:role/rotation-orchestrator-role arn:aws:states:us-east-1:000000000000:stateMachine:rotation-orchestrator [stdout] arn:aws:iam::000000000000:role/rotation-orchestrator-role arn:aws:states:us-east-1:000000000000:stateMachine:rotation-orchestrator
Start parent and wait for completion
BEFORE_CURRENT=$(aws --endpoint-url=http://localstack:4566 secretsmanager get-secret-value --secret-id app/db/password --query 'SecretString' --output text); BEFORE_VERSION=$(aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages' --output json); echo "BEFORE current=$BEFORE_CURRENT"; echo "BEFORE versions=$BEFORE_VERSION"; echo "---"; START_TS=$(date +%s); EXEC=$(aws --endpoint-url=http://localstack:4566 stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:000000000000:stateMachine:rotation-orchestrator --query 'executionArn' --output text); echo "started=$EXEC"; for i in $(seq 1 90); do STATUS=$(aws --endpoint-url=http://localstack:4566 stepfunctions describe-execution --execution-arn "$EXEC" --query 'status' --output text); if [ "$STATUS" != "RUNNING" ]; then ELAPSED=$(( $(date +%s) - START_TS )); echo "t=${ELAPSED}s status=$STATUS"; break; fi; sleep 1; done; aws --endpoint-url=http://localstack:4566 stepfunctions describe-execution --execution-arn "$EXEC" --query '[status,error,cause]'BEFORE current=Kv2m7amHpDEZ6Xc4QaoxU6QxTjyWoRjw
BEFORE versions={
"d4131316-3b48-4eda-bba9-cfe0b7711df5": [
"AWSPREVIOUS"
],
"b6ffbdf7-2c04-423a-b297-d94d82f84ff7": [
"AWSPENDING",
"AWSCURRENT"
]
}
---
started=arn:aws:states:us-east-1:000000000000:execution:rotation-orchestrator:70811aaa-09d1-4dae-b8d7-83b81b1e60ef
t=7s status=SUCCEEDED
[
"SUCCEEDED",
null,
null
]
[stdout]
BEFORE current=Kv2m7amHpDEZ6Xc4QaoxU6QxTjyWoRjw
BEFORE versions={
"d4131316-3b48-4eda-bba9-cfe0b7711df5": [
"AWSPREVIOUS"
],
"b6ffbdf7-2c04-423a-b297-d94d82f84ff7": [
"AWSPENDING",
"AWSCURRENT"
]
}
---
started=arn:aws:states:us-east-1:000000000000:execution:rotation-orchestrator:70811aaa-09d1-4dae-b8d7-83b81b1e60ef
t=7s status=SUCCEEDED
[
"SUCCEEDED",
null,
null
]Verify rotation effects
sleep 2 && AFTER_CURRENT=$(aws --endpoint-url=http://localstack:4566 secretsmanager get-secret-value --secret-id app/db/password --query 'SecretString' --output text); AFTER_PREVIOUS=$(aws --endpoint-url=http://localstack:4566 secretsmanager get-secret-value --secret-id app/db/password --version-stage AWSPREVIOUS --query 'SecretString' --output text); AFTER_VERSIONS=$(aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query 'VersionIdsToStages' --output json); echo "AFTER current=$AFTER_CURRENT"; echo "AFTER previous=$AFTER_PREVIOUS"; echo "AFTER versions=$AFTER_VERSIONS"; echo "---"; aws --endpoint-url=http://localstack:4566 logs filter-log-events --log-group-name /aws/lambda/notification-lambda --filter-pattern 'notified' --query 'events[].message' --output text | tr '\t' '\n' | tail -5
AFTER current=xpnxVBCFyKLwyLPVRUo6Gb6q7EpAGVYL
AFTER previous=Kv2m7amHpDEZ6Xc4QaoxU6QxTjyWoRjw
AFTER versions={
"b6ffbdf7-2c04-423a-b297-d94d82f84ff7": [
"AWSPREVIOUS"
],
"fabf8e52-5374-4883-98af-bbfecb326857": [
"AWSPENDING",
"AWSCURRENT"
]
}
---
Billed Duration: 10 ms
Memory Size: 128 MB
Max Memory Used: 128 MB
[stdout]
AFTER current=xpnxVBCFyKLwyLPVRUo6Gb6q7EpAGVYL
AFTER previous=Kv2m7amHpDEZ6Xc4QaoxU6QxTjyWoRjw
AFTER versions={
"b6ffbdf7-2c04-423a-b297-d94d82f84ff7": [
"AWSPREVIOUS"
],
"fabf8e52-5374-4883-98af-bbfecb326857": [
"AWSPENDING",
"AWSCURRENT"
]
}
---
Billed Duration: 10 ms
Memory Size: 128 MB
Max Memory Used: 128 MBGet all 'notified:' lines
aws --endpoint-url=http://localstack:4566 logs filter-log-events --log-group-name /aws/lambda/notification-lambda --filter-pattern '"notified:"' --query 'events[].message' --output json
[
"START RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3 Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"[INFO]\t2026-04-22T06:08:28.220Z\t8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tnotified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"END RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\n",
"REPORT RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tDuration: 10.24 ms\tBilled Duration: 11 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n",
"START RequestId: dce9e270-2388-4911-a597-db31d9ed950d Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=fabf8e52-5374-4883-98af-bbfecb326857\n",
"[INFO]\t2026-04-22T06:09:44.483Z\tdce9e270-2388-4911-a597-db31d9ed950d\tnotified: rotation complete for secret=<unknown> version=fabf8e52-5374-4883-98af-bbfecb326857\n",
"END RequestId: dce9e270-2388-4911-a597-db31d9ed950d\n",
"REPORT RequestId: dce9e270-2388-4911-a597-db31d9ed950d\tDuration: 9.98 ms\tBilled Duration: 10 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n"
]
[stdout]
[
"START RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3 Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"[INFO]\t2026-04-22T06:08:28.220Z\t8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tnotified: rotation complete for secret=<unknown> version=b6ffbdf7-2c04-423a-b297-d94d82f84ff7\n",
"END RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\n",
"REPORT RequestId: 8a7eb6e5-bc03-4d45-b8ff-f8b7921a27f3\tDuration: 10.24 ms\tBilled Duration: 11 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n",
"START RequestId: dce9e270-2388-4911-a597-db31d9ed950d Version: $LATEST\n",
"notified: rotation complete for secret=<unknown> version=fabf8e52-5374-4883-98af-bbfecb326857\n",
"[INFO]\t2026-04-22T06:09:44.483Z\tdce9e270-2388-4911-a597-db31d9ed950d\tnotified: rotation complete for secret=<unknown> version=fabf8e52-5374-4883-98af-bbfecb326857\n",
"END RequestId: dce9e270-2388-4911-a597-db31d9ed950d\n",
"REPORT RequestId: dce9e270-2388-4911-a597-db31d9ed950d\tDuration: 9.98 ms\tBilled Duration: 10 ms\tMemory Size: 128 MB\tMax Memory Used: 128 MB\t\n"
]Final inventory check
echo "=== KMS ==="; aws --endpoint-url=http://localstack:4566 kms list-aliases --query "Aliases[?AliasName=='alias/app-rotation-key'].[AliasName,TargetKeyId]" --output table; echo "=== Secret ==="; aws --endpoint-url=http://localstack:4566 secretsmanager describe-secret --secret-id app/db/password --query '[Name,KmsKeyId]' --output table; echo "=== Lambdas ==="; aws --endpoint-url=http://localstack:4566 lambda list-functions --query 'Functions[].[FunctionName,Runtime,Role]' --output table; echo "=== State machines ==="; aws --endpoint-url=http://localstack:4566 stepfunctions list-state-machines --query 'stateMachines[].[name,stateMachineArn]' --output table; echo "=== EventBridge bus + rule ==="; aws --endpoint-url=http://localstack:4566 events list-rules --event-bus-name rotation-events --query 'Rules[].[Name,EventBusName,EventPattern,State]' --output table; echo "=== Targets ==="; aws --endpoint-url=http://localstack:4566 events list-targets-by-rule --event-bus-name rotation-events --rule on-rotation-success --query 'Targets[].[Id,Arn]' --output table
=== KMS ===
--------------------------------------------------------------------
| ListAliases |
+-------------------------+----------------------------------------+
| alias/app-rotation-key | c48dd23c-f587-4c90-8e27-e1ed82f08626 |
+-------------------------+----------------------------------------+
=== Secret ===
----------------------------
| DescribeSecret |
+--------------------------+
| app/db/password |
| alias/app-rotation-key |
+--------------------------+
=== Lambdas ===
--------------------------------------------------------------------------------------------------
| ListFunctions |
+---------------------+-------------+------------------------------------------------------------+
| notification-lambda| python3.11 | arn:aws:iam::000000000000:role/notification-lambda-role |
| secrets-rotator | python3.11 | arn:aws:iam::000000000000:role/secrets-rotator-role |
+---------------------+-------------+------------------------------------------------------------+
=== State machines ===
-------------------------------------------------------------------------------------------------------
| ListStateMachines |
+-----------------------+-----------------------------------------------------------------------------+
| rotation-child | arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child |
| rotation-orchestrator| arn:aws:states:us-east-1:000000000000:stateMachine:rotation-orchestrator |
+-----------------------+-----------------------------------------------------------------------------+
=== EventBridge bus + rule ===
-------------------------------------------------------------------------------------------------------------------------------
| ListRules |
+---------------------+------------------+------------------------------------------------------------------------+-----------+
| on-rotation-success| rotation-events | {"source":["rotation.pipeline"],"detail-type":["RotationComplete"]}
| ENABLED |
+---------------------+------------------+------------------------------------------------------------------------+-----------+
=== Targets ===
-----------------------------------------------------------------------------------
| ListTargetsByRule |
+---------+-----------------------------------------------------------------------+
| notify1| arn:aws:lambda:us-east-1:000000000000:function:notification-lambda |
+---------+-----------------------------------------------------------------------+
[stdout]
=== KMS ===
--------------------------------------------------------------------
| ListAliases |
+-------------------------+----------------------------------------+
| alias/app-rotation-key | c48dd23c-f587-4c90-8e27-e1ed82f08626 |
+-------------------------+----------------------------------------+
=== Secret ===
----------------------------
| DescribeSecret |
+--------------------------+
| app/db/password |
| alias/app-rotation-key |
+--------------------------+
=== Lambdas ===
--------------------------------------------------------------------------------------------------
| ListFunctions |
+---------------------+-------------+------------------------------------------------------------+
| notification-lambda| python3.11 | arn:aws:iam::000000000000:role/notification-lambda-role |
| secrets-rotator | python3.11 | arn:aws:iam::000000000000:role/secrets-rotator-role |
+---------------------+-------------+------------------------------------------------------------+
=== State machines ===
-------------------------------------------------------------------------------------------------------
| ListStateMachines |
+-----------------------+-----------------------------------------------------------------------------+
| rotation-child | arn:aws:states:us-east-1:000000000000:stateMachine:rotation-child |
| rotation-orchestrator| arn:aws:states:us-east-1:000000000000:stateMachine:rotation-orchestrator |
+-----------------------+-----------------------------------------------------------------------------+
=== EventBridge bus + rule ===
-------------------------------------------------------------------------------------------------------------------------------
| ListRules |
+---------------------+------------------+------------------------------------------------------------------------+-----------+
| on-rotation-success| rotation-events | {"source":["rotation.pipeline"],"detail-type":["RotationComplete"]}
| ENABLED |
+---------------------+------------------+------------------------------------------------------------------------+-----------+
=== Targets ===
-----------------------------------------------------------------------------------
| ListTargetsByRule |
+---------+-----------------------------------------------------------------------+
| notify1| arn:aws:lambda:us-east-1:000000000000:function:notification-lambda |
+---------+-----------------------------------------------------------------------+No file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/j6Z67aqB8PWfCPA32awlD/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 15 items
../tests/test_state.py::test_aws_cli_binary_is_real PASSED [ 6%]
../tests/test_state.py::test_localstack_reachable PASSED [ 13%]
../tests/test_state.py::test_secret_exists_and_uses_customer_cmk PASSED [ 20%]
../tests/test_state.py::test_rotation_lambda_exists PASSED [ 26%]
../tests/test_state.py::test_notification_lambda_exists PASSED [ 33%]
../tests/test_state.py::test_parent_and_child_state_machines_exist PASSED [ 40%]
../tests/test_state.py::test_event_bus_and_rule_exist PASSED [ 46%]
../tests/test_state.py::test_rule_target_points_at_notification_lambda PASSED [ 53%]
../tests/test_state.py::test_rotator_role_grants_kms_actions PASSED [ 60%]
../tests/test_state.py::test_kms_key_policy_grants_rotator_role FAILED [ 66%]
../tests/test_state.py::test_rotator_lambda_allows_secretsmanager_invoke PASSED [ 73%]
../tests/test_state.py::test_parent_role_grants_sync2_managed_rule_perms FAILED [ 80%]
../tests/test_state.py::test_child_role_can_rotate_and_publish PASSED [ 86%]
../tests/test_state.py::test_notification_lambda_allows_eventbridge_invoke PASSED [ 93%]
../tests/test_state.py::test_end_to_end_parent_execution_rotates_and_notifies PASSED [100%]
=================================== FAILURES ===================================
___________________ test_kms_key_policy_grants_rotator_role ____________________
iam = <botocore.client.IAM object at 0xffffb70cdbe0>
kms = <botocore.client.KMS object at 0xffffb76182c0>
def test_kms_key_policy_grants_rotator_role(iam, kms):
role_arn = iam.get_role(RoleName=ROTATOR_ROLE)["Role"]["Arn"]
policy_str = kms.get_key_policy(KeyId=KEY_ALIAS, PolicyName="default")[
"Policy"
]
policy = json.loads(policy_str)
match = False
for st in policy.get("Statement", []):
if st.get("Effect") != "Allow":
continue
principal = st.get("Principal") or {}
if not isinstance(principal, dict):
continue
arns = set(_normalise_list(principal.get("AWS")))
if role_arn not in arns:
continue
actions = set(_normalise_list(st.get("Action")))
if _actions_cover(actions, REQUIRED_KMS_ACTIONS):
match = True
break
> assert match, (
f"KMS key policy on {KEY_ALIAS} has no Allow statement whose "
f"Principal.AWS includes {role_arn} and whose Action covers "
f"{sorted(REQUIRED_KMS_ACTIONS)}. Key policy: {policy_str}"
)
E AssertionError: KMS key policy on alias/app-rotation-key has no Allow statement whose Principal.AWS includes arn:aws:iam::000000000000:role/secrets-rotator-role and whose Action covers ['kms:Decrypt', 'kms:GenerateDataKey']. Key policy: {"Version": "2012-10-17", "Id": "key-default-1", "Statement": [{"Sid": "Enable IAM User Permissions", "Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::000000000000:root"}, "Action": "kms:*", "Resource": "*"}]}
E assert False
/tests/test_state.py:322: AssertionError
_______________ test_parent_role_grants_sync2_managed_rule_perms _______________
iam = <botocore.client.IAM object at 0xffffb70cdbe0>
def test_parent_role_grants_sync2_managed_rule_perms(iam):
"""The .sync:2 integration registers an EventBridge-managed callback
rule. Parent role needs events:PutRule/PutTargets/DescribeRule plus
full states:* on the child."""
found_events = False
found_states = False
for _pname, st in _inline_statements(iam, PARENT_ROLE):
actions = set(_normalise_list(st.get("Action")))
if _actions_cover(actions, REQUIRED_PARENT_EVENTS_ACTIONS):
found_events = True
if _actions_cover(actions, REQUIRED_PARENT_STATES_ACTIONS):
found_states = True
assert found_events, (
f"{PARENT_ROLE} missing events:PutRule/PutTargets/DescribeRule. "
f".sync:2 uses an EventBridge-managed callback rule; without "
f"these the parent cannot set up the callback."
)
> assert found_states, (
f"{PARENT_ROLE} missing states:StartExecution/DescribeExecution/"
f"StopExecution. .sync:2 requires all three on the child SM."
)
E AssertionError: rotation-orchestrator-role missing states:StartExecution/DescribeExecution/StopExecution. .sync:2 requires all three on the child SM.
E assert False
/tests/test_state.py:377: AssertionError
=============================== warnings summary ===============================
test_state.py: 35 warnings
/root/.cache/uv/archive-v0/j6Z67aqB8PWfCPA32awlD/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_state.py::test_aws_cli_binary_is_real
PASSED ../tests/test_state.py::test_localstack_reachable
PASSED ../tests/test_state.py::test_secret_exists_and_uses_customer_cmk
PASSED ../tests/test_state.py::test_rotation_lambda_exists
PASSED ../tests/test_state.py::test_notification_lambda_exists
PASSED ../tests/test_state.py::test_parent_and_child_state_machines_exist
PASSED ../tests/test_state.py::test_event_bus_and_rule_exist
PASSED ../tests/test_state.py::test_rule_target_points_at_notification_lambda
PASSED ../tests/test_state.py::test_rotator_role_grants_kms_actions
PASSED ../tests/test_state.py::test_rotator_lambda_allows_secretsmanager_invoke
PASSED ../tests/test_state.py::test_child_role_can_rotate_and_publish
PASSED ../tests/test_state.py::test_notification_lambda_allows_eventbridge_invoke
PASSED ../tests/test_state.py::test_end_to_end_parent_execution_rotates_and_notifies
FAILED ../tests/test_state.py::test_kms_key_policy_grants_rotator_role - Asse...
FAILED ../tests/test_state.py::test_parent_role_grants_sync2_managed_rule_perms
================== 2 failed, 13 passed, 35 warnings in 11.61s ==================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_8000615de48e44da. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_8000615de48e44da · verifier authoritative; classifier explanatory.