Cryptographic Supply Chain Tracker

Build a serverless AWS supply chain tracker with cryptographic signing.

Introduction

30 Second Summary

Every medicine you take and every meal you eat passes through a chain of hands before it reaches you. When contaminated infant formula reached 99 countries in 2025, no system could prove what happened at each handoff.

In this project, you will build a serverless supply chain tracking system on AWS where every handoff between parties is cryptographically signed. You will coordinate 5 parallel Cursor agents like an engineering manager leading cross-functional teams, then prove the system works by running crisis-inspired demo scenarios.

What You'll Build

A serverless supply chain tracking system that cryptographically signs every handoff between three parties using AWS KMS, detects anomalies, and exposes a public audit API.

By the end of this project, you'll have:

  • A fully deployed serverless system with 4 Lambda functions, a DynamoDB table, and an API Gateway endpoint, all defined in a single SAM template.
  • A KMS signing harness that cryptographically signs every supply chain handoff using ECDSA, paired with an anomaly detector that catches chain breaks in milliseconds.
  • Three effectiveness artifacts: an interactive HTML workflow visualization, a leadership KPI metrics dashboard with global crisis benchmarks, and optimized Cursor agent prompts for parallel execution.
  • Secret Mission: Add a SLSA L1 compliance dashboard that generates provenance reports for the full chain.

Are there any prerequisites?

You need an existing AWS account with the AWS CLI already configured, plus a Cursor Pro subscription. SAM CLI and Python setup are covered in Step 1.

Set Up Your Environment and Install Tools

Before you can build anything, you need the right tools on your machine. This project uses AWS SAM (Serverless Application Model) to define and deploy all of your Lambda functions, DynamoDB tables, and API Gateway endpoints from a single template file.

In this step, you will install the SAM CLI, initialize a project scaffold, create your team conventions file, and spin up 5 parallel Cursor agents. Each agent owns a different domain of the system, just like cross-functional teams in a real engineering org.

In this step, get ready to:
  • Install AWS SAM CLI and verify your Python and AWS CLI prerequisites.
  • Initialize the project directory with a SAM scaffold and create your DevSecOps conventions file.
  • Open the Cursor Agents Window and launch 5 parallel agent worktrees.
Install SAM CLI and verify Python

The SAM CLI is the command-line tool that builds, tests, and deploys your serverless application. It reads a template.yaml file and turns it into real AWS resources via CloudFormation.

You already have the AWS CLI configured. Let's verify that Python and AWS CLI are ready, then install SAM CLI.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Verify Python is installed by running this command:
python3 --version

You should see a version number starting with 3.12 or higher.

✔️ I see a version number

Python is ready. Move on to the next check.

ⓧ Command not found

You need Python 3.12 installed. Download it from the official Python website.

  • Go to python.org/downloads and download Python 3.12.
  • Run the installer and follow the on-screen prompts.
  • On Windows, check the box that says Add Python to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python3 --version again.
  • Confirm your AWS CLI is configured by running this command:
aws sts get-caller-identity

You should see a JSON response with your Account, UserId, and Arn.

Seeing 'Unable to locate credentials'?

This means your AWS CLI is not configured with access keys. Run aws configure and enter your Access Key ID, Secret Access Key, default region (us-east-1), and output format (json).

Help me configure AWS CLI credentials

Now install the AWS SAM CLI. The installation method depends on your operating system.

macOS

  • Install the SAM CLI using Homebrew by running this command:
brew install aws-sam-cli

Don't have Homebrew?

Homebrew is a package manager for macOS. Install it first by following the instructions at brew.sh, then re-run the command above.

Windows

  • Download the MSI installer from the AWS SAM CLI releases page. The file is named AWS_SAM_CLI_64_PY3.msi.
  • Run the downloaded .msi file and follow the on-screen prompts to complete the installation.
  • Close and reopen your PowerShell window after installation so the PATH update takes effect.
  • Verify the SAM CLI is installed by running this command:
sam --version

You should see output like SAM CLI, version 1.159.1 (your version may be slightly different).

sam command not found?

Make sure you closed and reopened your terminal after installation. On Windows, the MSI installer adds sam to your PATH, but only new terminal sessions pick up the change.

On macOS, if you installed via Homebrew and still see the error, run brew link aws-sam-cli to create the symlink.

Help me fix SAM CLI not being found after installation

Initialize the project and configure Cursor

Every SAM project starts from a scaffold. You will use the Hello World template as a starting point, then restructure the directory layout for the multi-function architecture this project needs.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Initialize a new SAM project by running this command:
sam init

What prompts will I see?

The sam init command launches an interactive wizard. It will ask you several questions. Follow the answers below exactly. Brackets [ ] indicate default values. Press Enter to accept a default.

  • For Which template source would you like to use?, enter 1 to select AWS Quick Start Templates.
  • For Choose an AWS Quick Start application template, enter 1 to select Hello World Example.
  • For Use the most popular runtime and package type? (python3.12 and zip), enter y.
  • For Would you like to enable X-Ray tracing?, press Enter to accept the default (no).
  • For Would you like to enable monitoring using CloudWatch Application Insights?, press Enter to accept the default (no).
  • For Would you like to set Structured Logging in JSON format?, press Enter to accept the default (no).
  • For Project name, enter supply-chain-tracker.

What did sam init just create?

SAM created a supply-chain-tracker/ folder on your Desktop with a starter Hello World template. The most important file is template.yaml. This is where all your AWS resources will be defined.

The scaffold also created a hello_world/ directory with sample code. You will replace this with your multi-function layout next.

  • Move into the project directory and initialize a Git repository by running these commands:
cd supply-chain-tracker
git init

You should see Initialized empty Git repository in the output.

Now restructure the scaffold from a single-function layout to the multi-function layout your project needs.

macOS

  • Remove the scaffold starter code and create the multi-function directory structure by running these commands:
# Remove the Hello World scaffold directories
rm -rf hello_world events tests

# Create the multi-function layout
mkdir -p functions/emitter functions/signer functions/anomaly functions/audit functions/shared
mkdir -p docs scripts tests

Windows

  • Remove the scaffold starter code and create the multi-function directory structure by running these commands:
# Remove the Hello World scaffold directories
Remove-Item -Recurse -Force hello_world, events, tests

# Create the multi-function layout
New-Item -ItemType Directory -Path functions\emitter, functions\signer, functions\anomaly, functions\audit, functions\shared, docs, scripts, tests

Why restructure the scaffold?

The Hello World template puts everything in one hello_world/ folder. Your supply chain tracker has 4 separate Lambda functions (emitter, signer, anomaly detector, audit API) plus shared models. Each function gets its own directory so agents can work on them in parallel without conflicts.

  • Confirm the new directories exist by running:
ls

You should see docs, functions, scripts, tests, template.yaml, and other root files like samconfig.toml.

Now create the .cursorrules file. This file tells every Cursor agent how to name things, what security rules to follow, and which files they own.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the supply-chain-tracker folder, and click Open.
  • Create a new file in the root of the project by right-clicking in the Explorer sidebar and selecting New File.
  • Name the file .cursorrules.
  • Paste the following content into .cursorrules:
# Supply Chain Tracker - DevSecOps Team Conventions
# Every Cursor agent reads this file before writing code.

## Naming Standards
# Python files: snake_case (e.g., chain_event.py, anomaly_rules.py)
# Functions: snake_case (e.g., sign_event, verify_chain)
# Classes: PascalCase (e.g., ChainEvent, AnomalyReport)
# Constants: SCREAMING_SNAKE_CASE (e.g., KMS_KEY_ID, TABLE_NAME)
# Lambda handlers: app.py with lambda_handler function in each function directory
# Environment variables: SCREAMING_SNAKE_CASE, prefixed with service context

## Security Rules
# NEVER hardcode AWS credentials, KMS key IDs, or secrets in source code
# All secrets come from environment variables defined in template.yaml
# IAM policies follow least-privilege (NIST SSDF PS.1.1)
# All KMS operations use ECDSA_SHA_256 with ECC_NIST_P256 keys
# Input validation on all API Gateway endpoints (alphanumeric + hyphens, max 64 chars)
# CORS headers on all public API responses
# Structured JSON logging for all Lambda functions (NIST SSDF RV.1.1)

## Code Style
# Python 3.12 runtime for all Lambda functions
# Type hints on all function signatures
# Docstrings on all public functions
# Imports grouped: stdlib, third-party, local (separated by blank lines)
# Canonical JSON serialization: json.dumps(payload, sort_keys=True, separators=(',', ':'))
# Error handling: catch specific exceptions, log structured errors, return proper HTTP status codes
# DynamoDB access via boto3.resource, not boto3.client
# Each Lambda function directory has: app.py, __init__.py, requirements.txt

## File Ownership (Agent Boundaries)
# Agent 1 (Infra): template.yaml, samconfig.toml
# Agent 2 (Chain): functions/emitter/, functions/shared/, scripts/
# Agent 3 (Security): functions/signer/, docs/key-rotation-runbook.md
# Agent 4 (Quality): functions/anomaly/, tests/
# Agent 5 (Platform): functions/audit/, docs/openapi.yaml

## Compliance Tags
# All AWS resources tagged: project:supply-chain-tracker, compliance:slsa-l1
# SLSA L1: scripted build via SAM template, provenance via signed chain events
# NIST SSDF: PS.1.1 (least privilege), PS.3.2 (provenance), PW.4.1 (docs), RV.1.1 (logging)

What does .cursorrules do?

This file is Cursor's project-level instruction set. Every agent reads it before generating code. It enforces consistent naming, prevents hardcoded secrets, and keeps each agent within its file ownership boundaries.

The File Ownership section is critical. It prevents Agent 2 (Chain) from editing template.yaml (that belongs to Agent 1) and prevents Agent 5 (Platform) from touching the test suite (that belongs to Agent 4). This is how you avoid merge conflicts.

  • Save .cursorrules by pressing Cmd+S (macOS) or Ctrl+S (Windows).

You should see .cursorrules appear in the Explorer sidebar at the root of your project.

Don't see .cursorrules in the sidebar?

Files starting with a dot are hidden by default on some systems. In Cursor's Explorer sidebar, they should still appear. If you cannot see the file, check that you saved it in the project root (supply-chain-tracker/) and not inside a subfolder.

Help me find my .cursorrules file

Set up the Cursor Agents Window and 5 agent worktrees

Cursor's Agents Window lets you run multiple AI agents in parallel, each in its own isolated Git worktree. A worktree is a separate working copy of your repo. Changes in one worktree do not affect the others until you merge them.

Before spinning up agents, you will use Plan Mode to generate the architecture document. This is your engineering manager "standup" moment.

  • Open the Cursor Command Palette by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows).
  • Type Agents Window and press Enter to open the Agents Window.

A new pane opens showing your agent tabs. This is your command center for parallel execution.

First, use Plan Mode to generate the architecture topology. Plan Mode makes the agent research your codebase and create an implementation plan before writing any code.

  • In the first agent tab, press Shift+Tab to toggle Plan Mode on.
  • Paste the following prompt into the agent input and press Enter:
Create docs/architecture.md with a Mermaid diagram of the full system topology for our supply chain tracker.

The diagram should show these components and their connections:
- Lambda functions: Emitter, Signer (sign + verify), Anomaly Detector, Audit API
- DynamoDB table: ChainEvents (partition key: chain_id, sort key: timestamp)
- API Gateway HTTP API with routes: GET /audit/{chain_id} and GET /audit/{chain_id}/verify
- KMS asymmetric signing key (ECC_NIST_P256, SIGN_VERIFY)
- CloudTrail capturing KMS sign/verify operations
- IAM roles with least-privilege boundaries

Connections:
- Emitter invokes Signer, then writes signed events to DynamoDB
- Audit API queries DynamoDB and invokes Anomaly Detector
- Anomaly Detector queries DynamoDB and calls Signer for verification
- Signer calls KMS for sign and verify operations
- CloudTrail audits all KMS operations
- API Gateway routes to Audit API Lambda

Also include an Agent Ownership table mapping each of the 5 agents to their file boundaries (from .cursorrules).

Read .cursorrules for the conventions.

What is Plan Mode?

Plan Mode tells the agent to research your codebase, ask clarifying questions, and present an implementation plan before it builds anything. It reads your .cursorrules file and the existing project structure first. Toggle it on with Shift+Tab and off the same way.

  • Review the agent's plan when it appears. If it looks correct, click Apply to have it create the docs/architecture.md file.
  • Open docs/architecture.md in the Explorer sidebar and confirm it contains a Mermaid diagram with all the components listed above.

Preview the Mermaid diagram

To see the diagram rendered visually, paste the Mermaid code block into mermaid.live. You should see boxes for each Lambda, DynamoDB, API Gateway, and KMS with arrows showing their connections.

Now spin up the 5 agent worktrees. Each agent gets its own isolated workspace and domain assignment.

  • In the Agents Window, click the + button to create a new agent tab. Repeat until you have 5 agent tabs total.
  • In the first agent tab, type /worktree to spin up an isolated worktree. This agent is Agent 1 (Infra Team).
  • In the second agent tab, type /worktree to create Agent 2 (Chain Team).
  • In the third agent tab, type /worktree to create Agent 3 (Security Team).
  • In the fourth agent tab, type /worktree to create Agent 4 (Quality Team).
  • In the fifth agent tab, type /worktree to create Agent 5 (Platform Team).

What does /worktree do?

The /worktree command creates an isolated Git worktree for each agent. Cursor manages the worktree creation automatically. Each agent can edit files without affecting the others.

When you are ready to integrate, you will review each agent's changes in Cursor's diff view and merge them back to main. This is exactly how real engineering teams work with feature branches.

Each agent tab should now show it is running in its own worktree. You will not send prompts to these agents yet. That happens in Steps 2 through 6.

Agent tab shows an error when running /worktree?

Make sure you initialized a Git repository in the project root (git init from earlier in this step). Worktrees require a Git repo. Also make sure you have committed at least once. Run git add -A && git commit -m "Initial scaffold" in the integrated terminal, then try /worktree again.

Help me set up Cursor agent worktrees

✔️ Awesome, I've got everything!

Double check you have saved all your files. You should have .cursorrules, docs/architecture.md, template.yaml, and the functions/ directory structure in your project.

ⓧ I'd like to double check the full code

Here is the full .cursorrules file. Make sure yours matches:

# Supply Chain Tracker - DevSecOps Team Conventions
# Every Cursor agent reads this file before writing code.

## Naming Standards
# Python files: snake_case (e.g., chain_event.py, anomaly_rules.py)
# Functions: snake_case (e.g., sign_event, verify_chain)
# Classes: PascalCase (e.g., ChainEvent, AnomalyReport)
# Constants: SCREAMING_SNAKE_CASE (e.g., KMS_KEY_ID, TABLE_NAME)
# Lambda handlers: app.py with lambda_handler function in each function directory
# Environment variables: SCREAMING_SNAKE_CASE, prefixed with service context

## Security Rules
# NEVER hardcode AWS credentials, KMS key IDs, or secrets in source code
# All secrets come from environment variables defined in template.yaml
# IAM policies follow least-privilege (NIST SSDF PS.1.1)
# All KMS operations use ECDSA_SHA_256 with ECC_NIST_P256 keys
# Input validation on all API Gateway endpoints (alphanumeric + hyphens, max 64 chars)
# CORS headers on all public API responses
# Structured JSON logging for all Lambda functions (NIST SSDF RV.1.1)

## Code Style
# Python 3.12 runtime for all Lambda functions
# Type hints on all function signatures
# Docstrings on all public functions
# Imports grouped: stdlib, third-party, local (separated by blank lines)
# Canonical JSON serialization: json.dumps(payload, sort_keys=True, separators=(',', ':'))
# Error handling: catch specific exceptions, log structured errors, return proper HTTP status codes
# DynamoDB access via boto3.resource, not boto3.client
# Each Lambda function directory has: app.py, __init__.py, requirements.txt

## File Ownership (Agent Boundaries)
# Agent 1 (Infra): template.yaml, samconfig.toml
# Agent 2 (Chain): functions/emitter/, functions/shared/, scripts/
# Agent 3 (Security): functions/signer/, docs/key-rotation-runbook.md
# Agent 4 (Quality): functions/anomaly/, tests/
# Agent 5 (Platform): functions/audit/, docs/openapi.yaml

## Compliance Tags
# All AWS resources tagged: project:supply-chain-tracker, compliance:slsa-l1
# SLSA L1: scripted build via SAM template, provenance via signed chain events
# NIST SSDF: PS.1.1 (least privilege), PS.3.2 (provenance), PW.4.1 (docs), RV.1.1 (logging)

Your project directory structure should look like this:

