Story 1336: Externalize the Atlas runtime ARN

This walkthrough records why the Atlas AgentCore runtime ARN moved out of source and into configuration, and how the broker now repoints.

Why (the catastrophe avoided)

Story-1214 replaces the CDK-provisioned Atlas runtime with a Terraform one. The new runtime gets a new ARN suffix. The chat/SSE broker held that ARN as a source literal, so cutover meant a code edit, review, build, and redeploy — a long staging outage in the seconds-matter cutover window. After this Story, repointing Atlas is a one-line config change plus redeploy (or just a param update once SSM-backed).

What changed

SurfaceBeforeAfter
src/app_api/router_agents.py (SSE/chat broker) Module-level literal _ATLAS_RUNTIME_ARN = "arn:aws:bedrock-agentcore:...pramaanAgents_atlas-Xk5GPs6zVp". Resolved lazily via _atlas_runtime_arn() from get_settings().agent_runtime_arn_atlas; fails fast with a clear PramaanError (pramaan.agents.runtime_arn_unset) when unset.
src/shared/settings.py No Atlas ARN field. New agent_runtime_arn_atlas field, alias AGENT_RUNTIME_ARN_ATLAS, default None.
src/clerk_worker/handler.py (intake-review) Already env-driven via AGENT_RUNTIME_ARN_ATLAS_INTAKE_REVIEW (Story-1222). Unchanged — it was already the good pattern; both consumers now resolve from config.
.github/workflows/deploy-app-api.yml No Atlas ARN env on the app-api Lambda. New "Resolve Atlas runtime ARN" step reads SSM param /pramaan/staging/agents/atlas-runtime-arn (single source) and injects AGENT_RUNTIME_ARN_ATLAS into the Lambda env on both create and update paths.

Why a lazy read, not module-level

The app-api Lambda imports router_agents at boot regardless of the dev-only SSE route flag (PRAMAAN_AGENTS_DEV_BUFFERED). A module-level os.environ[...] would crash the entire app-api boot whenever the var is absent. Resolving inside _atlas_runtime_arn() at request time keeps the failure scoped to an actual Atlas invocation — the only caller — and gives a clear, typed error.

Why settings.py, not os.environ

Repo doctrine (AGENTS.md) requires config to flow through src/shared/settings.py / get_settings() for handlers and services. The issue body suggested os.environ["AGENT_RUNTIME_ARN_ATLAS"] to mirror the clerk worker; the worker is the Lambda entrypoint where direct os.environ is the established exception, but router_agents is a router, so the typed-settings path is the correct home. The acceptance goal — no hardcoded ARN, config-only repoint, fail-fast when unset — is fully met either way.

Single source of truth

The SSM param /pramaan/staging/agents/atlas-runtime-arn is the one value that feeds the broker's AGENT_RUNTIME_ARN_ATLAS. The clerk worker's AGENT_RUNTIME_ARN_ATLAS_INTAKE_REVIEW is fed from the same value on the infra side (out of scope here). Repointing Atlas = update that one param + redeploy.

Zero behavior change now

Until the param exists, the deploy step falls back to the current CDK ARN (...pramaanAgents_atlas-Xk5GPs6zVp), so this deploy resolves to the exact same runtime staging already uses. The value only flips at the Story-1214 cutover.

Verification

grep -rn "pramaanAgents_atlas|Xk5GPs6zVp" src/   # clean: no runtime ARN literals in source
uv run ruff check src/ tests/                    # All checks passed
uv run pytest tests/unit/test_router_agents_arn.py tests/unit/test_sse_route_demoted.py   # 5 passed
make openapi && git diff --stat -- openapi/       # no contract drift

New test tests/unit/test_router_agents_arn.py proves the resolver returns the configured ARN and raises the typed error when unset. App-api imports cleanly with the var absent (lazy read confirmed).