Secrets Manager

Every credential the platform uses comes from AWS Secrets Manager at runtime. Nothing is baked into an environment variable, a container image, or a template.

There are two secrets, and the two halves of the platform read different ones.

Secret Created by Read by Referenced as

agentcore-configs

SAM, as AgentcoreSecret

All five Lambda functions

AGENTCORE_SECRET_ARN environment variable

agentcore-secrets-BHnvz6

Nothing — pre-existing

The AgentCore runtime

SECRET_ARN, from the secretArn CDK prop

This split is easy to miss. Adding a key to agentcore-configs does nothing for the AgentCore runtime, and adding it to the CDK secret does nothing for the Lambdas. A credential needed on both sides has to be written to both.

The CDK secret’s ARN is a hardcoded literal in cdk/bin/cdk.ts, including its random suffix. Recreating that secret changes the suffix and breaks the deploy until the literal is updated.

The exact key list for agentcore-configs is in Configuration and secrets.

The SAM resource

AgentcoreSecret:
  Type: AWS::SecretsManager::Secret
  Properties:
    Name: !Sub "agentcore-configs"
    Description: Shared API credentials for the agentcore pipeline

No GenerateSecretString, no SecretString. SAM creates the secret empty.

Deploying gives you a secret with no value in it. Every function will then fail at cold start when it tries to parse the payload. Populating it is a manual post-deploy step, and it is the single most common reason a fresh environment does not work:

aws secretsmanager put-secret-value \
  --secret-id agentcore-configs \
  --secret-string file://secrets.json \
  --region us-east-1

Delete the local file afterwards. Never commit it — .env files and credential JSON are covered by the hard rules in AGENTS.md.

Keeping the value out of the template is what allows the template to be public and reviewable. The consequence is that the secret’s contents are not versioned anywhere, so a key added by one person is invisible to everyone else until something breaks. When you add one, document it in Configuration and secrets in the same pull request.

Loading in a Lambda

from functools import lru_cache

@lru_cache(maxsize=1)
def load_secrets() -> Secrets:
    import boto3
    client = boto3.client("secretsmanager")
    raw = client.get_secret_value(SecretId=os.environ["AGENTCORE_SECRET_ARN"])
    return Secrets.model_validate_json(raw["SecretString"])

Three things are load-bearing here:

@lru_cache(maxsize=1)

One Secrets Manager API call per container lifetime, not per invocation. A warm container that handles a thousand messages makes one call. Secrets Manager charges per API call and throttles, so an uncached fetch on a queue consumer at concurrency 10 is both a cost and a rate-limit problem.

The deferred import boto3

Nothing is imported at module load, so a test can import the module and patch load_secrets() without AWS or the layer being present. See Testing Lambdas.

Pydantic validation

The payload is parsed into a typed model, so a missing key fails at cold start with a clear validation error rather than as a KeyError deep inside a handler an hour later.

The cache is per container, so rotating a secret does not take effect until every warm container is recycled. There is no cache invalidation and no rotation configured. To force a pickup immediately, publish a trivial function configuration change — updating an environment variable replaces all containers.

The call order at cold start is fixed: configure_sentry() first, then load_secrets(). If loading the secret throws, Sentry is already initialised to report it.

Loading in the AgentCore runtime

agentcore/config/loader.py exposes load_config(), which fetches SECRET_ARN, validates it with Pydantic, and injects the values into the process environment before the workflow registry is built. It runs once, from main.py, before app.run().

The injection step exists because third-party SDKs read their own environment variables — an Anthropic client wants ANTHROPIC_API_KEY in os.environ, not passed as an argument. See Configuration.

Permissions

Principal Grant

enrichment-scraper

secretsmanager:GetSecretValue and PutSecretValue on AgentcoreSecret

The other four functions

secretsmanager:GetSecretValue on AgentcoreSecret

AgentCore runtime role

GetSecretValue and DescribeSecret, scoped to props.secretArn when set, otherwise to arn:aws:secretsmanager:us-east-1:625663093496:secret:*

enrichment-scraper is the only writer. It logs into SmartScout, receives a session token, and writes it back so the next invocation reuses it instead of re-authenticating. That is a genuine requirement, but it means one function can overwrite the entire shared secret — the widest single permission in the platform.

The runtime role’s fallback to a wildcard resource only applies when secretArn is unset. It is set in cdk/bin/cdk.ts, so the deployed policy is scoped to one ARN. Removing that prop would silently widen the grant to every secret in the account. More in IAM.

Never in a secret

Value Where it belongs

Model ids, prompts, provider choice

Runtime code. Never in CDK, SAM, or a secret.

SUBSCRIPTION_ID

A plain environment variable — it is an identifier, not a credential.

Queue URLs, log group names, batch sizes

Environment variables from the template.

Watermarks, cursors, kill switches

The configs table. See Runtime state in configs.

The dividing line: a secret holds things that would cause harm if disclosed. Everything else is configuration, and configuration belongs somewhere reviewable.