supply-chain-tracker/
  .cursorrules
  .gitignore
  README.md
  samconfig.toml
  template.yaml
  docs/
    architecture.md
  functions/
    emitter/
    signer/
    anomaly/
    audit/
    shared/
  scripts/
  tests/

Your environment is fully set up. You have SAM CLI installed, a project scaffold restructured for multi-function development, team conventions locked in, and 5 parallel agents standing by. Next up, Agent 1 builds the infrastructure template that defines every AWS resource in the system.

Agent 1 Builds the Infrastructure (SAM Template)

Your environment is ready and all 5 agents are standing by in their worktrees. Time to start building.

Every system starts with infrastructure. Before any function code gets written, Agent 1 (your Infra Team) needs to define the full AWS resource topology in a single SAM template: Lambda functions, DynamoDB table, API Gateway, KMS signing key, and all the security controls that make this system trustworthy.

In this step, get ready to:
  • Direct Agent 1 to define all AWS resources in template.yaml.
  • Add security and compliance controls including CloudTrail logging, encryption, throttling, and resource tagging.
  • Validate the template and confirm a clean build.
Define the SAM template resources

The template.yaml file is the single source of truth for every AWS resource your system needs. Agent 1 will replace the scaffold placeholder with a fully defined Infrastructure as Code template.

What is a SAM template?

A SAM template is a YAML file that declaratively defines your AWS infrastructure. Instead of clicking through the AWS Console to create each resource, you describe everything in code.

When you run sam deploy later, CloudFormation reads the template and creates all resources automatically. This is the serverless equivalent of a Kubernetes manifest or Terraform config.

  • Switch to the Agent 1 (Infra) tab in the Agents Window.
  • Paste the following prompt into the agent's input field:
## Agent 1: Infrastructure Team - Define SAM Template Resources

You are the Infrastructure Team lead. Your job is to fully define `template.yaml`
for a supply chain tracking system. Replace the existing scaffold content entirely.

Read `.cursorrules` for naming conventions and security rules before writing any code.
Read `docs/architecture.md` for the full system topology.

### Resources to define:

1. **Globals** section:
   - Runtime: python3.12
   - Timeout: 30 seconds
   - MemorySize: 256 MB
   - Environment variables on every function:
     - KMS_KEY_ID: reference the signing key ARN via !GetAtt SigningKey.Arn
     - DYNAMODB_TABLE_NAME: reference the table via !Ref ChainEventsTable
   - Global tags: project=supply-chain-tracker, compliance=slsa-l1

2. **KMS Signing Key** (SigningKey):
   - Type: AWS::KMS::Key
   - KeySpec: ECC_NIST_P256
   - KeyUsage: SIGN_VERIFY
   - KeyPolicy granting root account full kms:* access
   - Tags: project=supply-chain-tracker, compliance=slsa-l1

3. **DynamoDB Table** (ChainEventsTable):
   - TableName: ChainEvents
   - BillingMode: PAY_PER_REQUEST
   - Partition key: chain_id (S)
   - Sort key: timestamp (S)
   - Do NOT specify SSESpecification (AWS-owned encryption is the default and free)
   - Tags: project=supply-chain-tracker, compliance=slsa-l1

4. **HTTP API Gateway** (AuditHttpApi):
   - Type: AWS::Serverless::HttpApi
   - StageName: prod
   - CORS: AllowOrigins ['*'], AllowMethods [GET], AllowHeaders [Content-Type]
   - DefaultRouteSettings with ThrottlingBurstLimit: 100, ThrottlingRateLimit: 50
   - Tags: project=supply-chain-tracker, compliance=slsa-l1

5. **EmitterFunction**:
   - CodeUri: functions/emitter/
   - Handler: app.lambda_handler
   - Policies: DynamoDBCrudPolicy on ChainEventsTable + lambda:InvokeFunction on SignerVerifierFunction

6. **SignerVerifierFunction**:
   - CodeUri: functions/signer/
   - Handler: app.lambda_handler
   - Policies: kms:Sign, kms:Verify, kms:DescribeKey on SigningKey

7. **AnomalyDetectorFunction**:
   - CodeUri: functions/anomaly/
   - Handler: app.lambda_handler
   - Policies: DynamoDBReadPolicy on ChainEventsTable + kms:Verify, kms:DescribeKey on SigningKey

8. **AuditApiFunction**:
   - CodeUri: functions/audit/
   - Handler: app.lambda_handler
   - Events: HttpApi GET /audit/{chain_id} and GET /audit/{chain_id}/verify on AuditHttpApi
   - Policies: DynamoDBReadPolicy on ChainEventsTable + kms:Verify, kms:DescribeKey on SigningKey + lambda:InvokeFunction on AnomalyDetectorFunction

9. **Outputs** section:
   - AuditApiUrl: the API Gateway endpoint URL
   - SigningKeyArn: the KMS key ARN
   - ChainEventsTableName: the DynamoDB table name

### Rules:
- Every Lambda function gets its own Tags block: project=supply-chain-tracker, compliance=slsa-l1
- IAM policies follow least privilege. No wildcards on Resource.
- Use SAM policy templates (DynamoDBCrudPolicy, DynamoDBReadPolicy) where available.
- Use inline Statement policies for KMS and Lambda invoke permissions.
- Single blank lines between logical sections. No double blank lines.
- Add inline comments explaining each resource's purpose.

What does this prompt tell Agent 1 to build?

  • A Globals section that sets shared configuration (runtime, timeout, environment variables) across all Lambda functions.
  • A KMS asymmetric signing key using the ECC_NIST_P256 curve for ECDSA signatures. This is the cryptographic heart of the system.
  • Four Lambda functions, each with least-privilege IAM policies scoped to only the resources they need.
  • A DynamoDB table using chain_id as the partition key and timestamp as the sort key, so events within a chain are always ordered.
  • An HTTP API Gateway fronting the Audit function with two public routes.

Agent 1 will start generating the template. This takes 1-2 minutes depending on your machine.

  • Wait for Agent 1 to finish generating the code.
  • Click Apply in the agent's diff view to write the changes to template.yaml.

Now put on your Engineering Manager hat and review what Agent 1 produced.

  • Open template.yaml in the Cursor editor and verify these resources exist:

Why least-privilege IAM policies?

Each Lambda function only gets permissions for the exact actions it needs. The Emitter can write to DynamoDB and invoke the Signer, but it cannot call KMS directly. The Signer can sign and verify with KMS, but it cannot read or write DynamoDB.

This follows NIST SSDF practice PS.1.1: protect the software by applying minimal access controls. If one function is compromised, the blast radius is limited to its own permissions.

Agent 1 generated something different?

AI agents sometimes deviate from the prompt. If Agent 1 used AWS::Serverless::Api (REST API) instead of AWS::Serverless::HttpApi, ask it to change to HttpApi. REST API costs $3.50 per million requests versus $1.00 for HTTP API.

If any resource names differ from the prompt (like ChainTable instead of ChainEventsTable), ask Agent 1 to correct the naming to match the architecture diagram.

Agent 1 produced an error or no output at all.

Add security and compliance controls

The core resources are in place, but a real supply chain system needs an audit trail. Every time someone signs or verifies a chain event, that operation needs to be logged for compliance evidence.

  • In the same Agent 1 tab, paste this follow-up prompt:
## Agent 1: Add Security and Compliance Controls

Add the following security resources to template.yaml, below the existing
Lambda function definitions but above the Outputs section.

### CloudTrail logging for KMS operations:

1. **KmsAuditTrail** (AWS::CloudTrail::Trail):
   - TrailName: supply-chain-kms-audit
   - IsLogging: true
   - S3BucketName: reference AuditTrailBucket
   - EventSelectors: filter to AWS::KMS::Key data events on the SigningKey ARN only
   - Tags: project=supply-chain-tracker, compliance=slsa-l1

2. **AuditTrailBucket** (AWS::S3::Bucket):
   - BucketName: !Sub 'supply-chain-audit-${AWS::AccountId}'
   - Tags: project=supply-chain-tracker, compliance=slsa-l1

3. **AuditTrailBucketPolicy** (AWS::S3::BucketPolicy):
   - Allow cloudtrail.amazonaws.com to s3:GetBucketAcl on the bucket
   - Allow cloudtrail.amazonaws.com to s3:PutObject on the bucket/* with condition s3:x-amz-acl=bucket-owner-full-control

### Verification checklist:
- DynamoDB table does NOT have SSESpecification set (AWS-owned encryption is the default and free)
- API Gateway DefaultRouteSettings has ThrottlingBurstLimit: 100, ThrottlingRateLimit: 50
- Every resource has project and compliance tags
- No IAM policy uses Resource: '*' except the KMS key policy for root account access
- All inline comments explain the purpose, not just the resource name

What are these security controls for?

  • CloudTrail logs every kms:Sign and kms:Verify call to an S3 bucket. This is the compliance evidence that NIST SSDF RV.1.1 requires.
  • DynamoDB encryption at rest is enabled by default with an AWS-owned key at no extra cost. No template configuration needed.
  • API Gateway throttling caps requests at 100 burst / 50 sustained per second to prevent abuse of the public audit endpoint.
  • Resource tags (project:supply-chain-tracker and compliance:slsa-l1) enable governance queries and cost tracking across all resources.
  • Wait for Agent 1 to finish, then click Apply.
  • Review the diff to confirm the CloudTrail trail, S3 bucket, and bucket policy were added.
  • Save template.yaml.

Why CloudTrail instead of just CloudWatch?

CloudWatch captures Lambda execution logs. CloudTrail captures AWS API calls themselves, including the caller identity, timestamp, and the exact KMS key used.

For compliance, you need both. CloudTrail proves who signed an event and when, not just that the Lambda function ran.

Agent 1 set SSESpecification on DynamoDB?

If the template includes SSESpecification: SSEEnabled: true with SSEType: KMS, ask Agent 1 to remove the entire SSESpecification block. DynamoDB encrypts at rest by default with an AWS-owned key at no cost. Setting SSEType: KMS switches to a KMS-managed key, which adds unnecessary charges.

Help me fix the DynamoDB encryption configuration in my SAM template.

Validate and build the template

The template is written, but you need to confirm the YAML syntax is valid and all functions can be compiled. SAM provides two commands for this.

  • Open the integrated terminal in Cursor by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Make sure you are inside the supply-chain-tracker directory where template.yaml lives.
  • Validate the template syntax by running this command:
sam validate

You should see a message ending with is a valid SAM Template.

Seeing a validation error?

The most common issue is an indentation error in the YAML. YAML is whitespace-sensitive, and a single misaligned line will cause sam validate to fail.

Copy the error message and paste it back into Agent 1's chat. The agent can usually fix YAML issues in one pass.

Help me debug my SAM template validation error.

  • Build all functions by running this command:
sam build

What does sam build do?

sam build reads template.yaml, finds each function's CodeUri directory, installs its dependencies from requirements.txt, and packages everything into a .aws-sam/build/ directory ready for deployment.

You should see Build Succeeded in the terminal output.

Function directories may not have code yet

The sam build command may show warnings if the functions/ directories are empty or contain only placeholder files. Agents 2-5 will write the actual function code in the next steps. As long as the build does not error out, you are good to proceed.

Build failed?

If sam build fails with a CodeUri error, the function directories may not exist yet. Create the missing directories by running:

mkdir -p functions/emitter functions/signer functions/anomaly functions/audit

Each directory also needs a minimal app.py and requirements.txt file for the build to succeed. Ask Agent 1 to create placeholder files in each directory.

Help me fix my sam build failure.

✔️ Awesome, I've got everything!

Double check you have saved template.yaml.

ⓧ I'd like to double check the full code

Your template.yaml should look similar to this. Your agent may have slightly different comments or formatting, but the resource names, types, and properties should match.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Supply Chain Tracker - Cryptographically signed supply chain tracking
  with anomaly detection and public audit API.

Globals:
  Function:
    Timeout: 30
    Runtime: python3.12
    MemorySize: 256
    Environment:
      Variables:
        KMS_KEY_ID: !GetAtt SigningKey.Arn
        DYNAMODB_TABLE_NAME: !Ref ChainEventsTable
    Tags:
      project: supply-chain-tracker
      compliance: slsa-l1

Resources:
  # KMS asymmetric signing key for ECDSA signatures
  SigningKey:
    Type: AWS::KMS::Key
    Properties:
      Description: ECC_NIST_P256 key for signing supply chain events
      KeySpec: ECC_NIST_P256
      KeyUsage: SIGN_VERIFY
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: EnableRootAccountAccess
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
      Tags:
        - Key: project
          Value: supply-chain-tracker
        - Key: compliance
          Value: slsa-l1

  # DynamoDB table for chain events (encrypted at rest by default)
  ChainEventsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ChainEvents
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: chain_id
          AttributeType: S
        - AttributeName: timestamp
          AttributeType: S
      KeySchema:
        - AttributeName: chain_id
          KeyType: HASH
        - AttributeName: timestamp
          KeyType: RANGE
      Tags:
        - Key: project
          Value: supply-chain-tracker
        - Key: compliance
          Value: slsa-l1

  # HTTP API Gateway with throttling and CORS
  AuditHttpApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      DefaultRouteSettings:
        ThrottlingBurstLimit: 100
        ThrottlingRateLimit: 50
      CorsConfiguration:
        AllowOrigins:
          - '*'
        AllowMethods:
          - GET
        AllowHeaders:
          - Content-Type
      Tags:
        project: supply-chain-tracker
        compliance: slsa-l1

  # Emitter Lambda - generates chain events per party
  EmitterFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/emitter/
      Handler: app.lambda_handler
      Description: Emits signed chain events for producer, shipper, and retailer
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ChainEventsTable
        - Statement:
            - Effect: Allow
              Action:
                - lambda:InvokeFunction
              Resource: !GetAtt SignerVerifierFunction.Arn
      Tags:
        project: supply-chain-tracker
        compliance: slsa-l1

  # Signer/Verifier Lambda - KMS ECDSA sign and verify operations
  SignerVerifierFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/signer/
      Handler: app.lambda_handler
      Description: Signs and verifies chain events using KMS ECDSA_SHA_256
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - kms:Sign
                - kms:Verify
                - kms:DescribeKey
              Resource: !GetAtt SigningKey.Arn
      Tags:
        project: supply-chain-tracker
        compliance: slsa-l1

  # Anomaly Detector Lambda - validates chain integrity
  AnomalyDetectorFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/anomaly/
      Handler: app.lambda_handler
      Description: Detects chain integrity anomalies using 6 validation rules
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ChainEventsTable
        - Statement:
            - Effect: Allow
              Action:
                - kms:Verify
                - kms:DescribeKey
              Resource: !GetAtt SigningKey.Arn
      Tags:
        project: supply-chain-tracker
        compliance: slsa-l1

  # Audit API Lambda - public chain verification endpoint
  AuditApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/audit/
      Handler: app.lambda_handler
      Description: Public audit API for chain queries and integrity verification
      Events:
        GetChain:
          Type: HttpApi
          Properties:
            ApiId: !Ref AuditHttpApi
            Path: /audit/{chain_id}
            Method: GET
        VerifyChain:
          Type: HttpApi
          Properties:
            ApiId: !Ref AuditHttpApi
            Path: /audit/{chain_id}/verify
            Method: GET
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref ChainEventsTable
        - Statement:
            - Effect: Allow
              Action:
                - kms:Verify
                - kms:DescribeKey
              Resource: !GetAtt SigningKey.Arn
        - Statement:
            - Effect: Allow
              Action:
                - lambda:InvokeFunction
              Resource: !GetAtt AnomalyDetectorFunction.Arn
      Tags:
        project: supply-chain-tracker
        compliance: slsa-l1

  # CloudTrail for KMS operation audit logging
  KmsAuditTrail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: supply-chain-kms-audit
      IsLogging: true
      S3BucketName: !Ref AuditTrailBucket
      EventSelectors:
        - ReadWriteType: All
          DataResources:
            - Type: AWS::KMS::Key
              Values:
                - !GetAtt SigningKey.Arn
      Tags:
        - Key: project
          Value: supply-chain-tracker
        - Key: compliance
          Value: slsa-l1

  # S3 bucket for CloudTrail logs
  AuditTrailBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'supply-chain-audit-${AWS::AccountId}'
      Tags:
        - Key: project
          Value: supply-chain-tracker
        - Key: compliance
          Value: slsa-l1

  # Bucket policy allowing CloudTrail to write logs
  AuditTrailBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AuditTrailBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AWSCloudTrailAclCheck
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:GetBucketAcl
            Resource: !GetAtt AuditTrailBucket.Arn
          - Sid: AWSCloudTrailWrite
            Effect: Allow
            Principal:
              Service: cloudtrail.amazonaws.com
            Action: s3:PutObject
            Resource: !Sub '${AuditTrailBucket.Arn}/*'
            Condition:
              StringEquals:
                s3:x-amz-acl: bucket-owner-full-control

Outputs:
  AuditApiUrl:
    Description: URL for the public audit API
    Value: !Sub 'https://${AuditHttpApi}.execute-api.${AWS::Region}.amazonaws.com/prod'
  SigningKeyArn:
    Description: ARN of the KMS signing key
    Value: !GetAtt SigningKey.Arn
  ChainEventsTableName:
    Description: Name of the DynamoDB table
    Value: !Ref ChainEventsTable

Agent 1 just delivered the entire infrastructure foundation in a single template. Next up, Agent 2 builds the chain emitter functions that simulate how goods move between parties.

Agent 2 Builds the Chain Emitter Functions

Your infrastructure foundation is locked in. Agent 1's SAM template defines every AWS resource your system needs, validates cleanly, and builds without errors.

Now Agent 2 (Chain Team) steps in to build the data backbone. Every supply chain handoff needs a consistent event schema that all functions can read, write, and verify. Agent 2 will define this shared model, build the Lambda function that creates and records events, and write a simulation script to test the full 3-party chain.

In this step, get ready to:
  • Define the ChainEvent schema that all functions share.
  • Build the emitter Lambda that creates, signs, and stores chain events.
  • Create a simulation script that exercises the full producer-shipper-retailer flow.
Define the 3-party event model

Chain events are the atomic unit of your supply chain tracker. Every handoff between parties is recorded as a ChainEvent with a unique ID, timestamp, party identifier, and a signature field that gets filled when the signer processes it.

All five agents need to agree on this schema, which is why it lives in a shared module. Agent 2 defines it first so Agents 3, 4, and 5 can import the same data model when they build signing, anomaly detection, and auditing.

  • Switch to the Agent 2 tab in your Cursor Agents Window.
  • Direct Agent 2 to create the shared event model by pasting this prompt:
You are Agent 2 (Chain Team). Your first task is to create the shared event model.

Create functions/shared/__init__.py (empty) and functions/shared/models.py.

In models.py, define a ChainEvent dataclass with these fields:
- event_id: str (UUID, generated via uuid.uuid4())
- chain_id: str (shipment identifier)
- party: str (one of: "producer", "shipper", "retailer")
- event_type: str (one of: "CREATED", "SHIPPED", "RECEIVED", "DELIVERED")
- timestamp: str (ISO 8601 format)
- payload: dict (contains product_name, quantity, temperature_celsius)
- previous_event_id: str or None (None for the first event in a chain)
- signature: str (empty string until the signer populates it)

Include these methods:
- to_dict(): returns all fields as a JSON-serializable dictionary
- from_dict(data): classmethod that reconstructs a ChainEvent from a dictionary
- to_canonical_json(): returns a sorted-keys, no-whitespace JSON string of all fields EXCEPT signature (this is the canonical form that gets signed)

Define VALID_PARTIES = ["producer", "shipper", "retailer"] and VALID_EVENT_TYPES = ["CREATED", "SHIPPED", "RECEIVED", "DELIVERED"] as module-level constants.

Follow the conventions in .cursorrules.

What will Agent 2 create?

The ChainEvent dataclass is the contract between all five agents. Here is what each key field does.

  • event_id uniquely identifies each event using a UUID.
  • chain_id groups all events belonging to the same shipment.
  • previous_event_id links events into a verifiable chain, like a linked list.
  • signature starts empty and gets populated later by Agent 3's signing harness.
  • to_canonical_json() produces a deterministic string for signing. Sorted keys and no whitespace ensure the same payload always produces the same signature.

Wait for Agent 2 to finish generating the files.

  • Review Agent 2's output in the diff view.
  • Confirm that functions/shared/models.py exists by running:
ls functions/shared/

You should see __init__.py and models.py listed.

  • Open models.py and verify all 8 fields are present: event_id, chain_id, party, event_type, timestamp, payload, previous_event_id, and signature.
  • Confirm the to_canonical_json() method excludes the signature field from its output.

Agent 2 didn't create the file?

  • Make sure Agent 2 is running in its own worktree. If it shares the main branch, file creation may conflict with Agent 1's work.
  • Check that the prompt mentions the exact file path functions/shared/models.py. The agent needs an explicit path to create the right directory structure.
  • If the agent created the file in a different location, ask it to move the file to the correct path.

Why isn't Agent 2 creating files in the right location?

Build the emitter Lambda

The emitter is the function that actually records supply chain events. When a producer ships goods, a shipper handles transit, or a retailer receives delivery, the emitter creates the corresponding event, gets it signed, and stores it in DynamoDB.

The critical feature is ordering enforcement. The system rejects out-of-order events because a shipper cannot log a handoff if the producer never created the original shipment. This is the exact verification gap that allowed contaminated cough syrup to reach children in three countries without anyone catching the broken chain.

  • In the Agent 2 tab, direct it to build the emitter function by pasting this prompt:
Next task, Agent 2. Build the emitter Lambda function.

Create functions/emitter/__init__.py (empty) and functions/emitter/app.py.

The Lambda handler (lambda_handler) receives an event dict with:
- chain_id: str (required)
- party: str (required, one of: producer/shipper/retailer)
- payload: dict (optional, use sample product data as default)

Handler logic:
1. Validate the party parameter against VALID_PARTIES from shared models
2. Query the DynamoDB table (name from DYNAMODB_TABLE_NAME env var) for all existing events with this chain_id, sorted by timestamp
3. Enforce ordering rules:
   - "producer" must be the first event (no existing events for this chain_id)
   - "shipper" requires exactly 1 existing event with party="producer"
   - "retailer" requires exactly 2 existing events (producer then shipper, in order)
   - Return HTTP 400 with a JSON body containing a descriptive error message if ordering is violated
4. Map party to event_type: producer->CREATED, shipper->SHIPPED, retailer->DELIVERED
5. Create a ChainEvent with a new UUID, current ISO 8601 timestamp, and previous_event_id from the last existing event (None if this is the first)
6. Invoke the SignerVerifierFunction (name from SIGNER_FUNCTION_NAME env var) synchronously to sign the event's canonical JSON
7. Attach the returned signature to the ChainEvent
8. Write the signed event to DynamoDB using put_item
9. Return HTTP 200 with the signed event as JSON

Use boto3 for DynamoDB and Lambda invocation. Import ChainEvent from functions.shared.models.
Wrap the handler body in try/except and return HTTP 500 with error details for unexpected failures.
Add inline comments explaining each logical section.
Follow .cursorrules conventions.

How does ordering enforcement work?

The emitter queries DynamoDB for all existing events on a given chain_id before writing a new one. It counts how many events exist and checks which parties have already submitted.

  • A producer event is only allowed when 0 events exist for the chain.
  • A shipper event is only allowed when exactly 1 event exists and it belongs to the producer.
  • A retailer event is only allowed when exactly 2 events exist (producer, then shipper).

Any submission that breaks this order gets an HTTP 400 rejection. No exceptions. This means a contaminated shipment cannot skip the shipper verification step and go straight to a retailer.

Why does the emitter call the signer function?

The emitter never touches the KMS signing key directly. It invokes the SignerVerifierFunction as a separate Lambda, passing the event's canonical JSON. The signer computes the cryptographic signature and returns it.

This separation of concerns means only one function needs KMS permissions. If the emitter were compromised, an attacker could not forge signatures because the signing logic lives in a different function with its own IAM role.

Wait for Agent 2 to finish generating the emitter code.

  • Confirm functions/emitter/app.py exists by running:
ls functions/emitter/

You should see __init__.py and app.py listed.

  • Open app.py and review the ordering enforcement logic.
  • Verify the handler queries DynamoDB for existing events before writing a new one.
  • Confirm that out-of-order requests return HTTP 400 with a descriptive error message in JSON format.
  • Check that the handler reads DYNAMODB_TABLE_NAME and SIGNER_FUNCTION_NAME from environment variables, not hardcoded strings.

Add the signer function name to template.yaml

If Agent 1's SAM template only defined KMS_KEY_ID and DYNAMODB_TABLE_NAME as environment variables, the emitter also needs SIGNER_FUNCTION_NAME to know which function to invoke.

Ask Agent 2 to add this env var to the EmitterFunction in template.yaml, referencing the SignerVerifierFunction with a SAM !Ref. You will review this change when you merge Agent 2's worktree in Step 7.

Emitter code missing ordering logic?

  • Some agents skip the DynamoDB query step and write events without checking order. Re-prompt with explicit instructions: "The handler MUST query DynamoDB for existing chain events and reject out-of-order submissions with HTTP 400."
  • If the agent imported boto3 but did not use boto3.resource("dynamodb").Table() for the query, ask it to switch to the resource API for cleaner DynamoDB interactions.

My Cursor Agent 2 generated emitter code without ordering enforcement. How do I re-prompt it?

Create the chain simulation script

The emitter handles one event at a time. To test a full supply chain, you need a script that invokes it three times in sequence, once for each party. This simulation script is your end-to-end test harness.

It also includes an inject-break mode that deliberately introduces anomalies. You will use these broken chains in Step 9 to run crisis-inspired demo scenarios against the anomaly detector.

  • In the Agent 2 tab, direct it to create the simulation script by pasting this prompt:
Final task, Agent 2. Create the chain simulation script.

Create a scripts/ directory and write scripts/simulate_chain.py.

This script invokes the EmitterFunction Lambda 3 times to simulate a complete supply chain:
1. Producer creates the shipment (party="producer")
2. Shipper handles transit (party="shipper")
3. Retailer receives delivery (party="retailer")

Requirements:
- Accept a --chain-id argument (default: generate a UUID)
- Accept a --inject-break flag with two modes:
  - "skip-shipper": skip the shipper invocation entirely, then attempt the retailer step (should trigger a 400 ordering error from the emitter)
  - "tamper-timestamp": invoke all 3 parties but pass a timestamp in the retailer event that is EARLIER than the shipper timestamp
- Use boto3 lambda client to invoke the EmitterFunction by name (read from EMITTER_FUNCTION_NAME env var or accept as --function-name argument)
- Use InvocationType="RequestResponse" for synchronous invocation
- Print the full response payload from each invocation
- Print a summary at the end showing chain_id, number of events created, and whether any errors occurred
- Include sample payload data for each party:
  - Producer: product_name="Infant Formula ARA-500", quantity=10000, temperature_celsius=4.0
  - Shipper: product_name="Infant Formula ARA-500", quantity=10000, temperature_celsius=2.5
  - Retailer: product_name="Infant Formula ARA-500", quantity=9950, temperature_celsius=3.8

Use argparse for CLI arguments. Add inline comments explaining each section.
Follow .cursorrules conventions.

What does the inject-break mode do?

The --inject-break flag deliberately creates a broken chain to test whether the system catches it.

  • skip-shipper simulates a pharmaceutical supply chain break. The producer creates the shipment, but the shipper step is skipped entirely. When the retailer tries to submit, the emitter rejects it with HTTP 400 because the chain is out of order.
  • tamper-timestamp simulates timestamp manipulation. All three parties submit events, but the retailer's timestamp is set earlier than the shipper's. The anomaly detector will catch this in Step 5.

Why infant formula as the sample data?

The sample payload references "Infant Formula ARA-500" with temperature tracking. This mirrors the real WHO infant formula contamination crisis (December 2025 to February 2026) where cereulide toxin in ARA oil reached 99 countries. The slight quantity difference between shipper (10,000) and retailer (9,950) simulates expected transit loss.

Wait for Agent 2 to finish generating the simulation script.

  • Confirm the script exists by running:
ls scripts/

You should see simulate_chain.py listed.

  • Open simulate_chain.py and verify it uses argparse for CLI argument handling.
  • Confirm the --inject-break flag supports both skip-shipper and tamper-timestamp modes.
  • Check that the script invokes the Lambda with InvocationType="RequestResponse" for synchronous execution.
  • Verify the sample payloads include product_name, quantity, and temperature_celsius fields.

Script missing the inject-break flag?

  • Some agents generate a clean-path-only simulation and skip the fault injection. Re-prompt Agent 2: "Add a --inject-break argument with choices skip-shipper and tamper-timestamp. The skip-shipper mode skips the shipper invocation. The tamper-timestamp mode passes a backdated ISO 8601 timestamp for the retailer event."
  • If the script uses hardcoded Lambda function names instead of an environment variable or argument, ask Agent 2 to add a --function-name argument so the script works after deployment.

My simulation script is missing fault injection modes. How do I fix it?

You cannot run simulate_chain.py yet because the Lambda functions are not deployed. You will use this script in Step 9 when everything is live on AWS.

✔️ Awesome, all three files look good!

Great. Confirm all three files are saved in Agent 2's worktree. You will merge them into the main branch in Step 7.

ⓧ I'd like to double check what Agent 2 built

Run this command to see all files Agent 2 created:

find functions/shared functions/emitter scripts -type f 2>/dev/null

You should see these files:

  • functions/shared/__init__.py (empty package init).
  • functions/shared/models.py (ChainEvent dataclass with 8 fields, to_dict(), from_dict(), and to_canonical_json()).
  • functions/emitter/__init__.py (empty package init).
  • functions/emitter/app.py (Lambda handler with ordering enforcement, signer invocation, and DynamoDB writes).
  • scripts/simulate_chain.py (3-party simulation with --inject-break flag for skip-shipper and tamper-timestamp modes).

If any file is missing, scroll up in Agent 2's chat history to find where it was supposed to be created. Re-prompt the agent for that specific file.

Agent 2's work is done. The data model, emitter function, and simulation script give your system the ability to create, enforce, and test supply chain events. Next up, Agent 3 builds the cryptographic signing harness that makes every event tamper-proof.

Agent 3 Builds the KMS Signing Harness

Your chain emitter can generate events and write them to DynamoDB, but right now those events are unsigned. Without cryptographic signatures, any party could forge or tamper with events after the fact. This is exactly the gap that allowed contaminated ingredients to pass through pharmaceutical supply chains undetected.

Agent 3 (your Security Team) builds the signing harness. This module uses AWS KMS to sign every chain event with an ECDSA digital signature and verify those signatures on demand. By the end of this step, no event enters the chain without cryptographic proof of who created it and that it has not been altered.

In this step, get ready to:
  • Build a sign function that creates ECDSA signatures via AWS KMS.
  • Build a verify function that validates event integrity on demand.
  • Add retry logic, structured compliance logging, and a key rotation runbook.
Instruct Agent 3 to build the sign function

Cryptographic signing only works if the same input always produces the same signature. JSON objects do not guarantee key order, so {"a":1,"b":2} and {"b":2,"a":1} would produce different digests even though they carry identical data.

The solution is canonical JSON serialization: sort keys alphabetically and strip whitespace before hashing. Agent 3 will use this pattern to produce a deterministic SHA-256 digest, then sign that digest with the KMS ECC_NIST_P256 key.

  • Switch to Agent 3's tab in the Agents Window.
  • Instruct Agent 3 to create functions/signer/app.py with the imports, configuration, helper functions, and the sign_event function.
  • Review Agent 3's output against this reference code:
import json
import hashlib
import base64
import os
import time
import logging
from datetime import datetime, timezone

import boto3
from botocore.exceptions import ClientError

# Configure structured JSON logging for CloudWatch
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Initialize KMS client and load key ID from environment
kms_client = boto3.client("kms")
KMS_KEY_ID = os.environ["KMS_KEY_ID"]

# Retry settings for KMS throttling (free tier has request quotas)
MAX_RETRIES = 3
BASE_DELAY = 0.5


def _canonical_json(event_payload):
    """Serialize payload to canonical JSON: sorted keys, no whitespace."""
    return json.dumps(event_payload, sort_keys=True, separators=(",", ":"))


def _sha256_digest(canonical_str):
    """Compute SHA-256 digest of a canonical JSON string."""
    return hashlib.sha256(canonical_str.encode("utf-8")).digest()


def _retry_with_backoff(operation, operation_name):
    """Retry a KMS call with exponential backoff on ThrottlingException."""
    for attempt in range(MAX_RETRIES):
        try:
            return operation()
        except ClientError as e:
            error_code = e.response["Error"]["Code"]
            if error_code == "ThrottlingException" and attempt < MAX_RETRIES - 1:
                delay = BASE_DELAY * (2 ** attempt)
                logger.warning(json.dumps({
                    "event": "kms_throttled",
                    "operation": operation_name,
                    "attempt": attempt + 1,
                    "retry_delay_seconds": delay
                }))
                time.sleep(delay)
            else:
                raise


def sign_event(event_payload):
    """Sign a chain event using KMS ECDSA_SHA_256."""
    # Create a deterministic representation of the event
    canonical = _canonical_json(event_payload)
    digest = _sha256_digest(canonical)

    # Sign the digest via KMS with retry logic
    def do_sign():
        return kms_client.sign(
            KeyId=KMS_KEY_ID,
            Message=digest,
            MessageType="DIGEST",
            SigningAlgorithm="ECDSA_SHA_256"
        )

    response = _retry_with_backoff(do_sign, "sign")

    # Encode the DER signature as base64 for JSON-safe storage
    signature_b64 = base64.b64encode(response["Signature"]).decode("utf-8")

    # Structured log entry for compliance (NIST SSDF RV.1.1)
    logger.info(json.dumps({
        "event": "sign_operation",
        "key_arn": response["KeyId"],
        "algorithm": response["SigningAlgorithm"],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "digest_hex": digest.hex()
    }))

    return {
        "signature": signature_b64,
        "key_arn": response["KeyId"]
    }

What does this code do?

  • _canonical_json sorts all keys alphabetically and strips whitespace. This guarantees the same payload always produces the same byte string, no matter what order Python dictionaries iterate.
  • _sha256_digest hashes that canonical string into a fixed-size 256-bit digest. KMS signs this digest, not the raw payload. The MessageType="DIGEST" parameter tells KMS the message is already hashed.
  • _retry_with_backoff catches ThrottlingException from KMS and retries with exponentially increasing delays (0.5s, 1s, 2s). The KMS free tier has request quotas, so this prevents transient failures from crashing the chain.
  • sign_event orchestrates the full flow: canonicalize, digest, sign via KMS, encode the signature as base64 for JSON-safe storage, log the operation for compliance, and return the signature plus key ARN.

Why base64 encoding?

KMS returns the signature as raw DER-encoded bytes. Raw bytes cannot be stored directly in JSON or DynamoDB string fields. Base64 converts those bytes into a safe ASCII string that can be stored, transmitted, and decoded back to the original bytes for verification.

  • Confirm Agent 3's output includes the sort_keys=True and separators=(",", ":") arguments in _canonical_json.
  • Confirm the SigningAlgorithm is set to ECDSA_SHA_256 and MessageType is set to DIGEST.

Agent 3 used a different signing approach?

If Agent 3 passes MessageType="RAW" instead of DIGEST, tell it to pre-hash with SHA-256 and use DIGEST mode. KMS has a 4096-byte message limit for raw payloads. Pre-hashing guarantees any payload size works.

If Agent 3 used a symmetric key or AES encryption instead of ECDSA signing, remind it that the project uses an ECC_NIST_P256 asymmetric key with SIGN_VERIFY usage (defined in template.yaml).

Help me fix Agent 3's signing function to use ECDSA_SHA_256 with pre-hashed DIGEST mode

Add the verify function to the signer module

Signing alone is only half the equation. The audit API and anomaly detector both need to verify signatures after the fact. The verify function recomputes the same canonical digest and asks KMS to confirm the signature matches.

  • Instruct Agent 3 to add verify_signature and a lambda_handler to the same functions/signer/app.py file, below the sign_event function.
  • Review Agent 3's output against this reference code:
def verify_signature(event_payload, signature_b64):
    """Verify a chain event signature using KMS."""
    # Recompute the same canonical digest the signer used
    canonical = _canonical_json(event_payload)
    digest = _sha256_digest(canonical)

    # Decode base64 signature back to DER bytes
    signature_bytes = base64.b64decode(signature_b64)

    verified_at = datetime.now(timezone.utc).isoformat()

    # Verify via KMS with retry logic
    def do_verify():
        return kms_client.verify(
            KeyId=KMS_KEY_ID,
            Message=digest,
            MessageType="DIGEST",
            Signature=signature_bytes,
            SigningAlgorithm="ECDSA_SHA_256"
        )

    try:
        response = _retry_with_backoff(do_verify, "verify")
        is_valid = response["SignatureValid"]
    except ClientError:
        is_valid = False

    # Structured log entry for compliance (NIST SSDF RV.1.1)
    logger.info(json.dumps({
        "event": "verify_operation",
        "key_id": KMS_KEY_ID,
        "valid": is_valid,
        "timestamp": verified_at,
        "digest_hex": digest.hex()
    }))

    return {
        "valid": is_valid,
        "verified_at": verified_at
    }


def lambda_handler(event, context):
    """Lambda entry point routing sign and verify requests."""
    action = event.get("action")

    if action == "sign":
        return sign_event(event["payload"])
    elif action == "verify":
        return verify_signature(event["payload"], event["signature"])
    else:
        return {"error": f"Unknown action: {action}"}

What does this code do?

  • verify_signature recomputes the canonical JSON and SHA-256 digest from the event payload. If even a single byte in the payload changed since signing, the digest will differ and verification will fail.
  • The kms:Verify call asks KMS to check whether the signature matches the digest using the same key. KMS returns a SignatureValid boolean.
  • If KMS throws any error during verification (expired key, wrong key, corrupted signature), the except ClientError block catches it and returns valid: False instead of crashing.
  • lambda_handler routes incoming Lambda invocations to either sign_event or verify_signature based on the action field in the event payload.
  • Confirm Agent 3's verify_signature returns both a valid boolean and a verified_at ISO 8601 timestamp.
  • Confirm the lambda_handler handles three cases: sign, verify, and an unknown action fallback.

Verify function returns different results than expected?

If Agent 3 skipped the try/except block around kms:Verify, ask it to wrap the call so that any ClientError returns valid: False instead of raising an exception. A crashed verification function is worse than one that returns "invalid."

If the function does not log every verify operation, remind Agent 3 that compliance logging is required for both sign and verify calls.

Help me fix Agent 3's verify function to handle errors gracefully and log every operation

Double check the complete functions/signer/app.py file matches the reference.

✔️ Awesome, I've got everything!

Make sure the file is saved in Agent 3's worktree before moving on.

ⓧ I'd like to double check the full code

import json
import hashlib
import base64
import os
import time
import logging
from datetime import datetime, timezone

import boto3
from botocore.exceptions import ClientError

# Configure structured JSON logging for CloudWatch
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Initialize KMS client and load key ID from environment
kms_client = boto3.client("kms")
KMS_KEY_ID = os.environ["KMS_KEY_ID"]

# Retry settings for KMS throttling (free tier has request quotas)
MAX_RETRIES = 3
BASE_DELAY = 0.5


def _canonical_json(event_payload):
    """Serialize payload to canonical JSON: sorted keys, no whitespace."""
    return json.dumps(event_payload, sort_keys=True, separators=(",", ":"))


def _sha256_digest(canonical_str):
    """Compute SHA-256 digest of a canonical JSON string."""
    return hashlib.sha256(canonical_str.encode("utf-8")).digest()


def _retry_with_backoff(operation, operation_name):
    """Retry a KMS call with exponential backoff on ThrottlingException."""
    for attempt in range(MAX_RETRIES):
        try:
            return operation()
        except ClientError as e:
            error_code = e.response["Error"]["Code"]
            if error_code == "ThrottlingException" and attempt < MAX_RETRIES - 1:
                delay = BASE_DELAY * (2 ** attempt)
                logger.warning(json.dumps({
                    "event": "kms_throttled",
                    "operation": operation_name,
                    "attempt": attempt + 1,
                    "retry_delay_seconds": delay
                }))
                time.sleep(delay)
            else:
                raise


def sign_event(event_payload):
    """Sign a chain event using KMS ECDSA_SHA_256."""
    # Create a deterministic representation of the event
    canonical = _canonical_json(event_payload)
    digest = _sha256_digest(canonical)

    # Sign the digest via KMS with retry logic
    def do_sign():
        return kms_client.sign(
            KeyId=KMS_KEY_ID,
            Message=digest,
            MessageType="DIGEST",
            SigningAlgorithm="ECDSA_SHA_256"
        )

    response = _retry_with_backoff(do_sign, "sign")

    # Encode the DER signature as base64 for JSON-safe storage
    signature_b64 = base64.b64encode(response["Signature"]).decode("utf-8")

    # Structured log entry for compliance (NIST SSDF RV.1.1)
    logger.info(json.dumps({
        "event": "sign_operation",
        "key_arn": response["KeyId"],
        "algorithm": response["SigningAlgorithm"],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "digest_hex": digest.hex()
    }))

    return {
        "signature": signature_b64,
        "key_arn": response["KeyId"]
    }


def verify_signature(event_payload, signature_b64):
    """Verify a chain event signature using KMS."""
    # Recompute the same canonical digest the signer used
    canonical = _canonical_json(event_payload)
    digest = _sha256_digest(canonical)

    # Decode base64 signature back to DER bytes
    signature_bytes = base64.b64decode(signature_b64)

    verified_at = datetime.now(timezone.utc).isoformat()

    # Verify via KMS with retry logic
    def do_verify():
        return kms_client.verify(
            KeyId=KMS_KEY_ID,
            Message=digest,
            MessageType="DIGEST",
            Signature=signature_bytes,
            SigningAlgorithm="ECDSA_SHA_256"
        )

    try:
        response = _retry_with_backoff(do_verify, "verify")
        is_valid = response["SignatureValid"]
    except ClientError:
        is_valid = False

    # Structured log entry for compliance (NIST SSDF RV.1.1)
    logger.info(json.dumps({
        "event": "verify_operation",
        "key_id": KMS_KEY_ID,
        "valid": is_valid,
        "timestamp": verified_at,
        "digest_hex": digest.hex()
    }))

    return {
        "valid": is_valid,
        "verified_at": verified_at
    }


def lambda_handler(event, context):
    """Lambda entry point routing sign and verify requests."""
    action = event.get("action")

    if action == "sign":
        return sign_event(event["payload"])
    elif action == "verify":
        return verify_signature(event["payload"], event["signature"])
    else:
        return {"error": f"Unknown action: {action}"}
Review the error handling and create the key rotation runbook

Agent 3's code already includes two critical production patterns: retry logic for KMS throttling and structured JSON logging for every cryptographic operation. The last piece is documenting how to rotate the signing key when the time comes.

Why does KMS throttle requests?

Asymmetric Sign/Verify operations are excluded from the 20,000 free tier requests. They cost $0.15 per 10,000 requests. AWS enforces per-second request quotas on KMS to prevent runaway costs and abuse.

The _retry_with_backoff function handles this gracefully. On a ThrottlingException, it waits 0.5 seconds, then 1 second, then 2 seconds before retrying. This exponential backoff pattern prevents the function from hammering KMS during a quota spike.

Why structured JSON logging?

Every logger.info call in the signer writes a JSON object to CloudWatch Logs. This is not just good practice. It is a compliance requirement.

NIST SSDF practice RV.1.1 requires logging of security-relevant operations for vulnerability identification and incident response. Structured JSON (instead of plain text) means you can query, filter, and alert on specific fields like valid: false to catch signature verification failures in real time.

The signing key itself also needs a documented rotation procedure. If a key is ever compromised or reaches its rotation schedule, your team needs a step-by-step playbook.

  • Instruct Agent 3 to create docs/key-rotation-runbook.md with the key rotation procedure.
  • Review Agent 3's output against this reference:
# KMS Key Rotation Runbook

## Overview

This documents the procedure for rotating the ECC_NIST_P256
signing key used by the supply chain tracker signer function.

## When to Rotate

- Scheduled rotation (every 365 days recommended).
- Suspected key compromise.
- Compliance audit requirement.

## Procedure

### 1. Create a New KMS Key

```bash
aws kms create-key \
  --key-spec ECC_NIST_P256 \
  --key-usage SIGN_VERIFY \
  --description "Supply chain signer key (rotated YYYY-MM-DD)" \
  --tags TagKey=project,TagValue=supply-chain-tracker
```

Record the new KeyId from the output.

### 2. Update the SAM Template

In `template.yaml`, replace the `KMS_KEY_ID` environment variable
value with the new key ARN for all Lambda functions.

### 3. Deploy the Update

```bash
sam build
sam deploy
```

### 4. Re-sign Active Chain Events

For any in-progress chains, run the re-signing script:

```bash
python scripts/resign_active_chains.py --new-key-id <NEW_KEY_ID>
```

### 5. Verify the Rotation

Call the audit API for a known chain to confirm signatures
verify correctly with the new key.

### 6. Schedule Old Key Deletion

```bash
aws kms schedule-key-deletion \
  --key-id <OLD_KEY_ID> \
  --pending-window-in-days 30
```

## Rollback

If verification fails after rotation, revert the `KMS_KEY_ID`
in `template.yaml` to the previous key ARN and redeploy.

What does the runbook cover?

  • Creating a new ECC_NIST_P256 key with the same spec and usage as the original.
  • Updating the SAM template so all Lambda functions point to the new key ARN.
  • Re-signing any in-progress chains that were signed with the old key.
  • Scheduling the old key for deletion with a 30-day waiting period (KMS requires a minimum 7-day wait before permanent deletion).
  • Confirm docs/key-rotation-runbook.md exists in Agent 3's worktree.
  • Verify it includes the aws kms create-key command with --key-spec ECC_NIST_P256 and --key-usage SIGN_VERIFY.
  • Verify it includes a rollback procedure in case the new key fails verification.

Agent 3 skipped the runbook or produced a minimal version?

Some agents treat documentation tasks as low-priority. If Agent 3 produced a one-paragraph stub, paste the full reference content into the agent and ask it to expand. The runbook needs concrete CLI commands, not just descriptions.

Help me get Agent 3 to write a complete key rotation runbook with CLI commands

Agent 3's signing harness is complete. Every chain event can now be cryptographically signed and verified, with retry protection and a full compliance audit trail. Next up, Agent 4 builds the anomaly detector that uses these signatures to catch chain breaks in real time.

Agent 4 Builds the Anomaly Detector

Agent 3 gave your system the ability to sign and verify every chain event cryptographically. But signing alone is not enough. If nobody inspects the chain for breaks, tampered timestamps, or missing handoffs, a forged event with a valid-looking structure could slip through undetected.

Agent 4 (Quality Team) builds the anomaly detector. This is the component that would have caught the pharmaceutical supply chain break in The Gambia, where a shipper step was skipped entirely. It enforces 6 integrity rules against every chain, and any violation produces a structured report pinpointing exactly what went wrong.

In this step, get ready to:
  • Define 6 anomaly detection rules that catch missing signatures, broken ordering, and tampered timestamps.
  • Build the anomaly detector Lambda that fetches chain events and runs all rules.
  • Write pytest test cases with mocked DynamoDB to verify each rule catches its target violation.
Define the anomaly rules

The anomaly rules are the core logic that decides whether a supply chain is trustworthy. Each rule targets one specific type of integrity violation. Some rules check individual events (is this event signed?). Others check the full chain sequence (are the parties in the right order?).

What are the 6 anomaly detection rules?

  • Rule 1 (Missing Signature): Every event must have a non-empty signature field.
  • Rule 2 (Invalid Signature): The signature must pass cryptographic verification via AWS KMS.
  • Rule 3 (Ordering Violation): Parties must follow the producer, then shipper, then retailer sequence.
  • Rule 4 (Timestamp Regression): Each event's timestamp must be later than the previous event's timestamp.
  • Rule 5 (Duplicate Event ID): No two events in a chain can share the same event_id.
  • Rule 6 (Missing Link): Every event after the first must reference the previous event's ID via previous_event_id.
  • Switch to Agent 4's tab in the Agents Window.
  • Provide Agent 4 with this prompt:
You are Agent 4 (Quality Team). Your file ownership: functions/anomaly/ and tests/.

Read .cursorrules for project conventions. Read functions/signer/app.py to understand the verify_signature function.

Create functions/anomaly/rules.py with 6 anomaly detection rule functions:

1. check_missing_signature(event) - Returns anomaly dict if event has no signature, else None.
2. check_invalid_signature(event) - Calls verify_signature from functions/signer/app.py. Returns anomaly dict if verification fails, else None. Skips if signature is already missing (Rule 1 handles that).
3. check_ordering_violation(events) - Validates producer -> shipper -> retailer party sequence. Returns list of anomaly dicts.
4. check_timestamp_regression(events) - Each event timestamp must be strictly later than the previous. Returns list of anomaly dicts.
5. check_duplicate_event_id(events) - No duplicate event_ids allowed. Returns list of anomaly dicts.
6. check_missing_previous_event_id_link(events) - Every event after the first must have previous_event_id matching the prior event's event_id. Returns list of anomaly dicts.

Anomaly dict format: {"rule": str, "event_id": str, "description": str}
Per-event rules (1-2) return None or a single dict.
Chain-level rules (3-6) return a list of dicts (empty list if no anomalies).

Define VALID_ORDER = ["producer", "shipper", "retailer"] as a module constant.
Import verify_signature using sys.path manipulation to reach the signer module.
Add inline comments explaining each rule's purpose.

Agent 4 creates functions/anomaly/rules.py. Once the agent finishes, review the generated file in the diff view.

What should I look for in the review?

Rules 1 and 2 operate on a single event. They return None when the event is clean, or a single anomaly dict when it fails. Rule 2 imports verify_signature from the signer module that Agent 3 built.

Rules 3 through 6 operate on the full list of events. They return a list of anomaly dicts (empty if no violations). Rule 3 compares each event's party field against the VALID_ORDER constant. Rule 4 parses ISO 8601 timestamps and compares consecutive pairs.

✔️ Awesome, I've got everything!

Click Apply in the diff view to accept Agent 4's code for rules.py.

ⓧ I'd like to double check the full code

Compare Agent 4's output against this reference. The function signatures and return types must match exactly.

import os
import sys
from datetime import datetime

# Import verify_signature from the signer module
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from signer.app import verify_signature

# Expected ordering of parties in a valid chain
VALID_ORDER = ["producer", "shipper", "retailer"]


def check_missing_signature(event):
    """Rule 1: Every event must have a non-empty signature."""
    if not event.get("signature"):
        return {
            "rule": "missing_signature",
            "event_id": event.get("event_id", "unknown"),
            "description": f"Event {event.get('event_id')} has no signature."
        }
    return None


def check_invalid_signature(event):
    """Rule 2: Signature must pass KMS cryptographic verification."""
    if not event.get("signature"):
        return None  # Missing signatures handled by Rule 1

    try:
        result = verify_signature(event, event["signature"])
        if not result["valid"]:
            return {
                "rule": "invalid_signature",
                "event_id": event["event_id"],
                "description": f"Event {event['event_id']} has an invalid signature."
            }
    except Exception as e:
        return {
            "rule": "invalid_signature",
            "event_id": event["event_id"],
            "description": f"Signature verification failed for {event['event_id']}: {str(e)}"
        }
    return None


def check_ordering_violation(events):
    """Rule 3: Parties must follow producer -> shipper -> retailer order."""
    anomalies = []
    for i, event in enumerate(events):
        party = event.get("party", "")
        if i < len(VALID_ORDER) and party != VALID_ORDER[i]:
            anomalies.append({
                "rule": "ordering_violation",
                "event_id": event["event_id"],
                "description": f"Expected '{VALID_ORDER[i]}' at position {i}, got '{party}'."
            })
    return anomalies


def check_timestamp_regression(events):
    """Rule 4: Each event timestamp must be later than the previous one."""
    anomalies = []
    for i in range(1, len(events)):
        prev_ts = datetime.fromisoformat(events[i - 1]["timestamp"])
        curr_ts = datetime.fromisoformat(events[i]["timestamp"])
        if curr_ts <= prev_ts:
            anomalies.append({
                "rule": "timestamp_regression",
                "event_id": events[i]["event_id"],
                "description": f"Timestamp {events[i]['timestamp']} is not after {events[i - 1]['timestamp']}."
            })
    return anomalies


def check_duplicate_event_id(events):
    """Rule 5: No two events can share the same event_id."""
    seen = {}
    anomalies = []
    for event in events:
        eid = event["event_id"]
        if eid in seen:
            anomalies.append({
                "rule": "duplicate_event_id",
                "event_id": eid,
                "description": f"Event ID {eid} appears more than once in the chain."
            })
        seen[eid] = True
    return anomalies


def check_missing_previous_event_id_link(events):
    """Rule 6: Every event after the first must reference the previous event's ID."""
    anomalies = []
    for i, event in enumerate(events):
        if i == 0:
            continue  # First event has no predecessor
        prev_id = event.get("previous_event_id")
        if prev_id != events[i - 1]["event_id"]:
            anomalies.append({
                "rule": "missing_previous_event_id_link",
                "event_id": event["event_id"],
                "description": f"Expected previous_event_id '{events[i - 1]['event_id']}', got '{prev_id}'."
            })
    return anomalies

Agent 4 produced something different?

Check that Agent 4 is working in its own worktree, not on the main branch. If the agent combined all rules into a single function instead of 6 separate functions, re-prompt with explicit function names.

If the agent cannot import verify_signature, confirm that functions/signer/app.py from Agent 3 is already merged to main or accessible in the worktree.

Help me troubleshoot Agent 4's rules.py output in my Cursor Agents Window.

Build the anomaly detector Lambda

The rules module defines what to check. The anomaly detector Lambda is the orchestrator that fetches a chain's events from DynamoDB, runs every rule, and returns a structured integrity report.

  • In Agent 4's tab, provide this follow-up prompt:
Now create functions/anomaly/app.py - the anomaly detector Lambda handler.

This function:
1. Accepts a chain_id from the event payload (check body, then pathParameters).
2. Fetches all events for that chain from DynamoDB using chain_id as the partition key, sorted ascending by sort key (ScanIndexForward=True).
3. Runs all 6 rules from rules.py against the events.
4. Returns a JSON report: {chain_id, integrity_status: "PASS" or "FAIL", anomalies: [{rule, event_id, description}], checked_at: ISO8601 UTC timestamp}.

Rules 1-2 are per-event (loop over each event). Rules 3-6 are chain-level (pass the full events list).

Return HTTP 400 if chain_id is missing. Return 404 if no events found. Return 200 if integrity passes. Return 422 if anomalies detected.

Use os.environ for CHAIN_EVENTS_TABLE with default "ChainEvents".
Import all 6 rule functions from rules.py.
Add inline comments for each section.

Agent 4 creates functions/anomaly/app.py. Review the generated file once the agent finishes.

What does the anomaly detector do?

The handler splits the 6 rules into two groups. Per-event rules (missing signature, invalid signature) loop over each event individually. Chain-level rules (ordering, timestamps, duplicates, links) receive the full sorted event list.

The integrity_status field is PASS only when zero anomalies are found. A single violation flips the status to FAIL and the HTTP response changes from 200 to 422. The audit API in the next step calls this detector directly.

✔️ Awesome, I've got everything!

Click Apply to accept Agent 4's code for app.py.

ⓧ I'd like to double check the full code

Compare Agent 4's output against this reference. Pay close attention to the import of all 6 rule functions and the HTTP status code logic.

import os
import json
import boto3
from datetime import datetime, timezone
from boto3.dynamodb.conditions import Key

from rules import (
    check_missing_signature,
    check_invalid_signature,
    check_ordering_violation,
    check_timestamp_regression,
    check_duplicate_event_id,
    check_missing_previous_event_id_link,
)

# Initialize DynamoDB table reference
dynamodb = boto3.resource("dynamodb")
TABLE_NAME = os.environ.get("CHAIN_EVENTS_TABLE", "ChainEvents")
table = dynamodb.Table(TABLE_NAME)


def fetch_chain_events(chain_id):
    """Fetch all events for a chain, sorted by sort key (ascending)."""
    response = table.query(
        KeyConditionExpression=Key("chain_id").eq(chain_id),
        ScanIndexForward=True
    )
    return response.get("Items", [])


def run_all_rules(events):
    """Execute all 6 anomaly detection rules against a chain."""
    anomalies = []

    # Per-event rules: check each event individually
    for event in events:
        result = check_missing_signature(event)
        if result:
            anomalies.append(result)

        result = check_invalid_signature(event)
        if result:
            anomalies.append(result)

    # Chain-level rules: check the full sequence
    anomalies.extend(check_ordering_violation(events))
    anomalies.extend(check_timestamp_regression(events))
    anomalies.extend(check_duplicate_event_id(events))
    anomalies.extend(check_missing_previous_event_id_link(events))

    return anomalies


def lambda_handler(event, context):
    """Anomaly detector entry point: validate integrity for a chain_id."""
    # Parse chain_id from the request
    body = json.loads(event.get("body", "{}")) if isinstance(event.get("body"), str) else event
    chain_id = body.get("chain_id") or event.get("pathParameters", {}).get("chain_id")

    if not chain_id:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "chain_id is required"})
        }

    # Fetch all events for this chain from DynamoDB
    events = fetch_chain_events(chain_id)

    if not events:
        return {
            "statusCode": 404,
            "body": json.dumps({"error": f"No events found for chain {chain_id}"})
        }

    # Run all anomaly detection rules
    anomalies = run_all_rules(events)

    # Build the integrity report
    report = {
        "chain_id": chain_id,
        "integrity_status": "PASS" if len(anomalies) == 0 else "FAIL",
        "anomalies": anomalies,
        "checked_at": datetime.now(timezone.utc).isoformat()
    }

    status_code = 200 if report["integrity_status"] == "PASS" else 422
    return {
        "statusCode": status_code,
        "body": json.dumps(report)
    }

Agent returned different HTTP status codes?

The status codes must be exactly: 400 for missing chain_id, 404 for an unknown chain, 200 for a clean chain, and 422 for a chain with anomalies. If Agent 4 used 500 or 409, re-prompt specifying these exact codes.

Help me fix Agent 4's HTTP status code logic in the anomaly detector.

Write anomaly test cases

Rules without tests are just opinions. Agent 4 now writes pytest test cases that prove each rule catches its target violation. The tests use the moto library to mock DynamoDB so you can run them locally without hitting AWS.

  • Install the test dependencies by running this command in your terminal:
pip install pytest "moto[dynamodb]"

What is moto?

The moto library creates fake AWS services in memory. Your tests can create tables, insert items, and query results without making real AWS API calls. The mock_aws decorator intercepts all boto3 calls and routes them to the in-memory mock.

  • In Agent 4's tab, provide this prompt to generate the test file:
Create tests/test_anomaly.py with pytest test cases for the anomaly rules.

Use the moto library (mock_aws decorator) to mock DynamoDB. Create a pytest fixture that sets up a mocked ChainEvents table with chain_id (HASH, S) and timestamp (RANGE, S) keys using PAY_PER_REQUEST billing.

Create a make_event helper that generates test chain events with: event_id (uuid4), chain_id, party, event_type, timestamp (base time + offset in minutes), payload (JSON string), previous_event_id, and signature fields.

Write exactly 5 test cases:
1. test_clean_chain_passes_all_rules - 3-event chain (producer/shipper/retailer) with correct ordering, increasing timestamps, valid signatures, and proper previous_event_id links. Assert all rules return None or empty lists.
2. test_missing_signature_detected - One event with empty signature string. Assert check_missing_signature returns anomaly with rule "missing_signature".
3. test_ordering_violation_detected - Chain where shipper comes before producer. Assert check_ordering_violation returns anomalies with rule "ordering_violation".
4. test_timestamp_regression_detected - Chain where the second event has an earlier timestamp than the first. Assert check_timestamp_regression catches it.
5. test_duplicate_event_id_detected - Two events forced to share the same event_id. Assert check_duplicate_event_id catches it.

Import rule functions inside each test (after moto context is active).
Set os.environ["CHAIN_EVENTS_TABLE"] = "ChainEvents" at module level.
Do NOT test check_invalid_signature (it requires a real KMS key).

Agent 4 creates tests/test_anomaly.py. Review the generated test file.

What do the 5 test cases cover?

  • test_clean_chain_passes_all_rules is the baseline. A valid 3-party chain with correct ordering, increasing timestamps, and proper linking produces zero anomalies across all rules.
  • The remaining 4 tests each inject one specific violation and assert that the corresponding rule catches it. This is the same pattern as the crisis scenarios you will run in the end-to-end demo.
  • The tests skip check_invalid_signature because it calls the real KMS API. That rule gets verified during the live demo in Step 9.

✔️ Awesome, I've got everything!

Click Apply to accept Agent 4's test file.

ⓧ I'd like to double check the full code

Compare Agent 4's output against this reference. Confirm the fixture creates the right table schema and each test asserts against the correct rule name.

import os
import json
import pytest
import boto3
from moto import mock_aws
from datetime import datetime, timezone, timedelta
from uuid import uuid4

# Set table name for test environment
os.environ["CHAIN_EVENTS_TABLE"] = "ChainEvents"


@pytest.fixture
def dynamodb_table():
    """Create a mocked DynamoDB table for testing."""
    with mock_aws():
        dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
        table = dynamodb.create_table(
            TableName="ChainEvents",
            KeySchema=[
                {"AttributeName": "chain_id", "KeyType": "HASH"},
                {"AttributeName": "timestamp", "KeyType": "RANGE"},
            ],
            AttributeDefinitions=[
                {"AttributeName": "chain_id", "AttributeType": "S"},
                {"AttributeName": "timestamp", "AttributeType": "S"},
            ],
            BillingMode="PAY_PER_REQUEST",
        )
        yield table


def make_event(chain_id, party, event_type, ts_offset_minutes=0, previous_event_id="", signature="valid-sig"):
    """Helper: create a test chain event."""
    base_time = datetime(2026, 1, 1, tzinfo=timezone.utc)
    return {
        "event_id": str(uuid4()),
        "chain_id": chain_id,
        "party": party,
        "event_type": event_type,
        "timestamp": (base_time + timedelta(minutes=ts_offset_minutes)).isoformat(),
        "payload": json.dumps({"product": "test-product", "quantity": 100}),
        "previous_event_id": previous_event_id,
        "signature": signature,
    }


def test_clean_chain_passes_all_rules(dynamodb_table):
    """A valid 3-party chain should produce zero anomalies."""
    from functions.anomaly.rules import (
        check_missing_signature,
        check_ordering_violation,
        check_timestamp_regression,
        check_duplicate_event_id,
        check_missing_previous_event_id_link,
    )

    chain_id = "clean-chain-001"
    evt1 = make_event(chain_id, "producer", "CREATED", ts_offset_minutes=0)
    evt2 = make_event(chain_id, "shipper", "SHIPPED", ts_offset_minutes=10, previous_event_id=evt1["event_id"])
    evt3 = make_event(chain_id, "retailer", "DELIVERED", ts_offset_minutes=20, previous_event_id=evt2["event_id"])
    events = [evt1, evt2, evt3]

    # Per-event rule: no missing signatures
    for event in events:
        assert check_missing_signature(event) is None

    # Chain-level rules: all pass
    assert check_ordering_violation(events) == []
    assert check_timestamp_regression(events) == []
    assert check_duplicate_event_id(events) == []
    assert check_missing_previous_event_id_link(events) == []


def test_missing_signature_detected(dynamodb_table):
    """An event with an empty signature should trigger Rule 1."""
    from functions.anomaly.rules import check_missing_signature

    evt = make_event("sig-test-001", "producer", "CREATED", signature="")
    result = check_missing_signature(evt)

    assert result is not None
    assert result["rule"] == "missing_signature"


def test_ordering_violation_detected(dynamodb_table):
    """Swapping producer and shipper should trigger Rule 3."""
    from functions.anomaly.rules import check_ordering_violation

    chain_id = "order-test-001"
    evt1 = make_event(chain_id, "shipper", "SHIPPED", ts_offset_minutes=0)
    evt2 = make_event(chain_id, "producer", "CREATED", ts_offset_minutes=10, previous_event_id=evt1["event_id"])

    anomalies = check_ordering_violation([evt1, evt2])
    assert len(anomalies) > 0
    assert anomalies[0]["rule"] == "ordering_violation"


def test_timestamp_regression_detected(dynamodb_table):
    """A later party with an earlier timestamp should trigger Rule 4."""
    from functions.anomaly.rules import check_timestamp_regression

    chain_id = "ts-test-001"
    evt1 = make_event(chain_id, "producer", "CREATED", ts_offset_minutes=10)
    evt2 = make_event(chain_id, "shipper", "SHIPPED", ts_offset_minutes=0, previous_event_id=evt1["event_id"])

    anomalies = check_timestamp_regression([evt1, evt2])
    assert len(anomalies) > 0
    assert anomalies[0]["rule"] == "timestamp_regression"


def test_duplicate_event_id_detected(dynamodb_table):
    """Two events with the same event_id should trigger Rule 5."""
    from functions.anomaly.rules import check_duplicate_event_id

    chain_id = "dup-test-001"
    evt1 = make_event(chain_id, "producer", "CREATED", ts_offset_minutes=0)
    evt2 = make_event(chain_id, "shipper", "SHIPPED", ts_offset_minutes=10, previous_event_id=evt1["event_id"])
    evt2["event_id"] = evt1["event_id"]  # Force duplicate ID

    anomalies = check_duplicate_event_id([evt1, evt2])
    assert len(anomalies) > 0
    assert anomalies[0]["rule"] == "duplicate_event_id"

Now run the tests to confirm every rule catches its target violation.

  • Run the test suite from the project root by running this command:
pytest tests/test_anomaly.py -v
  • You should see 5 tests pass with green PASSED markers next to each test name.

Tests failing or not found?

If you see ModuleNotFoundError: No module named 'functions', make sure you are running pytest from the project root directory, not from inside the tests/ folder.

If you see ModuleNotFoundError: No module named 'moto', confirm you installed the test dependencies with pip install pytest "moto[dynamodb]".

Help me debug my failing anomaly detection tests.

Agent 4 just gave your system the ability to detect 6 distinct types of supply chain integrity violations. Next up, Agent 5 builds the public audit API that exposes this detection to the outside world.

Agent 5 Builds the Audit API

Your anomaly detector can catch 6 types of chain breaks, from missing signatures to timestamp regressions. But those detection rules are locked inside a Lambda that only other Lambdas can call.

For a supply chain verification system to have real impact, external parties need to query it directly. Agent 5 (Platform Team) builds the public-facing audit API that exposes chain verification to anyone with an HTTP client.

In this step, get ready to:
  • Build the audit query Lambda with two API Gateway routes.
  • Add CORS headers, input validation, and proper HTTP status codes.
  • Create an OpenAPI specification documenting the API for external partners.
Build the audit query endpoint

The audit API is what makes this system publicly verifiable. It exposes two routes through API Gateway: one that returns the full chain with per-event verification, and one that runs the anomaly detector to check integrity.

  • Switch to the Agent 5 tab in Cursor's Agents Window.
  • Give Agent 5 (Platform Team) the following prompt by pasting it into the agent input:
You are Agent 5 (Platform Team). Read .cursorrules before writing any code.

Create functions/audit/app.py — the public audit API Lambda handler fronted by API Gateway.

This handler receives HTTP events from API Gateway (HTTP API) and routes based on the path:

Route 1: GET /audit/{chain_id}
- Fetch all events for the given chain_id from DynamoDB (table name from CHAIN_EVENTS_TABLE env var)
- For each event, call verify_signature from functions/signer/app.py to check its signature
- Return JSON: { chain_id, events: [{ event_id, party, event_type, timestamp, signature, signature_valid }], verified_at: <ISO 8601 timestamp>, kms_key_arn: <from KMS_KEY_ID env var> }
- If no events found for chain_id, return HTTP 404 with { error: "Chain not found", chain_id }

Route 2: GET /audit/{chain_id}/verify
- Call the anomaly detector logic (import from functions/anomaly/app.py or invoke the anomaly Lambda)
- Return the anomaly report: { chain_id, integrity_status, anomalies, checked_at }
- HTTP 200 if integrity_status is PASS
- HTTP 422 if integrity_status is FAIL (anomalies found)
- HTTP 404 if chain not found

Requirements:
- Extract chain_id from the API Gateway event pathParameters
- Validate chain_id: alphanumeric and hyphens only, max 64 characters. Return HTTP 400 for invalid input.
- Add CORS headers to ALL responses: Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET,OPTIONS, Access-Control-Allow-Headers: Content-Type
- Use structured JSON logging to CloudWatch
- Follow OWASP API security: never expose internal stack traces in error responses
- Use os.environ for CHAIN_EVENTS_TABLE and KMS_KEY_ID
- Add inline comments explaining each section

What does this prompt ask for?

Agent 5 creates a single Lambda handler that routes between two endpoints based on the URL path. The first endpoint (GET /audit/{chain_id}) fetches every event in a chain and verifies each signature individually.

The second endpoint (GET /audit/{chain_id}/verify) runs the full anomaly detector and returns a pass/fail integrity report. Together, these two routes let anyone verify what happened in a supply chain without needing direct access to DynamoDB or the signing key.

  • Wait for Agent 5 to finish generating the code.
  • Review the generated functions/audit/app.py in the diff view.
  • Confirm the handler imports verify_signature from the signer module and reads CHAIN_EVENTS_TABLE and KMS_KEY_ID from environment variables.

Why two separate routes?

The /audit/{chain_id} route gives a detailed view of each event and its individual signature status. The /audit/{chain_id}/verify route gives a quick pass/fail verdict using all 6 anomaly detection rules.

An external auditor might use the first route to inspect specific events, then the second to get the overall integrity assessment.

Agent 5 not generating the file?

Check that Agent 5 is running in its own worktree. If it tries to write into another agent's file path, the worktree isolation may block the write.

Also confirm the .cursorrules file is at the project root. Without it, the agent may use different naming conventions that conflict with your other agents' code.

Why isn't Agent 5 creating the audit Lambda correctly?

Add response formatting and error handling

A public API that exposes supply chain data needs to follow OWASP API security guidelines. This means proper HTTP status codes, input validation to prevent injection, and CORS headers for browser-based access.

  • Review the generated functions/audit/app.py and verify it includes the following patterns.

Check these four areas in the generated code:

  • HTTP status codes: 200 for a valid chain, 422 for a chain with anomalies, 404 for an unknown chain_id, and 400 for invalid input.
  • Input validation: chain_id must be alphanumeric plus hyphens only, with a max of 64 characters.
  • CORS headers: every response should include Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET,OPTIONS.
  • Error responses: error JSON should contain a generic message. No internal stack traces or Lambda ARNs in the response body.
  • If any of these four areas are missing, give Agent 5 a follow-up prompt requesting the specific fix by pasting it into the agent input:
Review functions/audit/app.py and ensure:
1. chain_id validation uses a regex pattern ^[a-zA-Z0-9\-]{1,64}$ and returns HTTP 400 with { error: "Invalid chain_id format" } for non-matching input
2. All responses include CORS headers: Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET,OPTIONS, Access-Control-Allow-Headers: Content-Type
3. The /verify route returns HTTP 422 (not 200) when anomalies are found
4. Error responses never include Python tracebacks or internal details

Fix any missing items. Keep all existing functionality intact.

Why HTTP 422 for anomalies?

HTTP 422 means "Unprocessable Entity." The request was valid and the chain exists, but the chain's data has integrity problems. This is more precise than 400 (bad request format) or 500 (server error).

  • Click Apply in Cursor's diff view to accept Agent 5's changes.
  • Confirm that functions/audit/app.py now appears in the Explorer sidebar under the functions/audit/ directory.

Agent producing 200 for everything?

A common Cursor agent pattern is to return 200 with error details in the JSON body. The audit API needs distinct status codes (200, 400, 404, 422) because external consumers parse the status code before reading the body.

Paste the follow-up prompt above to specifically request the correct status code mapping.

Help me fix Agent 5's audit API status codes.

Create the OpenAPI spec

The audit API is only useful if external parties know how to call it. An OpenAPI specification is the industry-standard way to document a REST API. It describes every endpoint, parameter, response schema, and error code in a machine-readable format.

  • Give Agent 5 this prompt to generate the OpenAPI spec by pasting it into the agent input:
Create docs/openapi.yaml — an OpenAPI 3.0 specification documenting the audit API.

Endpoints to document:

1. GET /audit/{chain_id}
   - Path parameter: chain_id (string, pattern: ^[a-zA-Z0-9\-]{1,64}$)
   - 200 response: JSON object with chain_id, events array (each with event_id, party, event_type, timestamp, signature, signature_valid boolean), verified_at timestamp, kms_key_arn string
   - 404 response: JSON object with error string and chain_id
   - 400 response: JSON object with error string for invalid chain_id format

2. GET /audit/{chain_id}/verify
   - Path parameter: chain_id (same validation as above)
   - 200 response: JSON object with chain_id, integrity_status "PASS", anomalies (empty array), checked_at timestamp
   - 422 response: JSON object with chain_id, integrity_status "FAIL", anomalies array (each with rule, event_id, description), checked_at timestamp
   - 404 response: same as above

Include:
- CORS headers in response definitions
- Example request and response bodies for each status code
- Description text explaining each endpoint's purpose
- Server URL placeholder for the API Gateway endpoint
- Info section with title "Supply Chain Audit API" and version "1.0.0"

What is an OpenAPI spec?

An OpenAPI spec (formerly Swagger) is a YAML or JSON file that describes your API's endpoints, parameters, and responses. Tools like Swagger UI can render it into interactive documentation automatically.

In a real supply chain system, this spec is what you would hand to a partner organization so their developers can integrate with your audit system.

  • Wait for Agent 5 to generate the file.
  • Review the generated docs/openapi.yaml in the diff view.

Verify the spec includes these key sections:

  • An info block with the title "Supply Chain Audit API" and version 1.0.0.
  • Both /audit/{chain_id} and /audit/{chain_id}/verify paths with GET operations defined.
  • Response schemas for all four status codes: 200, 400, 404, and 422.
  • Example request and response bodies under each response definition.
  • Click Apply to accept the OpenAPI spec.
  • Confirm that docs/openapi.yaml appears in the Explorer sidebar under the docs/ directory.

Validate the spec instantly

You can paste the contents of openapi.yaml into editor.swagger.io to see it rendered as interactive documentation. The editor also highlights any syntax errors in the spec.

OpenAPI spec has validation errors?

Common issues include missing schema definitions under response codes, or incorrect indentation in the YAML. If editor.swagger.io shows red errors, paste the error message back into Agent 5's input and ask it to fix the spec.

Help me fix the OpenAPI spec errors.

✔️ Awesome, I've got everything!

Double check you have saved both functions/audit/app.py and docs/openapi.yaml.

ⓧ I'd like to double check the full code

Your functions/audit/app.py should contain a Lambda handler that:

  • Validates chain_id input with a regex pattern (alphanumeric + hyphens, max 64 chars).
  • Routes GET /audit/{chain_id} to a function that fetches all chain events from DynamoDB and verifies each event's signature individually.
  • Routes GET /audit/{chain_id}/verify to a function that runs the anomaly detector and returns the integrity report.
  • Returns status codes: 200 (valid), 400 (bad input), 404 (not found), 422 (anomalies found).
  • Includes CORS headers on every response.
  • Includes verified_at timestamp and kms_key_arn in the chain detail response.

Your docs/openapi.yaml should be a valid OpenAPI 3.0 spec documenting both endpoints with request parameters, all four response schemas, CORS headers, and example request/response bodies.

All five agents have now delivered their domains. The infrastructure, chain emitter, signing harness, anomaly detector, and audit API are all built. Next up, you step back into your Engineering Manager role to review every agent's work, merge the worktrees, and deploy the full system to AWS.

Merge and Deploy

All 5 of your agent teams have delivered their components. The infrastructure template, chain emitter, signing harness, anomaly detector, and audit API each live in their own isolated Git worktree.

Isolated code does not catch contaminated shipments. This step brings everything together by merging all worktrees, deploying the full stack to AWS, and smoke testing every component.

In this step, get ready to:
  • Review and merge all 5 agent worktrees to the main branch.
  • Deploy the full stack to AWS using SAM and CloudFormation.
  • Smoke test every Lambda function and the API Gateway endpoint.
Review and merge all agent worktrees

As the engineering manager, your job before merging is to review each agent's diff. The critical check is naming consistency across all functions.

If one agent calls a field signature and another calls it sig, the system breaks at runtime with no obvious error message.

  • Open the Agents Window in Cursor by pressing Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) and typing Agents Window.
  • Click on the first agent tab (Agent 1, Infra Team).
  • Review the agent's changed files in the diff view.
  • Confirm all resource names in template.yaml match the names other agents reference (function names, table name, key alias).
  • Repeat for each remaining agent tab (Agent 2 through Agent 5), checking each diff for the same consistency.

What to check in each agent's diff

  • event_id, chain_id, and signature field names must be identical across models.py, emitter/app.py, signer/app.py, anomaly/app.py, and audit/app.py.
  • Environment variable names in the SAM template (KMS_KEY_ID, TABLE_NAME) must match what each function reads via os.environ.
  • The DynamoDB table name referenced in function code must match the logical resource name in template.yaml.
  • Import paths between functions (e.g. the emitter importing from shared.models) must resolve correctly.

Once you have reviewed all 5 agents, it is time to merge their work into your main checkout.

  • Select Agent 1 (Infra Team) in the Agents Window and use /apply-worktree to bring that agent's changes into your main checkout.
  • Apply Agent 2 (Chain Team) next.
  • Apply Agent 3 (Security Team).
  • Apply Agent 4 (Quality Team).
  • Apply Agent 5 (Platform Team).
  • If Cursor shows merge conflicts (most likely in functions/shared/models.py or template.yaml), resolve them by keeping the version that includes all fields from both agents.

Why merge in this order?

Agent 1's SAM template is the foundation that defines every resource name, environment variable, and IAM role. All other agents depend on these definitions. Merging infrastructure first ensures every subsequent merge resolves against the canonical resource names.

Use git diff as a backup

If /apply-worktree does not bring over all files, run git diff main inside the agent's worktree directory to see the full set of changes. You can then manually copy any missing files.

  • Confirm that all 13 expected files exist in your main checkout by running:
ls template.yaml functions/shared/models.py functions/emitter/app.py functions/signer/app.py functions/anomaly/app.py functions/anomaly/rules.py functions/audit/app.py scripts/simulate_chain.py tests/test_anomaly.py docs/openapi.yaml docs/architecture.md docs/key-rotation-runbook.md .cursorrules

You should see all 13 file paths echoed back with no errors.

Missing files after applying worktrees?

If any file is missing, navigate to that agent's worktree directory on disk and copy the file manually. You can find worktree paths under your project's .git/worktrees/ directory, or ask the agent where its worktree lives.

Help me find and copy missing files from my Cursor agent worktrees

Deploy to AWS

With all code merged to main, the codebase is complete. SAM will compile your Python functions and push the entire stack to AWS via CloudFormation.

  • Validate that your merged SAM template has no syntax errors by running:
sam validate

You should see a message confirming the template is valid.

  • Build all functions by running:
sam build

What does sam build do?

This command reads your template.yaml, installs each function's Python dependencies from its requirements.txt, and packages everything into the .aws-sam/build/ directory. All 4 Lambda functions get compiled in one pass.

  • Confirm the build succeeded. You should see Build Succeeded in the terminal output.

Build failed?

If you see import errors, check that functions/shared/models.py exists and that each function's requirements.txt lists all its dependencies.

If you see a template-not-found error, make sure you are running the command from your project root directory (the folder that contains template.yaml).

Help me debug my sam build failure

  • Deploy the stack to AWS by running:
sam deploy --guided

What happens during guided deployment?

The --guided flag launches an interactive flow that asks you to configure your deployment. SAM writes your choices to samconfig.toml so future deployments reuse the same settings automatically.

The guided flow will ask you a series of questions. Answer each one as follows:

  • For Stack Name, enter supply-chain-tracker.
  • For AWS Region, enter us-east-1.
  • For Confirm changes before deploy, press Enter to accept the default (Y).
  • For Allow SAM CLI IAM role creation, press Enter to accept the default (Y). This lets SAM create the IAM roles your Lambda functions need.
  • For Disable rollback, press Enter to accept the default (N). Keeping rollback enabled means a failed deploy reverts cleanly.
  • When prompted about functions that may not have authorization defined, enter y for each. This is expected for the audit API since it is a public endpoint.
  • For Save arguments to configuration file, press Enter to accept the default (Y).
  • For SAM configuration file, press Enter to accept the default.
  • For SAM configuration environment, press Enter to accept the default.

SAM will then show you a changeset preview listing every resource it will create. Review the list and enter y to confirm the deployment.

How long does deployment take?

The first deployment typically takes 3 to 5 minutes. CloudFormation creates resources in parallel where possible.

The KMS asymmetric signing key may take the longest since it involves generating the cryptographic key material.

  • When the deployment finishes, look for the Outputs section at the bottom of the terminal output.
  • Copy the API Gateway endpoint URL from the outputs and save it somewhere accessible. You will use it for the demo scenarios later.

Deployment failed with a rollback?

If CloudFormation rolls back, the terminal shows which resource failed. Common causes: your AWS CLI credentials lack permissions for KMS or IAM, or the region us-east-1 is not set correctly in your AWS CLI config.

Verify your identity by running aws sts get-caller-identity in your terminal. If this fails, reconfigure your AWS CLI credentials before retrying the deploy.

Help me debug my sam deploy failure for supply-chain-tracker

Smoke test the deployed stack

A successful deployment does not guarantee working code. The smoke test confirms every function initializes correctly and the API Gateway routes traffic to your Lambda functions.

  • Confirm the CloudFormation stack deployed successfully by running:
aws cloudformation describe-stacks --stack-name supply-chain-tracker --query "Stacks[0].StackStatus" --output text

You should see CREATE_COMPLETE in the output.

Seeing CREATE_FAILED or ROLLBACK_COMPLETE?

This means the deployment did not succeed. Check the CloudFormation events for the specific resource that failed by running aws cloudformation describe-stack-events --stack-name supply-chain-tracker and looking for events with CREATE_FAILED status.

Help me understand why my CloudFormation stack failed to create

  • List all the Lambda functions in your stack to get their physical names by running:
aws cloudformation describe-stack-resources --stack-name supply-chain-tracker --query "StackResources[?ResourceType=='AWS::Lambda::Function'].[LogicalResourceId,PhysicalResourceId]" --output table

Why do I need physical names?

CloudFormation appends a unique suffix to each Lambda function name. The logical name you defined in template.yaml (like EmitterFunction) becomes something like supply-chain-tracker-EmitterFunction-abc123. You need the physical names to invoke functions directly.

You should see 4 functions listed: EmitterFunction, SignerVerifierFunction, AnomalyDetectorFunction, and AuditApiFunction.

  • Test the API Gateway endpoint by sending a request for a nonexistent chain. Run this command, replacing the URL with your API endpoint:
curl -s [[API_ENDPOINT="your API Gateway endpoint URL from the deploy output"]]/audit/smoke-test

You should see a 404 response with a JSON body indicating the chain was not found. This is the correct behavior for a nonexistent chain ID.

Why does a 404 mean success?

This single request exercises the full request path: API Gateway receives the HTTP request, routes it to the AuditApiFunction Lambda, which queries DynamoDB for the chain, finds nothing, and returns a properly formatted 404 response. Every component in the chain fired correctly.

  • Open the AWS Console in your browser.
  • Navigate to CloudWatch.
  • Select Log groups in the left sidebar.
  • Type supply-chain-tracker in the search bar to filter for your functions.
  • Confirm you see log groups for all 4 Lambda functions.
  • Click into the AuditApiFunction log group.
  • Open the most recent log stream and confirm it shows a clean invocation with no error stack traces.

Curl returned a connection error or 500?

A connection error means the API Gateway URL is wrong. Double-check the URL from the sam deploy outputs and make sure you include https:// at the start.

A 500 error means the Lambda function crashed. Check the CloudWatch Logs for the AuditApiFunction log group to see the specific error. Common causes are a missing environment variable or an import that failed to resolve.

Help me debug my API Gateway 500 error for the supply chain audit endpoint

Your supply chain tracker is live on AWS. Every function is deployed, the API is accepting requests, and CloudWatch is capturing logs.

Next up, you will generate the effectiveness artifacts that turn this deployed system into a presentation-ready portfolio piece.

Generate Effectiveness Artifacts

Your supply chain tracker is deployed and every Lambda function has passed its smoke test. The system works. Now it's time to prove it.

Effective engineering leaders don't just build systems. They communicate their architecture, measure their impact, and document their processes for reproducibility. In this step, you'll use Cursor Agent mode to generate three presentation-ready artifacts that turn your deployed stack into a compelling portfolio piece.

In this step, get ready to:
  • Generate an interactive HTML workflow visualization with 4 topology views.
  • Generate a leadership KPI metrics dashboard with global crisis benchmarks.
  • Create 5 Cursor agent prompts with file ownership and acceptance criteria.
Generate the interactive HTML workflow visualization

The workflow visualization is a single-file HTML app that renders 4 interactive views of your entire system. It uses CDN-loaded React and Tailwind CSS so it works by opening the file directly in a browser with zero build steps.

  • In Cursor, open Agent mode by pressing Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) and selecting Agents Window.
  • Open a new agent tab and paste the following prompt:
Generate a self-contained single-file HTML app at docs/workflow-dashboard.html.

Use CDN-loaded React 18 (via unpkg.com) and Tailwind CSS (via CDN link). No build step, no npm, no local dependencies. The file must work by opening directly in a browser.

The app renders 4 clickable tab views:

TAB 1 - System Architecture:
Show all AWS services in the supply-chain-tracker stack and their connections:
- Lambda functions: EmitterFunction, SignerVerifierFunction, AnomalyDetectorFunction, AuditApiFunction
- DynamoDB: ChainEvents table (partition key: chain_id, sort key: timestamp)
- API Gateway: HTTP API with /audit/{chain_id} and /audit/{chain_id}/verify routes
- KMS: ECC_NIST_P256 asymmetric signing key (ECDSA_SHA_256)
- CloudTrail: audit logging for all KMS operations
- IAM: least-privilege roles per function
Draw directional arrows showing data flow between services.

TAB 2 - Agent Coordination:
Show 5 parallel Cursor agents working in isolated git worktrees:
- Agent 1 (Infra Team): template.yaml, IAM policies, CloudTrail config
- Agent 2 (Chain Team): functions/emitter/app.py, functions/shared/models.py, scripts/simulate_chain.py
- Agent 3 (Security Team): functions/signer/app.py, docs/key-rotation-runbook.md
- Agent 4 (Quality Team): functions/anomaly/app.py, functions/anomaly/rules.py, tests/test_anomaly.py
- Agent 5 (Platform Team): functions/audit/app.py, docs/openapi.yaml
Show merge flow from each agent worktree back to main branch with a review step.

TAB 3 - Data Flow:
Show a chain event's complete journey through the system:
1. Producer creates event with product details
2. EmitterFunction validates ordering and calls SignerVerifierFunction
3. SignerVerifierFunction computes SHA-256 digest, calls KMS Sign (ECDSA_SHA_256)
4. Signed event written to DynamoDB ChainEvents table
5. Steps repeat for shipper and retailer parties
6. Audit API receives GET /audit/{chain_id}/verify request via API Gateway
7. AuditApiFunction fetches chain from DynamoDB, calls AnomalyDetectorFunction
8. AnomalyDetectorFunction runs 6 rules, calls KMS Verify for each signature
9. API Gateway returns integrity report (HTTP 200 for pass, HTTP 422 for anomalies)

TAB 4 - Build Roadmap:
Show milestone-based progression (NOT time-based):
Scaffold -> Infrastructure Ready -> Functions Complete -> Integrated -> Deployed + Verified
Each milestone node shows what was accomplished at that stage.

Design requirements:
- Dark theme with professional styling
- All nodes interactive with hover tooltips showing descriptions
- Smooth tab transitions
- Responsive layout
- Color-coded nodes by service type or agent team
- Reference .cursorrules for naming conventions

What does this prompt do?

This prompt instructs Cursor to generate docs/workflow-dashboard.html as a fully self-contained app. Because it loads React and Tailwind from CDN links, there is nothing to install.

The 4 tab views cover everything a viewer needs to understand your system. System Architecture shows what you built. Agent Coordination shows how you built it. Data Flow shows how the system works. Build Roadmap shows the progression from zero to deployed.

  • Wait for Cursor to finish generating the file. This may take 30 to 60 seconds.
  • Click Apply to save Cursor's output to docs/workflow-dashboard.html.
  • Open docs/workflow-dashboard.html in your browser by double-clicking the file in your file explorer.
  • Click through all 4 tabs: System Architecture, Agent Coordination, Data Flow, and Build Roadmap. Each tab should render interactive nodes with hover details.

Tabs not rendering or page is blank?

  • Open your browser's developer console (F12) and check for JavaScript errors. A failed CDN load for React or Tailwind is the most common cause.
  • If Cursor generated incomplete HTML (missing closing tags or truncated JavaScript), re-run the agent prompt. Cursor sometimes truncates long outputs on the first attempt.
  • If specific tabs are missing, tell the agent: "The Data Flow tab is missing. Add it following the original prompt requirements."

Why isn't my workflow-dashboard.html rendering in the browser?

Generate the leadership KPI metrics dashboard

A deployed system is only half the story. The KPI metrics dashboard translates your technical work into leadership-ready numbers. It benchmarks your system against the global crisis data and enterprise competitors.

  • In Cursor's Agents Window, open a new agent tab and paste the following prompt:
Generate a leadership-ready metrics dashboard at docs/kpi-metrics.html.

Self-contained single HTML file using CDN-loaded Tailwind CSS. No build step.

Display 8 KPI metric cards in a responsive grid layout:

1. Build Velocity
   - Current: 5 parallel agents, concurrent worktree execution
   - Target: All 5 agents complete within single session
   - Benchmark: Traditional sequential development takes 3-5x longer

2. Security Posture Score
   - Current: KMS ECDSA signing on every event, 6 anomaly detection rules, least-privilege IAM per function
   - Target: 100% of chain events cryptographically signed
   - Benchmark: Most supply chains have zero cryptographic verification between handoff parties

3. Cost Efficiency
   - Current: $1.01/month (1 KMS key + negligible request costs)
   - Target: Under $5/month at 10x scale
   - Benchmark: IBM Food Trust ~$10,000+/month, VeChain ~$5,000+/month (enterprise tiers)

4. Compliance Coverage
   - Current: SLSA Build L1, NIST SSDF (PS.1.1, PS.3.2, PW.4.1, RV.1.1), OWASP API Security
   - Target: Full SLSA L1 with provenance attestation
   - Benchmark: Most startups have zero formal compliance mapping

5. Global Impact Context
   - 334 children dead from contaminated medicines since 2022 (WHO/UNODC 2025)
   - 99 countries received contaminated infant formula (WHO Dec 2025 - Feb 2026)
   - $75 billion annual cost of foodborne illness in the US (GAO 2025)
   - 1 in 10 medicines in low/middle-income countries are falsified (WHO)
   - 93% increase in FDA food recalls Jan-Apr 2025 vs 2024
   Display these as a highlighted statistics panel with source citations.

6. Anomaly Detection Rate
   - Current: 6 rules with 100% catch rate on injected chain breaks
   - Target: Millisecond detection time vs FDA's 3-to-7-day recall detection
   - Benchmark: Manual audit processes take days to weeks

7. Audit Trail Completeness
   - Current: CloudTrail captures all KMS sign/verify operations, CloudWatch structured JSON logs
   - Target: Every cryptographic operation traceable to caller identity and timestamp
   - Benchmark: NIST SSDF RV.1.1 requires anomaly detection and logging

8. Industry Comparison
   Show a comparison table with columns: Feature, This Project, IBM Food Trust, VeChain, TradeLens
   Rows: Monthly Cost, Signing Method, Open Source, Compliance Framework, Status
   Note that TradeLens was discontinued in 2022.
   Include source citations for each benchmark.

Design requirements:
- Dark theme matching the workflow dashboard
- Each card shows: current value (large), target value, industry benchmark with source
- Responsive grid (2 columns on desktop, 1 on mobile)
- Functional PDF export button using window.print() with @media print CSS that hides the button and optimizes layout
- Global Impact Context section should be visually prominent with colored stat cards

What does this dashboard measure?

The 8 KPI cards map directly to the project's core value proposition. Build Velocity and Cost Efficiency show your engineering execution. Security Posture and Compliance Coverage show your governance maturity.

The Global Impact Context section is the most important card. It grounds the entire project in the real-world crisis data. When a reviewer sees "$1.01/month" next to "$75 billion annual problem," the value is immediately clear.

  • Wait for Cursor to finish generating the file.
  • Click Apply to save Cursor's output to docs/kpi-metrics.html.
  • Open docs/kpi-metrics.html in your browser.
  • Verify that all 8 KPI cards render with current values, targets, and benchmarks.
  • Scroll down to the Industry Comparison table. You should see columns for your project, IBM Food Trust, VeChain, and TradeLens.
  • Click the Export PDF button. Your browser's print dialog should open with a clean, print-optimized layout.

PDF export button missing or layout broken?

  • If the export button does not appear, check that Cursor included the window.print() JavaScript call and a corresponding button element in the generated HTML.
  • If the print layout looks wrong, ask the agent to add @media print CSS rules that hide the export button and set a white background.
  • If KPI cards are missing data, tell the agent which specific card is incomplete and provide the values from the prompt.

My KPI dashboard is not rendering correctly or is missing sections.

Create the Cursor agent prompts file

The final artifact is a reference document containing the exact prompts you would paste into each of the 5 Cursor agent tabs for parallel execution. This makes your entire multi-agent workflow reproducible.

  • In Cursor's Agents Window, open a new agent tab and paste the following prompt:
Generate docs/agent-prompts.md containing 5 copy-paste-ready Cursor agent prompts.

Each prompt section must include:
- Agent role name as a level 2 heading
- File ownership boundaries (which files this agent creates and owns)
- A note to reference .cursorrules for naming conventions and security rules
- Specific deliverables list (what the agent must produce)
- Acceptance criteria (how to verify the agent's work is correct)

The 5 agents are:

## Agent 1 - Infra Team
- Owns: template.yaml, IAM policies, CloudTrail configuration
- Deliverables: Complete SAM template with all resources (3 Lambda functions, 1 audit API Lambda, DynamoDB table, API Gateway HTTP API, KMS ECC_NIST_P256 key, IAM roles with least-privilege, CloudTrail logging)
- Acceptance: sam validate passes, sam build succeeds, all resources tagged project:supply-chain-tracker and compliance:slsa-l1

## Agent 2 - Chain Team
- Owns: functions/emitter/app.py, functions/shared/models.py, scripts/simulate_chain.py
- Deliverables: ChainEvent data model with all fields (event_id, chain_id, party, event_type, timestamp, payload, previous_event_id, signature), emitter Lambda that enforces party ordering (producer -> shipper -> retailer), simulation script with clean and injected-break modes
- Acceptance: Emitter rejects out-of-order events with 400 error, simulation script creates a complete 3-event chain

## Agent 3 - Security Team
- Owns: functions/signer/app.py, docs/key-rotation-runbook.md
- Deliverables: Sign function (canonical JSON -> SHA-256 digest -> KMS ECDSA_SHA_256 sign), verify function (recompute digest -> KMS verify -> boolean), retry logic with exponential backoff for KMS throttling, structured JSON CloudWatch logging, key rotation runbook
- Acceptance: Sign returns base64 signature and key ARN, verify correctly validates good signatures and rejects tampered ones

## Agent 4 - Quality Team
- Owns: functions/anomaly/app.py, functions/anomaly/rules.py, tests/test_anomaly.py
- Deliverables: 6 anomaly detection rules (missing signature, invalid signature, ordering violation, timestamp regression, duplicate event_id, missing previous_event_id link), anomaly detector Lambda returning integrity report, pytest test cases for each rule using moto mocks
- Acceptance: All pytest tests pass, detector catches all injected anomalies in simulation

## Agent 5 - Platform Team
- Owns: functions/audit/app.py, docs/openapi.yaml
- Deliverables: Audit API Lambda with GET /audit/{chain_id} and GET /audit/{chain_id}/verify routes, HTTP status codes (200 pass, 422 anomalies, 404 unknown), CORS headers, input validation (alphanumeric + hyphens, max 64 chars), OpenAPI 3.0 specification with example requests/responses
- Acceptance: API returns correct status codes for clean chain, broken chain, and unknown chain_id

Format each prompt so it can be directly copied and pasted into a Cursor Agent tab.
Include a note at the top of the file explaining that all 5 agents run in parallel via /worktree isolation.

Why document the agent prompts?

This file is your engineering playbook. Anyone who wants to reproduce your multi-agent build can open 5 Cursor agent tabs, paste these prompts, and get the same parallel execution workflow you used.

The file ownership boundaries are the key detail. They prevent two agents from editing the same file, which eliminates merge conflicts during parallel execution.

  • Wait for Cursor to finish generating the file.
  • Click Apply to save the output to docs/agent-prompts.md.
  • Open docs/agent-prompts.md and confirm it contains all 5 agent sections.
  • Check that each agent section includes the role name, file ownership list, deliverables, and acceptance criteria.

Agent prompts file missing sections?

  • If Cursor only generated 3 or 4 agents, tell it which specific agents are missing (e.g., "Add the Quality Team and Platform Team agent prompts").
  • If the file is missing acceptance criteria, ask the agent to "Add acceptance criteria to each agent section that describe how to verify the agent's output is correct."

Cursor didn't generate all 5 agent prompts correctly. Help me fix it.

Working with AI-generated artifacts

Cursor Agent may produce slightly different output each time you run a prompt. The important thing is that each artifact meets its acceptance criteria: the workflow dashboard has 4 working tabs, the KPI dashboard has 8 cards with a PDF export, and the agent prompts file has all 5 agents with file ownership.

If any artifact is close but not quite right, iterate. Tell the agent what to fix in a follow-up message. This iterative refinement is exactly how you would work with AI tools on a real engineering team.

You now have three presentation-ready artifacts sitting alongside your deployed system. Next up, you'll run 4 crisis-inspired scenarios through the entire stack and verify every component end-to-end.

Run the End-to-End Systems Demo

Your effectiveness artifacts are built and verified. The interactive visualization, KPI dashboard, and agent prompts all sit in your docs/ folder, ready for review.

Now you will run 4 crisis-inspired scenarios through the entire deployed system. Each scenario exercises a different failure mode drawn from real supply chain disasters where cryptographic verification was missing.

In this step, get ready to:
  • Run 4 crisis-inspired demo scenarios through the full deployed system.
  • Verify the compliance audit trail in CloudTrail and CloudWatch.
  • Complete the 14-item artifact verification checklist.
Run the 4 crisis-inspired demo scenarios

Each scenario maps to a real supply chain failure. Scenario 1 models the "how it should work" baseline. Scenarios 2 and 3 simulate the exact breakdowns that led to contaminated medicines and ingredient substitutions in real incidents.

Scenario 4 proves the public audit API responds correctly to every category of request. This is the demo you would walk a hiring committee or partner organization through.

  • Open Cursor Agent mode (Ctrl+Shift+P (macOS: Cmd+Shift+P), then search for Agent).
  • Paste the following prompt to generate your demo script:
Create scripts/run_demo.py that runs 4 crisis-inspired end-to-end demo scenarios
against our deployed supply-chain-tracker stack.

Use boto3 to invoke Lambda functions and query DynamoDB.
Read the API Gateway endpoint URL from the SAM stack outputs
(stack name: supply-chain-tracker, region: us-east-1).

## Scenario 1: Infant Formula Safe Path
- Simulate a clean 3-party chain: producer (Belgium manufacturer) -> shipper
  (certified cold-chain) -> retailer (Singapore).
- Invoke the EmitterFunction 3 times in order (producer, shipper, retailer)
  with a chain_id like "demo-infant-formula-safe".
- After all 3 events, invoke the AnomalyDetectorFunction for the chain.
- Assert integrity_status is PASS and print all 3 signed events.

## Scenario 2: Pharmaceutical Supply Chain Break
- Simulate a chain where the shipper step is skipped entirely.
- Invoke the EmitterFunction for producer, then immediately for retailer
  (skipping shipper).
- The emitter should reject the out-of-order retailer event with HTTP 400.
- Print the error response showing ordering enforcement.

## Scenario 3: Contaminated Ingredient Substitution
- Create a normal 3-party chain (chain_id like "demo-contamination-inject").
- After the chain completes, inject a second producer event mid-chain with a
  different payload (simulating ingredient substitution).
- Run the AnomalyDetectorFunction on this chain.
- Assert integrity_status is FAIL with ordering/duplicate violation details.

## Scenario 4: Audit API Public Verification
- Use the requests library (or urllib) to call the live API Gateway endpoint.
- GET /audit/{chain_id} for Scenario 1 chain -> expect HTTP 200,
  integrity_status PASS.
- GET /audit/{chain_id}/verify for Scenario 3 chain -> expect HTTP 422
  with anomaly details.
- GET /audit/nonexistent-chain-id-12345 -> expect HTTP 404.
- Print each response status code and body.

## Output
- Print a clear header for each scenario.
- Print PASS/FAIL for each scenario assertion.
- Print a final summary showing all 4 scenario results.

Refer to .cursorrules for naming and style conventions.
Refer to functions/shared/models.py for the ChainEvent schema.
Refer to template.yaml for the exact Lambda function logical IDs and
DynamoDB table name.

What does this prompt do?

This prompt instructs Cursor to generate a Python script that exercises your entire deployed system. Each scenario maps to a real-world crisis.

  • Scenario 1 proves the happy path works end-to-end with full cryptographic signing.
  • Scenario 2 tests the ordering enforcement that was missing in the contaminated cough syrup incidents.
  • Scenario 3 tests anomaly detection against ingredient substitution attacks.
  • Scenario 4 proves the public audit API Gateway endpoint returns the correct HTTP status codes for every request type.
  • Wait for Cursor to generate scripts/run_demo.py.
  • Review the generated script in the diff view. Confirm it references your deployed Lambda function names from template.yaml.
  • Click Accept to save the file.

The demo script needs to call the live API endpoint for Scenario 4. If Cursor did not automatically read the API Gateway URL from the stack outputs, you may need to pass it as an argument or set it as an environment variable.

  • Run the full demo by executing this command:
python scripts/run_demo.py
  • Watch the output as each scenario runs.

You should see 4 scenario headers printed in sequence. Scenario 1 prints 3 signed events and a PASS integrity result. Scenario 2 prints an HTTP 400 error showing the ordering enforcement caught the missing shipper. Scenario 3 prints a FAIL integrity result with anomaly details. Scenario 4 prints HTTP 200, 422, and 404 responses from the live API.

Seeing a Lambda invocation error?

  • Confirm your AWS CLI session is still active by running aws sts get-caller-identity. If it fails, re-authenticate.
  • Verify the stack is still deployed by running aws cloudformation describe-stacks --stack-name supply-chain-tracker. The status should be CREATE_COMPLETE or UPDATE_COMPLETE.
  • Check that the Lambda function names in the script match the logical IDs in your template.yaml. Cursor may have guessed different names.

Still stuck? Ask for help.

Now verify the audit API manually with curl to see the raw HTTP responses. This is exactly how an external partner would query your system.

  • Verify the clean chain returns HTTP 200 by running this command:
curl -s -w "\nHTTP Status: %{http_code}\n" [[API_GATEWAY_URL="your API Gateway endpoint URL from Step 7"]]/audit/demo-infant-formula-safe
  • You should see a JSON response with integrity_status: PASS and HTTP Status 200.
  • Verify the tampered chain returns HTTP 422 by running this command:
curl -s -w "\nHTTP Status: %{http_code}\n" [[API_GATEWAY_URL="your API Gateway endpoint URL from Step 7"]]/audit/demo-contamination-inject/verify
  • You should see a JSON response with integrity_status: FAIL, a list of anomalies, and HTTP Status 422.
  • Verify that a nonexistent chain returns HTTP 404 by running this command:
curl -s -w "\nHTTP Status: %{http_code}\n" [[API_GATEWAY_URL="your API Gateway endpoint URL from Step 7"]]/audit/nonexistent-chain-id-12345
  • You should see an empty or error response with HTTP Status 404.

Why these 3 status codes matter

HTTP 200 confirms a verified chain. HTTP 422 signals that the chain exists but has integrity violations. HTTP 404 means no chain was found for that ID.

Together, these 3 responses cover every state an external auditor would encounter. This follows OWASP API security guidelines for consistent, meaningful error responses.

Getting a connection refused or 403?

  • Double check that you pasted the full API Gateway URL including the https:// prefix.
  • Confirm the URL does not have a trailing slash before the /audit path.
  • If you see a 403, your API Gateway may have authorization enabled. Check the template.yaml for any authorizer configuration on the HTTP API.

Need help debugging the API response?

Verify the compliance audit trail

Running the demo generated dozens of KMS sign and verify operations across all 4 scenarios. Every one of those cryptographic operations was recorded by CloudTrail automatically.

This audit trail is the compliance evidence that NIST SSDF practice RV.1.1 requires. You will now confirm it captured everything.

  • Open the AWS Console in your browser.
  • Type CloudTrail in the search bar at the top and select CloudTrail.
  • Click Event history in the left sidebar.
  • In the Lookup attributes dropdown, select Event source.
  • Enter kms.amazonaws.com in the search field and press Enter.

You should see a list of Sign and Verify events with recent timestamps. Each event shows the caller identity (your Lambda function ARN) and the KMS key ARN.

What am I looking for in CloudTrail?

Click on any Sign event to expand its details. You should see three key fields.

  • eventTime shows the exact timestamp of the cryptographic operation.
  • userIdentity shows the Lambda function ARN that called KMS. This proves which function performed the signing.
  • resources shows the KMS key ARN. This proves which key was used.

This three-way link (who signed, when, with which key) is the cryptographic audit trail. It is non-repudiable evidence that the operation happened.

Now check CloudWatch Logs to confirm the signer function logged structured entries.

  • Type CloudWatch in the AWS Console search bar and select CloudWatch.
  • Click Log groups under Logs in the left sidebar.
  • Find the log group for your Signer/Verifier function. It will start with /aws/lambda/ followed by the function name from your stack.
  • Click into the most recent log stream.

You should see structured JSON log entries for each sign and verify operation. Each entry includes the chain ID, event ID, operation type, and timestamp.

Not seeing any KMS events in CloudTrail?

  • CloudTrail events can take up to 15 minutes to appear. If your demo just finished, wait a few minutes and refresh.
  • Confirm you are in the us-east-1 region in the AWS Console. The region selector is in the top right corner of the console.
  • Make sure you filtered by Event source (not Event name) and entered kms.amazonaws.com exactly.

Help me find my CloudTrail KMS events.

Run the artifact verification checklist

This project promised 14 artifacts. Every one of them must exist and work. This checklist is your final proof that the system is complete.

  • template.yaml deploys without errors (confirmed in Step 7).
  • All 4 Lambda functions invoke successfully (confirmed by demo scenarios).
  • DynamoDB ChainEvents table contains signed events from all demo scenarios.
  • API Gateway returns correct responses for all 3 HTTP status codes: 200, 422, 404 (confirmed in Scenario 4).
  • KMS signing key exists and is enabled.
  • CloudTrail audit trail captures all KMS operations (confirmed in Substep 9.2).
  • docs/workflow-dashboard.html opens in a browser and all 4 tabs render correctly.
  • docs/kpi-metrics.html opens in a browser, all KPI cards show data, the industry comparison table renders, and the PDF export button works.
  • docs/openapi.yaml is valid (paste into editor.swagger.io or run a linter).
  • docs/agent-prompts.md contains all 5 agent prompts with file ownership and acceptance criteria.
  • docs/architecture.md contains the Mermaid diagram.
  • docs/key-rotation-runbook.md documents the rotation procedure.
  • .cursorrules file exists with DevSecOps conventions.
  • tests/test_anomaly.py runs with pytest and all tests pass.

Several items on the checklist you already confirmed in previous steps. Focus now on the ones that need verification.

  • Verify the KMS key is enabled by running this command:
aws kms list-keys --region us-east-1
  • You should see at least one key listed. Find the key ID that matches the one in your template.yaml outputs or your Lambda environment variables.
  • Verify the key details by running this command (replace the key ID):
aws kms describe-key --key-id [[KMS_KEY_ID="your KMS key ID from the list-keys output"]] --region us-east-1
  • Confirm the output shows KeyState: Enabled, KeyUsage: SIGN_VERIFY, and KeySpec: ECC_NIST_P256.

What do those key properties mean?

  • ECC_NIST_P256 is an ECDSA elliptic curve key, NIST FIPS 186-4 compliant. This is the same standard used by TLS certificates protecting web traffic.
  • SIGN_VERIFY means the key can only sign data and verify signatures. It cannot encrypt or decrypt. This follows the principle of least privilege.

Now verify the DynamoDB table contains events from your demo.

  • Query the ChainEvents table for your Scenario 1 chain by running this command:
aws dynamodb query \
  --table-name ChainEvents \
  --key-condition-expression "chain_id = :cid" \
  --expression-attribute-values '{":cid": {"S": "demo-infant-formula-safe"}}' \
  --region us-east-1
  • You should see 3 items returned (producer, shipper, retailer). Each item includes a signature field with a base64-encoded value. This proves every handoff was cryptographically signed.

DynamoDB query returning empty results?

  • The chain_id in your query must exactly match what your run_demo.py script used. Open the script and check the chain IDs in the Scenario 1 section.
  • Confirm the table name matches your SAM template. Run aws dynamodb list-tables --region us-east-1 to see your actual table name.

Help me query my DynamoDB table.

Finally, run the test suite to confirm all anomaly detection rules pass.

  • Run the anomaly tests by executing this command:
pytest tests/test_anomaly.py -v
  • You should see all 5 tests passing with green check marks. Each test covers a different anomaly rule: clean chain, missing signature, swapped ordering, tampered timestamp, and duplicate event ID.

Seeing test failures or import errors?

  • If you see ModuleNotFoundError: No module named 'moto', install the test dependencies by running pip install moto[dynamodb] pytest.
  • If tests fail with assertion errors, check that the anomaly rules in functions/anomaly/rules.py match the test expectations in tests/test_anomaly.py.

Help me fix my failing tests.

What does this checklist prove?

Every item on this checklist maps to a real deliverable. The deployed infrastructure proves your IaC works. The demo scenarios prove your chain logic catches real attack patterns. The audit trail proves compliance. The artifacts prove you can communicate what you built.

Save this checklist as your demo record. This is presentation-ready evidence for a VP, a hiring committee, or a partner organization evaluating supply chain integrity solutions.

  • Open docs/workflow-dashboard.html in your browser and click through all 4 tabs (System Architecture, Agent Coordination, Data Flow, Build Roadmap) to confirm each renders.
  • Open docs/kpi-metrics.html in your browser. Confirm all KPI cards display data and the PDF export button works.
  • Validate docs/openapi.yaml by pasting its contents into editor.swagger.io. Confirm it renders without errors.
  • Open docs/agent-prompts.md and confirm all 5 agent prompts are present with file ownership and acceptance criteria.
  • Open docs/architecture.md and confirm the Mermaid diagram is present.
  • Open docs/key-rotation-runbook.md and confirm it documents the key rotation procedure.
  • Confirm .cursorrules exists in the project root with your DevSecOps conventions.

All 14 artifacts are verified. Every component has been exercised. Every claim this system makes has been proven by running real scenario data through it.

Secret mission

Add a SLSA L1 Compliance Dashboard

Your supply chain tracker can sign events and detect anomalies. But can it prove compliance to an auditor? Build a 6th Lambda function that generates SLSA L1 provenance reports in the in-toto attestation format, complete with integrity scoring and NIST SSDF mapping. This is the artifact a hiring manager at a supply chain company would want to see.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses one AWS KMS asymmetric key that costs $1/month (prorated hourly). All other services (Lambda, DynamoDB, API Gateway, S3) fall within the AWS always-free tier.

Cost warning

Your KMS asymmetric signing key costs $1/month, prorated hourly. Asymmetric Sign/Verify requests are excluded from the KMS free tier and cost $0.15 per 10,000 requests.

If you are not planning to demo the project again soon, choose Pause or Delete below to stop the recurring charge.

Resources you used:

  • 1 KMS asymmetric signing key (ECC_NIST_P256, $1/month).
  • 4 Lambda functions (Emitter, Signer, Anomaly Detector, Audit API).
  • 1 DynamoDB table (ChainEvents).
  • 1 API Gateway (HTTP API) with 2 public endpoints.
  • 1 S3 deployment bucket (created by SAM).
  • 1 CloudFormation stack (supply-chain-tracker).
  • IAM roles with least-privilege policies.
  • CloudTrail logging configuration for KMS operations.
  • Local project files (code, docs, tests, .cursorrules).

Keep everything running

No action needed. Choose this if you are still actively building or plan to demo the project soon.

  • Your total ongoing cost is approximately $1.01/month for the KMS key and negligible signing requests.
  • All Lambda, DynamoDB, API Gateway, and S3 resources remain within the always-free tier.
  • Your API Gateway endpoint stays live for public demos.

Pause - I'll come back to this later

Delete the KMS key to stop the $1/month charge. Keep everything else running for free. You can re-create the key later if you want to demo again.

  • Find your KMS key ID by running this command:
aws kms list-keys --query "Keys[].KeyId" --output table
  • Schedule the key for deletion by running this command:
aws kms schedule-key-deletion --key-id [[KMS_KEY_ID="your KMS key ID"]] --pending-window-in-days 7

Why a 7-day waiting period?

AWS KMS requires a minimum 7-day waiting period before deleting a key. This protects against accidental deletion. During this window, the key is disabled (no charges) but can still be cancelled if you change your mind.

  • Your Lambda functions, DynamoDB table, API Gateway, and S3 bucket all stay within the always-free tier. No further action needed for those.
  • To resume later, create a new KMS key and update the environment variables in your SAM template with the new key ID.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes the entire CloudFormation stack and all associated AWS resources.

  • Delete the CloudFormation stack and all its resources by running this command:
sam delete --stack-name supply-chain-tracker
  • When prompted to confirm, enter y for each question.

This removes your Lambda functions, DynamoDB table, API Gateway, IAM roles, and S3 deployment bucket. However, KMS keys require separate deletion.

  • Schedule the KMS key for deletion by running this command:
aws kms schedule-key-deletion --key-id [[KMS_KEY_ID="your KMS key ID"]] --pending-window-in-days 7
  • The key enters a 7-day waiting period, then deletes automatically. No charges apply during this period.
  • To delete local project files, remove the supply-chain-tracker folder from your Desktop.

Mission accomplished

That's a Wrap!

You just built, deployed, and verified a cryptographically signed supply chain tracking system on AWS serverless. You coordinated 5 parallel Cursor agents like a real engineering manager, ran 4 crisis-inspired scenarios through the full system, and proved every component works end-to-end.

You've learned how to:

  • Deploy a serverless supply chain tracker with ECDSA cryptographic signing via AWS KMS, 6 anomaly detection rules, and a public audit API verified across 3 HTTP status codes.
  • Coordinate 5 parallel Cursor agents in isolated Git worktrees, following DevSecOps conventions encoded in a .cursorrules file and aligned to SLSA L1 provenance principles and NIST SSDF practices.
  • Run 4 real-world crisis scenarios end-to-end, verify the compliance audit trail in CloudTrail, and produce leadership-ready effectiveness artifacts including an interactive workflow visualization and a KPI metrics dashboard benchmarked against global crisis data.
  • Secret Mission: Built a SLSA L1 compliance dashboard with in-toto attestation format and chain integrity scoring.

Ready to quiz yourself?