Releases: Boise-State-Development/agentcore-public-stack
Release list
Release 1.0.4
Release Date: July 1, 2026
Previous Release: v1.0.3 (June 30, 2026)
⚠️ Upgrading from a beta? 1.0.4 is an in-place upgrade from any 1.0.x with no migration. Moving from a pre-1.0.0 beta is still the destructive backup → teardown → redeploy → restore migration described in the 1.0.0 notes. Brand-new deployments need none of this.
Highlights
v1.0.4 is a one-line-per-role IAM hotfix that restores AgentCore Memory functionality. Both the App API task role and the AgentCore Runtime execution role granted every memory data-plane action except bedrock-agentcore:GetMemory — the action the SDK's get_memory_strategies() call requires to resolve a memory's strategy IDs. Without it, strategy discovery failed silently: the Settings → memories/preferences page came back empty (the App API returned empty lists with a 200), and the agent stopped recalling long-term memories (the runtime kept writing conversation events but ran with retrieval disabled). Granting GetMemory on both roles fixes both symptoms. This is an infrastructure (IAM) change, so it deploys via the platform (CDK) pipeline.
🐛 Bug fixes
- Memory dashboard showed no memories/preferences.
GET /memorycallsget_memory_strategies()→bedrock-agentcore:GetMemory, which the App API task role didn't allow. The callAccessDenied, strategy discovery returned no IDs, and the endpoint returned emptyfacts/preferenceswith a 200 — so the page rendered blank even though records existed. (app-api-iam-grants.ts) - Agent didn't recall long-term memories. At session creation the runtime's
_discover_strategy_ids()makes the sameGetMemorycall to build its retrieval namespaces; the runtime execution role also lacked the action, so retrieval was silently disabled ("long-term memory retrieval disabled") while event writes (CreateEvent) continued to succeed. (inference-api-iam-roles.ts)
🏗️ Infrastructure
- Added
bedrock-agentcore:GetMemoryto theAgentCoreMemoryAccesspolicy statement on both roles — the App API Fargate task role (scoped to the memory ARN) and the AgentCore Runtime execution role (scoped tomemory/*). No other action names changed. Verified against the AWS Service Authorization Reference (GetMemoryis a Read action on thememoryresource type).
🚀 Deployment notes
This is an IAM change on PlatformStack, so it ships through the platform (CDK) pipeline, not the API-only backend path. After deploy, no data migration is needed and existing memories become visible immediately. Note the App API caches strategy discovery per process (functools.lru_cache) — a normal deploy rolls the ECS tasks and AgentCore Runtime, so the cache starts fresh; no manual restart required.
Release 1.0.3
Release Date: June 30, 2026
Previous Release: v1.0.2 (June 29, 2026)
⚠️ Upgrading from a beta? 1.0.3 is an in-place upgrade from 1.0.0/1.0.1/1.0.2 with no migration. Moving from any pre-1.0.0 beta is still the destructive backup → teardown → redeploy → restore migration described in the 1.0.0 notes. Brand-new deployments need none of this.
Highlights
v1.0.3 is a maintenance patch — no application code or user-facing behavior changes. It's almost entirely CI/CD pipeline work: platform and backend deploys are now serialized through a shared concurrency group so they can't race each other onto the same ECS service / AgentCore Runtime / Lambda, push-triggered (path-scoped) auto-deploys are back on for the platform/backend/frontend workflows, and the duplicated test gates are consolidated into one reusable workflow. Rounding it out is a small dependency + CodeQL sweep (a joserfc CVE patch and a couple of unused-import removals). Operators on 1.0.x upgrade in place with no migration.
🔒 Security & 📦 Dependencies
joserfc1.6.3 → 1.7.2 (backend) and 1.6.5 → 1.7.2 (backup-data tooling), remediating Dependabot GHSA-wphv-vfrh-23q5 / CVE-2026-48990. (#526)- Removed unused imports flagged by CodeQL —
Optionalinagents/main_agent/agent_types.py,ssminapp-api/app-api-environment.ts. (#526)
🔧 CI/CD
- Serialized deploys.
platform.ymlandbackend.ymlnow share one repo-global concurrency group (deploy-<ref>), so a CloudFormation deploy and the API-driven backend code deploys queue instead of running at once and stomping the same ECS service / AgentCore Runtime / Lambda. Frontend stays independent;cancel-in-progressstays false. (#525) - Auto-deploy restored. Push-triggered, path-scoped deploys are re-enabled for platform/backend/frontend (develop → development, main → production) after being manual-dispatch-only since v1.0.0. Each trigger is scoped to its own surface so unrelated changes don't redeploy. (#524)
- Reusable test gates. Duplicated test jobs are extracted into a shared
tests.ymlconsumed byci,platform,backend,frontend-deploy, andnightly-deploy-pipeline; skipped single-suite callers now render correct job labels instead of raw${{ }}expressions. (#524, #526) - Pipeline cleanup. Pruned dead nightly tracks (AI coverage analysis, merge-validation) and orphaned scripts;
docs-deploynow publishes frommain(wasdevelop), anddocs-deploy/releaseare fork-gated so forks syncingmaindon't auto-publish or auto-release. (#524)
🚀 Deployment notes
In-place patch on the single-stack PlatformStack — no new infrastructure, env vars, or migration. The only operator-visible change is to CI/CD behavior: pushes to develop/main once again auto-deploy (path-scoped), and platform vs. backend deploys now queue rather than run concurrently.
Release 1.0.2
Release Date: June 29, 2026
Previous Release: v1.0.1 (June 26, 2026)
⚠️ Coming from a pre-1.0.0 (beta) deployment? Read the 1.0.0 release notes first. There is no special upgrade path for 1.0.2 itself — if you're already on 1.0.0 or 1.0.1 you upgrade in place with no migration. But 1.0.0 was the single-stack consolidation, and upgrading from any beta to 1.0.0 (and therefore to 1.0.2) is a destructive backup → teardown → redeploy → restore migration, not an in-placecdk deploy. If you haven't already worked through it, do that before deploying 1.0.2: see Upgrading an existing deployment (1.0.0 notes) below, or the published guide at https://boise-state-development.github.io/agentcore-public-stack/deployment/upgrade/. Brand-new deployments need none of this.
Highlights
v1.0.2 is a small, security-focused patch on the 1.0.0 single-stack architecture with one notable behavior change. The headline is that assistants can use tools again: 1.0.0 had locked assistant chats to a knowledge-base-only, tool-free mode (#382), and this release reverts that so an assistant can once more leverage the user's selected MCP and built-in tools. Alongside it, this release lands a CodeQL security-hardening sweep (two HIGH findings around URL/host validation, a log-injection pass across 24 call sites, and a hardened CI checkout), remediates 6 Dependabot alerts (Astro XSS/SSRF, esbuild dev-server file read, pydantic-settings path traversal), and fixes the nightly coverage pipeline that broke when the single-stack refactor moved its scripts. There is no migration — operators on 1.0.0 or 1.0.1 upgrade in place.
Assistants can use tools again
1.0.0 introduced a deliberate restriction: assistant ("RAG") chats ran knowledge-base-grounded with zero external tools — the inference API forced enabled_tools=[] on assistant turns and the system prompt told the model it had no external tools (#382). That made assistants safe and predictable but also meant they couldn't search the web, hit an MCP server, or run code even when the user had those tools enabled. v1.0.2 reverts the restriction so assistants behave like a normal chat with the assistant's knowledge and instructions layered on top.
What stays the same: knowledge-base context is still pre-stuffed into the user message, and the assistant's custom instructions still apply. What changes: the user's tool-picker selection now flows through to the agent on assistant turns, and assistant chats once again emit tool-use and MCP-App events.
Backend
inference_api/chat/routes.py— dropped theenabled_tools=[]override in therag_assistant_idbranch so the client's tool selection reaches the agent, and removed the "Knowledge Base Grounding / no external tools" directive from both the with-instructions and no-instructions system-prompt paths (restoring the pre-#382 prompt composition).
Frontend
chat-request.service.ts— no longer force-sendsenabled_tools=[]on assistant turns; the user's tool-picker selection rides along (skills mode stays gated on non-assistant turns).preview-chat.service.ts— the editor preview now forwards the owner's enabled tools instead of[], and builds the streaming assistant message as ordered content blocks (text interleaved with tool use) wired toonToolUse/onToolResult, so the shared message-list renders tool cards in the preview exactly like a consumer chat.
Test coverage
Specs updated to assert tools are forwarded on both assistant and preview turns (chat-request.service.spec.ts, preview-chat.service.spec.ts).
🐛 Bug fixes
- Nightly coverage pipeline was failing. The single-stack refactor removed
scripts/stack-app-api/andscripts/stack-frontend/, butnightly.yml'stest-backend,test-frontend, andinstall-frontendjobs still called them, so they died with "No such file or directory." The three scripts were ported to the sanctioned post-refactor layout —scripts/backend/test.sh,scripts/frontend/install.sh, andscripts/frontend/test.sh— with behavior preserved 1:1 (uv install/sync +pytest --cov;npm cifor frontend and infra;ng test --no-watch --coverage). (#518)
🔒 Security
This release closes a CodeQL sweep (#521) and 6 Dependabot alerts (#520), each with regression tests where applicable.
CodeQL code findings (#521):
- HIGH
py/incomplete-url-substring-sanitization—external_mcp_clientpreviously substring-checked the whole URL for an AWS marker before deciding to SigV4-sign. A crafted URL with the marker in a path, query, or userinfo segment could trick it into attaching IAM credentials to a non-AWS host. It now parses the host withurlparseand matches an anchored suffix. Covered byTestAwsUrlHostSanitization(adversarial URLs). - HIGH
js/regex/missing-regexp-anchor—admin-tool.modelnow parses the host vianew URLand anchors the AWS-endpoint regexes ($) so a spoofed host can't satisfy the match. Covered byadmin-tool.model.spec.ts(13 cases including spoofed hosts). - MEDIUM
py/log-injection(24 sites across 16 files) — a newapis.shared.security.scrub_log()helper neutralizes CR/LF and control characters; every flagged user-controlled log value is now wrapped. Covered bytest_log_sanitize.py. - MEDIUM
actions/untrusted-checkout— allinputs.refcheckouts innightly-deploy-pipeline.ymlnow setpersist-credentials: false. - WARNING
py/regex/duplicate-in-character-class— removed a stray[from are.VERBOSEcomment that the regex parser misread as a character class.
Dependency CVE remediation (#520):
| Component | Package | From | To | Fix |
|---|---|---|---|---|
| docs-site | astro |
6.3.1 | 6.4.8 | Reflected XSS via slot name, host-header SSRF in prerendered error page, spread-attribute XSS |
| docs-site | esbuild |
— | 0.28.1 (override) | Dev-server arbitrary file read (GHSA-g7r4-m6w7-qqqr) |
| frontend | esbuild |
— | 0.28.1 (override) | Dev-server arbitrary file read (transitive via @angular/build 21.2.16) |
| backend | pydantic-settings |
2.13.1 | 2.14.2 | NestedSecretsSettingsSource symlink traversal / local file read (GHSA-4xgf-cpjx-pc3j) |
🚀 Deployment notes
v1.0.2 is a patch on the single-stack PlatformStack architecture. Operators on 1.0.0 or 1.0.1 upgrade in place — no migration, no new infrastructure, no new env vars.
- Behavior change to be aware of: after deploying, assistant chats will once again use whatever tools the user has enabled (web search, MCP servers, code interpreter, etc.) rather than running knowledge-base-only. If your deployment relied on assistants being tool-free, note that this 1.0.0 restriction has been intentionally reverted.
- The security and dependency fixes require no operator action beyond deploying the new images/SPA build.
Release 1.0.1
Release Date: June 26, 2026
Previous Release: v1.0.0 (June 24, 2026)
⚠️ Coming from a pre-1.0.0 (beta) deployment? Read the 1.0.0 release notes first. 1.0.1 lands just two days after 1.0.0, so most operators haven't deployed 1.0.0 yet. There is no special upgrade path for 1.0.1 itself — if you're already on 1.0.0 you upgrade in place with no migration. But 1.0.0 was the single-stack consolidation, and upgrading from any beta to 1.0.0 (and therefore to 1.0.1) is a destructive backup → teardown → redeploy → restore migration, not an in-placecdk deploy. If you haven't already worked through it, do that before deploying 1.0.1: see Upgrading an existing deployment (1.0.0 notes) below, or the published guide at https://boise-state-development.github.io/agentcore-public-stack/deployment/upgrade/. Brand-new deployments need none of this.
Highlights
v1.0.1 is the first patch on top of the 1.0.0 general-availability release, and it ships two operator- and user-facing additions. Save conversations to connected apps ("Save to…") extends the existing connector/adapter pattern in the write direction: a user can push a full conversation transcript out to an app they've connected — Google Drive is the reference destination — as a native Google Doc, Markdown, or plain-text file, reusing the same OAuth consent, RBAC visibility, and AgentCore Identity token flow as document import. External (cross-account) Route53 hosted zones lets deployments whose DNS zone lives in a different AWS account (or is managed out-of-band) stand up the full platform without the deploy failing on an in-account hosted-zone lookup. No action is required for existing 1.0.0 deployments; both features are additive.
Save conversations to connected apps ("Save to…")
The platform already lets a user connect an app and pull documents into an assistant's knowledge base. v1.0.1 adds the opposite direction: save a conversation transcript out to a connected app. The architectural move is to split the existing connector pattern into a direction-agnostic auth layer (OAuthProvider + AgentCore Identity + consent UX, reused as-is) and a direction-specific capability layer — a new ExportTargetAdapter registry mirroring the read-side FileSourceAdapter registry. Google Drive is the first export target; adding OneDrive / SharePoint / Dropbox later is "write one adapter class, register it, and have an admin map a connector." (#507, #508, #509, #510, #511)
Backend
- New
apis.app_api.export_targetspackage: anExportTargetAdaptercontract (adapter.py), a code-shippedregistry, the referenceadapters/google_drive.py(creates a Drive file via the user's own token), arender.pytranscript renderer (native Google Doc / Markdown / plain text),models.py(ExportFormat,ExportInclude,ExportTargetError), andservice.py(connector resolution, RBAC visibility gate, AgentCore Identity token mint with consent/503handling). - User-facing routes (
export_targets/routes.py, on app-api — the inference-API boundary only proxies/invocations+/ping):GET /export-targetsreturns the catalog the "Save to…" dialog reads (per-connectorconnectedstate,supportedFormats, and abrowsableflag for the folder picker), andPOST /sessions/{id}/exportrenders the full transcript (paged, with a runaway guard) and creates the document via the resolved adapter. A409signals the user must complete OAuth consent; a503signals workload/callback misconfiguration. - Admin mapping: new
GET /admin/export-target-adaptersplus anOAuthProvider.export_target_adapter_idfield — a connector becomes an export target only once an admin maps it to a shipped adapter. Export receipts are persisted to session metadata (ExportReceiptonsessions/models.py, written best-effort byadd_export_receipt) so the SPA can reflect "saved" state.
Frontend
- New
ExportDialogComponent(the "Save to…" dialog: connector picker, format picker driven bysupportedFormats, optional destination-folder picker reusing the file-source browse dialog) and anExportService(session/services/export/). A "Save to…" action is added to the conversation list (session-list). Admin connector form gains an export-target-adapter mapping dropdown.
Test coverage
1,400+ lines of new tests: test_export_routes.py, test_export_target_service.py, test_export_google_drive.py, test_export_render.py, and test_export_target_adapters_admin.py on the backend; export-dialog.component.spec.ts and export.service.spec.ts on the frontend.
External (cross-account) Route53 hosted zones
Deployments where the Route53 hosted zone for domainName lives in a different AWS account — or is otherwise managed out-of-band — previously failed: the stack's in-account HostedZone.fromLookup + ALIAS/A record creation cannot reach a zone it doesn't own. v1.0.1 makes DNS record management optional so these deployments succeed, with the platform emitting the records an operator needs to create by hand. (#512)
Infrastructure
- New
manageDnsRecordsconfig flag (envCDK_MANAGE_DNS_RECORDS, contextmanageDnsRecords; defaults totrue, so existing single-account deployments are unaffected). Loaded ininfrastructure/lib/config.tsand threaded into the four custom-domain origins. - When
manageDnsRecords=false, the SPA, ALB, artifacts, and mcp-sandbox constructs still attach the custom domain + ACM certificate to each origin but skip the in-accountHostedZone.fromLookupand ALIAS/A record creation. Instead each origin emitsCfnOutputs with the record name and alias target (e.g.AlbDnsRecordName/AlbDnsAliasTarget,FrontendDnsRecordName,ArtifactsDnsRecordName/ArtifactsDnsAliasTarget,McpSandboxDnsRecordName/McpSandboxDnsAliasTarget) so an operator can create the records manually in the owning account.
CI/CD
CDK_MANAGE_DNS_RECORDSplumbed end-to-end: exported and passed as a--contextflag inscripts/common/load-env.sh, and added to the job-levelenv:of theplatform.yml,teardown.yml, andnightly-deploy-pipeline.ymlworkflows. The nightly smoke test reads it as well.
Docs
- Deployment docs updated for the cross-account workflow —
docs-site(environments, platform-cdk, troubleshooting) and.github/docs/deploy/(GitHub config, troubleshooting) plus.github/ACTIONS-REFERENCE.md.
🚀 Deployment notes
v1.0.1 is a patch release on the single-stack PlatformStack architecture introduced in 1.0.0. Operators already on 1.0.0 upgrade in place — there is no migration. Both features are additive and off by default until configured.
- Save to… (export targets): opt-in. An admin maps an existing connector (e.g. the Google Drive connector) to the
google-driveexport-target adapter from the admin connector form; until a connector is mapped, the "Save to…" dialog shows no destinations. No new infrastructure or env vars. - Cross-account DNS: if your hosted zone is in the same AWS account as the deployment (the default), no action is required —
manageDnsRecordsdefaults totrueand behavior is unchanged. If your zone lives in a different account (or you manage DNS out-of-band), setCDK_MANAGE_DNS_RECORDS=false(GitHub Actions Variable), then after the deploy read the*DnsRecordName/*DnsAliasTargetCloudFormation outputs and create the matching ALIAS/A records in the owning account for the SPA, ALB, artifacts, and mcp-sandbox origins.
Release 1.0.0
Release Date: June 24, 2026
Previous Release: v1.0.0-beta.27 (May 20, 2026)
Highlights
This is 1.0.0 — the first general-availability release. After 27 betas, the platform is stable, and the headline of this release is as much about the foundation as the features built on it: the entire CDK app collapses from nine CloudFormation stacks into a single PlatformStack with a platform-as-bootstrap code-deploy model, so day-to-day code changes ship in ~2 minutes via AWS APIs and cdk deploy runs only when infrastructure actually changes.
On top of that foundation, 1.0.0 lands a large slate of product work: Conversation Modes (admin-curated system prompts users opt into), file-source connectors and website crawling that turn external systems and the open web into assistant knowledge bases, self-service AgentCore Gateway MCP targets, a curated model catalog with a new Amazon Bedrock Mantle provider, per-turn context attribution, and a public Starlight documentation site. It also delivers a complete backup/restore disaster-recovery toolchain, a coordinated security-hardening sweep, and remediation of all 22 HIGH Dependabot findings.
Action required for operators with an existing deployment. Because 1.0.0 consolidates the old nine-stack architecture into a single PlatformStack, upgrading any prior (beta) deployment is a destructive backup → teardown → redeploy → restore migration — not an in-place cdk deploy. We've written step-by-step instructions to make it as painless as possible. Do not deploy over an existing environment without reading the Upgrading an existing deployment section below first. Brand-new deployments need no special steps.
Upgrading an existing deployment
Read this before you deploy 1.0.0 over any existing environment.
1.0.0 replaces the previous nine-stack CloudFormation layout with a single PlatformStack. There is no in-place upgrade path from a beta deployment — the old stacks must be torn down and replaced. Your data is preserved through a backup/restore cycle, but the steps are destructive and must run in order. We've written and tested detailed, click-by-click instructions so you can work through it confidently.
Start here — the full step-by-step guides:
- 📖 Upgrading from Multi-Stack (published docs site) — the complete walkthrough with screenshots-level detail, a timeline, rollback steps, and migration gotchas.
- 📄 In-repo copy:
.github/docs/deploy/upgrade-from-multi-stack.md
The migration at a glance (≈45–75 min total — see the guide for the exact inputs for each workflow):
- Back up — run the Backup Data (Pre-Migration) workflow. This is the most critical step; confirm
summary.failedis zero and note the{prefix}-backup-{timestamp}bucket name. Do not proceed on a failed backup. - Tear down — run the Teardown All Infrastructure workflow (
confirm: DESTROY) to delete the old stacks. If your environment retained stateful resources (CDK_RETAIN_DATA_ON_DELETE=true), clear them — at minimum delete the legacy/{prefix}/{app-api,inference-api}/image-tagSSM parameters, which otherwise fail the first new deploy. - Redeploy — run Platform Stack → Backend Deploy → Frontend Deploy → Seed Bootstrap Data.
- Restore — run the Restore Data workflow against your backup bucket with
dry_run: truefirst, thendry_run: false. - Verify — confirm login, chat history, file uploads, the admin dashboard, and RAG assistants.
⚠️ Two things to know going in: the backup bucket is immutable and survives every teardown (it's your safety net and rollback source), and Cognito passwords do not transfer — native-password users must use "Forgot Password" on first login, while federated (OIDC/SAML) users are unaffected.
Brand-new deployments skip all of this — see Deployment notes.
Single-stack platform-as-bootstrap architecture
The biggest structural change in the project's history: the CDK app that used to be nine CloudFormation stacks is now one PlatformStack.
Why this overhaul. The multi-stack layout treated the platform like a fleet of independently deployable microservices — but the application is, by definition, a monolith: one cohesive product whose pieces are released together, version-locked, and only ever deployed as a unit. Splitting it across nine stacks bought none of the benefits of microservices and all of their operational cost. Cross-stack Fn::ImportValue references created brittle deploy-ordering requirements; a change in one stack routinely forced careful, manual sequencing of the others; and the seams between stacks were a constant source of deployment issues and gotchas — exported-value locks that blocked updates, drift between stacks that had to be reconciled by hand, and first-deploy chicken-and-egg problems. Consolidating into a single PlatformStack removes that entire class of failure: there are no cross-stack references to order, no inter-stack drift to reconcile, and one cdk deploy either succeeds or rolls back as a whole. Treating the monolith as a monolith from a DevOps standpoint is simpler to reason about, faster to deploy, and dramatically less error-prone.
Infrastructure
infrastructure/lib/platform-stack.tscomposes ~39 single-responsibility constructs underlib/constructs/(network, identity, data, rag, artifacts, mcp-sandbox, agentcore, inference-api, app-api, fine-tuning, spa, zones). It is built in two phases — the constructor (data + edge + Cognito + AgentCore Memory/Code-Interpreter/Browser/Gateway) andwireCompute()(Inference Runtime + SageMaker + App API Fargate) — which eliminates every cross-stackFn::ImportValueand all deploy-ordering between stacks.npx cdk listnow returns exactly${prefix}-PlatformStack.- Platform-as-bootstrap. CDK ships small, byte-stable placeholder assets from
infrastructure/bootstrap-assets/{app-api,inference-api,rag-ingestion,artifact-render}/(stdlib HTTP servers / 503 handlers). The real code ships out-of-band via AWS control-plane APIs:aws ecs register-task-definition+update-service(app-api Fargate),aws bedrock-agentcore-control update-agent-runtime(inference-api Runtime), andaws lambda update-function-code(rag-ingestion image Lambda + artifact-render zip Lambda). Because CFN tracks eachCode/imageproperty from its own constant model, subsequent Platform deploys leave the out-of-band-deployed real code untouched. - All per-component CDK feature flags were removed (
CDK_FRONTEND_ENABLED,CDK_APP_API_ENABLED,CDK_INFERENCE_API_ENABLED,CDK_GATEWAY_ENABLED,CDK_FILE_UPLOAD_ENABLED,CDK_ASSISTANTS_ENABLED,CDK_RAG_ENABLED,CDK_FINE_TUNING_ENABLED,CDK_ARTIFACTS_ENABLED,CDK_MCP_SANDBOX_ENABLED). The platform now deploys everything, always.
CI/CD
- New
platform.yml(CDK),backend.yml(build → API-driven code deploy), andfrontend-deploy.ymlworkflows replace the legacy per-stack workflows, which were deleted along with their scripts and tests. A content-hash Docker build pipeline underscripts/build/skips a rebuild when the computed hash already exists as an ECR tag. Day-to-day backend code deploys in ~2 minutes without touching CloudFormation;cdk deployruns only on real infrastructure changes.
Test coverage
Carried forward from the refactor's stabilization: 7 policy-level assertions in infrastructure/test/security-policy.test.ts (Action:* + Resource:* prohibition, BFF-cookie-key Decrypt-only, every bucket SSE + public-access-block + enforceSSL, every DDB table SSE), 5 in compute-image-resolution.test.ts (SSM-resolved image shape), 2 in ssm-safety.test.ts (same-stack valueForStringParameter deadlock at synth), and a tests/supply_chain/test_env_var_contract.py reflection test that fails on any orphan CDK env var.
Conversation Modes
Shipped enabled. Admins curate a catalog of custom system prompts ("Guided Learning", "Concise", and so on) that users opt into per conversation.
Backend
- New
apis.shared.system_promptsmodule (models / repository / service) with optimistic-concurrency updates so a concurrent delete+edit can't resurrect a deleted prompt. Admin CRUD/admin/system-prompts(fullprompt_text) and a user read/system-prompts(name + description only — prompt text stays server-side). Inference resolves the active prompt viachat/system_prompt_resolver.pyand appends it to the base prompt; gating skips resume, continuation, preview, and assistant-attached turns. Selection precedence is request-body-first (so the first turn of a new session works without a metadata round-trip), with session preferences as fallback.
Infrastructure
- New
SystemPromptsTableDynamoDB construct (envDYNAMODB_SYSTEM_PROMPTS_TABLE_NAME; app-api CRUD, inference-apiGetItemonly); name + ARN published to SSM.
Frontend
- Lazy
SystemPromptsService, admin list/form pages, and a per-conversation chip + radio group in the settings panel.
Knowledge bases: file-source connectors and website crawling
Two complementary ways to fill an assistant's knowledge base from outside a manual upload.
File-source connectors
A four-PR arc turns OAuth connectors into RAG document sources. A provider-agnostic backend (FileSourceAdapter ABC + shipped-code-only registry, normalized FileEntry/BrowseResult/SourceRoot/DownloadedFile contract) ships with a GoogleDriveAdapter (Drive v3 browse/search/download including native-doc export). The Document model gains provenance (sourceConnectorId/sourceAdapterKey/sourceFileId/sourceEtag/importedByUserId). Admins opt a connector in by mapping it ...
Release 1.0.0-beta.27
Release Date: May 20, 2026
Previous Release: v1.0.0-beta.26 (May 13, 2026)
Highlights
The largest release since the BFF cutover. Beta.27 lands two new user-visible surfaces, both built on top of brand-new CDK stacks, plus a major admin redesign and a handful of inference-API correctness fixes.
- Artifacts — the agent can now produce versioned, iframe-isolated HTML, Markdown, and code artifacts that render in a docked side panel beside the chat. Backed by a new
ArtifactsStack(S3 + DynamoDB + render Lambda + CloudFront onartifacts.{domain}) and short-lived JWT render tokens minted by app-api. - MCP Apps host renderer — third-party MCP servers can ship UI alongside their tools. The agent advertises a UI extension on
initialize, fetchesui_resourcepayloads viaresources/read, and the SPA frames them in a sandboxed<mcp-app-frame>over a strict CSP, with an app-initiatedtools/callproxy and explicit user consent. Backed by a newMcpSandboxStack(CloudFront origin onmcp-sandbox.{domain}with dynamic per-resource CSP via a CloudFront Function). Default-on this release. - Admin shell redesign — the 15-card admin grid is replaced with a persistent grouped sidebar, and dense list redesigns for models and tools turn cards into compact expandable rows. Quotas and Fine-Tuning collapse from seven sibling routes into two tabbed pages.
- Recoverable
max_tokenstruncation — what used to be a leaky, infinite-loopingMaxTokensReachedExceptionis now an inline "Response length limit reached" notice with a Continue button that resumes the truncated turn instead of resending the prompt. Survives a page refresh. - Model-aware adaptive thinking — Opus 4.7's 400 on
thinking.type=enabledis fixed: Opus 4.6/4.7, Sonnet 4.6, and Mythos now emit{type: adaptive, display: summarized}and depth is governed by a new admin- and user-configurableeffortknob. Older models keep the legacyenabledshape. /pingreaper fix — fixes silent mid-stream microVM reaping by emitting the integertime_of_last_updatefield AgentCore's idle reaper requires. Workaround forbedrock-agentcore-sdk-python#471until async-task busy tracking lands.- Pre-migration backup tool —
scripts/backup-data/produces a complete, restore-friendly snapshot of all DynamoDB tables, user-content S3 buckets, and Cognito (config + users + groups + IdPs + plaintext app-client secrets) for a givenCDK_PROJECT_PREFIX. Workflow-dispatch wired. - Dependency upgrades —
bedrock-agentcore1.6.4 → 1.9.1 (with coupledboto31.42.96 → 1.43.9) andstrands-agents1.39.0 → 1.40.0.
This release adds two new CDK stacks (ArtifactsStack, McpSandboxStack) and one new DynamoDB table (user-menu-links). Both new stacks are gated by config flags. Deploy order matters — see "Deployment notes" below.
Artifacts
The agent can now author versioned standalone documents — HTML pages, charts, Markdown reports — that render in a sandboxed iframe alongside the chat. Artifacts solve two problems the existing create_visualization and Code Interpreter outputs couldn't: persistence (the user can re-open and download), and isolation (HTML/JS runs in a cross-origin sandbox so it can't read cookies or the SPA DOM).
Architecture
A new leaf stack, ArtifactsStack, owns the rendering pipeline:
- DynamoDB
user-artifactstable — version log + HEAD pointer per artifact. PKUSER#{user_id}, SKARTIFACT#{aid}#V#{version:05d}for versions andARTIFACT#{aid}#HEADfor the latest pointer. GSI1 indexes bySESSION#{session_id}so the SPA can list artifacts produced in the current chat. - S3
artifacts-contentbucket — private, no CORS. Layout{user_id}/{aid}/v{n}/index.html. Versions are immutable: there's nos3:DeleteObjectgrant on the inference-api role, so anupdate_artifactwrites a new version and re-points HEAD instead of mutating. - Render Lambda — validates a render-token JWT scoped to one
(artifact_id, version), fetches the blob from S3, and returns it with a strict per-origin CSP that allows inline<style>/<script>plus scripts fromcdn.tailwindcss.com,esm.sh,cdn.jsdelivr.net, andunpkg.com.connect-src 'none'— artifacts cannot make outbound network calls. - CloudFront distribution on
artifacts.{domain}— terminates TLS, attaches the security-headers policy. The artifact origin is intentionally a different cookie-jar host from the SPA so a script in an artifact can't read__Host-bff_session. - HMAC signing key — the render-token signing secret lives in Secrets Manager in
InfrastructureStack(notArtifactsStack), so app-api and the render Lambda can both read it withoutArtifactsStackbecoming a stack-dependency root. App-api mints short-lived JWTs that the SPA embeds as the iframesrc.
Agent tools
Two new built-in tools, registered as default public tools so the feature is usable on first deploy without an admin opting them in per role:
create_artifact(title, content, content_type="text/html; charset=utf-8")— writes v1. HTML mode requires a complete standalone document (<!doctype html>+ full<html>); Markdown mode (content_type="text/markdown") takes raw GFM and the writer wraps it in a self-contained HTML render harness server-side.update_artifact(artifact_id, content, ...)— writes a new version and re-points HEAD; the render-token mints against the latest version when the panel updates.
The system prompt documents the dual authoring contract and the CSP allowlist (Chart.js auto-registering build, import Chart from "https://esm.sh/chart.js@4/auto" etc.) so the model produces output that actually renders.
SSE + SPA
A new artifact SSE event streams from the inference-api each time the agent creates or updates an artifact. The frontend has:
ArtifactStateService+ArtifactHttpService+ArtifactDownloadService— signal-backed state, render-token fetch, blob download.- A docked, resizable artifact panel beside the chat that auto-opens on first creation, shows a skeleton while loading, and on update jumps to the latest version. Per-version history cards in the panel let the user step backwards through revisions.
- An inline artifact card anchored to the producing message, with a preview/code toggle (syntax-highlighted source view) and a download button on both the card and the panel.
- Full-width inline cards, scoped
isolation: isolatez-indexing so a focused artifact card doesn't escape its message row, and live tool-output streaming into the tool rail while the artifact is being authored.
Configuration
Artifacts is opt-in at deploy time via CDK_ARTIFACTS_ENABLED=true. When enabled, CDK_HOSTED_ZONE_DOMAIN and CDK_ARTIFACTS_CERTIFICATE_ARN become required. Validation runs on every stack synth, so all five consumer GitHub workflows now thread these env vars through the OIDC composite action — a missing var on a non-ArtifactsStack workflow would otherwise fail synth.
MCP Apps Host-Renderer
A scoping document landed early in the cycle (docs/kaizen/scoping/mcp-apps-host-renderer.md) and the implementation followed a deliberate seven-PR sequence (#339 PR #0 → #349 PR #7). The result: third-party MCP servers can ship a small interactive UI alongside their tools, and that UI renders in a sandboxed iframe with the same isolation guarantees as artifacts.
Architecture
A new leaf stack, McpSandboxStack, mirrors the artifacts pattern:
- CloudFront distribution on
mcp-sandbox.{domain}— fronts an S3 origin that serves a tiny "basic-host" mount page. App URLs land atmcp-sandbox.{domain}/<resource-encoded-path>, the mount page reads the encoded resource URL from the path and frames the actual MCP App content in an inner blob iframe withallow-same-originmatching the basic-host reference. - Dynamic per-resource CSP — a CloudFront Function on the viewer-response decodes a
?csp=query param (URL-encodedframe-ancestorssource list scoped to that one resource) and emits a per-requestContent-Security-Policyheader. The function source is loaded fromassets/mcp-sandbox/csp-function.jsand theframe-ancestorsallowlist is JSON-injected at synth — the substitution asserts the placeholder marker is present exactly once so a future refactor that loses it fails loudly at synth, not at edge runtime. - Outer
frame-ancestorsallowlist — configurable viamcpSandbox.extraFrameAncestorsso a deploy can permit framing from custom origins (preview environments, alternate SPA hosts) without rebuilding the function asset.
MCP protocol surface
The agent now advertises an experimental.ui extension during MCP initialize so a server knows whether the host can render UI. Tools whose only output is a ui_resource are filtered out for non-capable clients (the existing API-key path, scripted callers).
When a tool result references a UI resource, the agent fetches it via the standard MCP resources/read flow and emits a ui_resource SSE event with uri, permissions, and a sandboxOrigin that points at the deployed mcp-sandbox host (sourced from SSM, so the value is correct per environment). Two app-initiated message types complete the protocol:
ui/message— the App pushes structured data into the chat input as a tool-input draft (acts like a smart form).ui/update-model-context— the App contributes context the agent should consider on the next turn.tools/callproxy — the App can invoke other tools on the same MCP server. The frontend brokers these through app-api over an event broker rather than letting an iframe call the Bedrock runtime directly.
Frontend
<mcp-app-frame>Angular custom element + apostMessagebridge that enforces the allowed message types and rejects unknown origins.- A consent prompt rendered as an inline message component — the user explicitly approves an App before it gets framed. Consent d...
Release 1.0.0-beta.26
Release Date: May 13, 2026
Previous Release: v1.0.0-beta.25 (May 11, 2026)
Highlights
A small, focused release that lands two operator-facing fixes and one user-facing feature on top of the beta.25 production hardening. The big ones: multi-sheet XLSX support in the spreadsheet analysis tool with defensive caps so a pathological workbook can't blow up latency or context, and an async refactor of the spreadsheet file-lookup path that closes a regression where concurrent chat load could block the event loop. Also shipping a user default model preference applied at chat time, a green nightly E2E pipeline after a multi-attempt fix, and upstream contribution governance — PRs are now restricted to approved collaborators (GitHub "Collaborators only") and Dependabot version-update PRs are disabled in favor of manual weekly upgrades.
This release has no schema or infrastructure changes. Deploy in any order.
Multi-Sheet XLSX Support in Spreadsheet Analysis
The spreadsheet analysis tool from beta.25 only handled the first sheet of an XLSX file, which silently misled the agent on multi-tabbed workbooks (financial models, fine-tuning datasets, anything from a real BI export). Beta.26 expands the tool to convert every sheet into its own predictable CSV, with sane defaults that protect the latency budget and the model's context window from pathological inputs.
Backend
backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.py— adds two environment-configurable caps (MAX_SHEETS_TO_CONVERT,MAX_ROWS_PER_SHEET) so a workbook with thousands of small sheets can't blow out the Code Interpreter sandbox. New helpers:_sanitize_sheet_name()produces filesystem-safe deterministic CSV filenames (stem.sheetname.csv) so the model's downstream code paths are predictable_parse_sheet_inventory()extracts structured sheet metadata from the bootstrap stdout withouteval/literal_evalon untrusted output_safe_int()parses bootstrap integers defensively_format_sheet_note()generates a markdown footer documenting which sheets converted, which were truncated, and the per-sheet CSV paths — surfacing caps to the model with actionable warnings rather than silently wrong results
- Tool docstring documents the dual contract: single-sheet workbooks keep the legacy
stem.csvfast path; multi-sheet workbooks get per-sheet CSVs plus a primary alias for the first sheet backend/src/agents/main_agent/core/system_prompt_builder.py— system-prompt guidance updated so the model handles per-sheet filenames correctly on retries
Test Coverage
2,800+ lines of new tests across backend/tests/agents/builtin_tools/spreadsheet_analysis/:
test_analyze_tool_integration.py(779 lines) — multi-sheet XLSX and CSV workflows end-to-endtest_sheet_inventory.py(307 lines) — parser robustness against malformed bootstrap outputtest_build_preview_code.py(127 lines) — filename escaping for quotes and special characters viarepr()indirection (closes a code-generation injection edge case)test_clean_stderr.py(202 lines) —MAX_ERROR_CHARSbudget is now respected strictly, accounting for ellipsis lengthtest_helpers.py,test_find_file.py,test_list_spreadsheets.py,test_strip_first_row.py— coverage for the smaller utilities
A small robustness fix landed alongside the tests: code generation now stashes the filename as a _FNAME variable inside the generated snippet to prevent f-string interpolation conflicts when filenames contain quotes or braces.
Async Spreadsheet File Lookups
The analyze_spreadsheet and list_spreadsheets tools shipped in beta.25 ran synchronous DynamoDB queries on the event loop (_find_file, _get_kb_files, _get_session_files), and the inference-api _build_tabular_inventory chat-route helper used a nested asyncio.run + thread pool executor pattern that could block under concurrent chat load. This release converts the entire path to native async: tool entry points are async def, every DynamoDB query is offloaded via asyncio.to_thread, and the inference-api helper awaits directly. This fixes a regression introduced in #260 where high-concurrency chat traffic could stall the event loop during file lookups — the same class of bug the BFF middleware fix in beta.25 addressed for session resolution.
Backend
backend/src/agents/builtin_tools/spreadsheet_analysis/analyze_tool.pyandlist_spreadsheets_tool.py—analyze_spreadsheet,list_spreadsheets,_find_file,_get_kb_files,_get_session_filesare allasync def; DynamoDB calls offload viaasyncio.to_threadbackend/src/apis/inference_api/chat/routes.py—_build_tabular_inventoryis nowasyncand awaits the file-operation calls directly. Replaces the nestedasyncio.run+ thread pool executor pattern that could deadlock under load
User Default Model Preference
User-saved default model preferences (set in Settings → Chat Preferences) are now actually applied when the chat starts. Previously the persisted defaultModelId was ignored and chat fell back to the hardcoded factory default — closes issue #161.
Backend
backend/src/apis/app_api/chat/routes.pyandbackend/src/apis/inference_api/chat/routes.py— new_resolve_user_default_model()helper looks up the persisteddefaultModelIdfrom user settings. Applied inchat_agent_streamand the invocations endpoint when the request does not specify amodel_id- RBAC re-checks the resolved default at chat time, so a user whose access to the previously-saved default has been revoked falls back gracefully rather than getting a permission error mid-stream
- A missing user-settings table now surfaces as
503 Service Unavailableinstead of silently dropping the user choice backend/src/apis/app_api/user_settings/routes.py— defaults endpoint adjustments
Frontend
frontend/ai.client/src/app/session/services/model/model.service.ts— supports persisted default model resolutionfrontend/ai.client/src/app/settings/pages/chat-preferences/chat-preferences-settings.page.ts— Chat Preferences page now wires the default model picker to the persisted setting
Test Coverage
model.service.spec.ts— 56 lines covering the default-model resolution flowchat-preferences-settings.page.spec.ts— 101 lines covering the settings UI
Nightly E2E Pipeline Restored
The nightly E2E pipeline had been red since the multi-stack deployment hit a series of cookie/JWT validation issues against the dynamic CloudFront URL. This release lands the fixes that turn the pipeline green:
- CloudFront URL handling for cookie auth in the test environment
- CDK certificate ARN wiring through the nightly job
- Increased agent test time limits (the multi-tool turns were tripping default timeouts)
- Switched the nightly suite from global Bedrock model IDs to US-region IDs to avoid cross-region routing flakes
- Rebased fix branch on
developto pick up the release-notes strategy update from #248
Upstream Contribution Governance
A non-code change worth flagging because it changes how external contributors interact with this repository.
CONTRIBUTING.md— pull requests are now restricted to approved collaborators only (GitHub "Collaborators only" setting). The repository remains source-available under PolyForm Noncommercial 1.0.0; issues stay open to everyone for bug reports and proposed changes, and a maintainer triages each one. The contributing guide explains the path: open an issue → maintainer triages → maintainer either implements upstream or coordinates next steps with the reporter..github/dependabot.yml—open-pull-requests-limit: 0across all four ecosystems (pip, frontend npm, infrastructure npm, github-actions). Scheduled version-update PRs are off; we handle dependency upgrades manually on a weekly cadence. Dependabot security updates are unaffected — when a CVE is published against a dependency, you'll still see a PR.
The full schedules, groups, and labels are retained in the config so flipping the limit back to a positive number restores the previous behavior with a one-line change.
Documentation
backend/src/.env.example— BFF cookie encryption architecture documentation updated to reflect the beta.25 shift from direct KMS cookie encryption to Secrets Manager-mediated approach. Clarifies that theBFFCookieSigningKeyCMK now encrypts the Secrets Manager secret at rest (not the cookie directly), documents the newBFF_COOKIE_DATA_KEY_SECRET_ARNvariable, explains the cross-task SHA-256 derivation, and adds the SSM parameter path for locating the secret ARN with an example ARN format
📦 Dependencies
No dependency upgrades in this release. Dependabot version-update PRs are disabled going forward; the next deps refresh will land as a manually curated batch.
🏗️ Infrastructure
No infrastructure changes. No new resources, no IAM changes, no SSM parameter changes.
🔧 CI/CD
- Nightly E2E pipeline fixes (#290) — CloudFront URL handling, CDK certificate ARN, agent test timeouts, US-region Bedrock model IDs
🚀 Deployment notes
- Deploy in any order. No schema, infrastructure, or IAM changes.
- After deployment, set the
MAX_SHEETS_TO_CONVERTandMAX_ROWS_PER_SHEETenv vars on the Inference API task definition if you want non-default caps for the spreadsheet analysis tool. Reasonable defaults are baked into the code; only set these if your workbooks routinely need higher limits. - Manual follow-up (not deploy-blocking): in the GitHub repo settings, flip Settings → General → Pull Requests → Collaborators only to actually enforce the contribution policy documented in
CONTRIBUTING.md. Verify Settings → Code security → Dependabot security updates is still enabled — we explicitly want CVE-driven PRs to keep flowing even with version-update PRs disabled.
...
Release 1.0.0-beta.25
Release Date: May 11, 2026
Previous Release: v1.0.0-beta.24 (May 6, 2026)
Highlights
This release is the production-readiness fix for the BFF Token Handler shipped in v1.0.0-beta.24. Beta.24 rewrote the SPA's auth surface onto cookie-based sessions but left three production-breaking bugs that only surfaced under real traffic: the SessionRefreshMiddleware ran synchronous boto3 on the uvicorn event loop so Angular's ~8-endpoint page-load fan-out produced ~16 serialized blocking AWS calls per user per minute (504s, 80s /files/quota tails, 15.6s p-max on a 0.7% CPU task); the CookieCodec minted a fresh random AES-256 key per process, so as soon as we raised desiredCount for concurrency slack every cookie started failing as bad seal on ~50% of requests; and the per-session refresh lock only coalesced in-process, so two tasks could still race cognito-idp:initiate_auth with the same refresh token and Cognito's rotation would silently log out the loser. This release lands the event-loop offload + single-flight resolve, a cross-task shared AES key via Secrets Manager, and a DDB conditional-write refresh lock that elects exactly one leader fleet-wide.
Also shipping: server-rendered PDF page-1 thumbnails on attachment cards, rich iMessage-style image mosaics with a full-screen lightbox and inline markdown preview for .md files in user messages, spreadsheet analysis tools (list_spreadsheets, analyze_spreadsheet) that run CSV/XLSX analysis inside the Code Interpreter sandbox, centralized 401 handling with proactive session-loss detection on tab refocus, and a SKIP_AUTH=true local-dev bypass gated by a CORS-origin allowlist and a CI guard workflow. Token accounting was corrected across the board — per-message cost no longer double-counts tool-use turns and the context-% badge reflects current context occupancy rather than Strands' summed-across-calls value.
Heads-up on beta.24
If you deployed beta.24 to a multi-replica environment, you saw some or all of: 401 storms on /auth/session, page-load latency tails in the tens of seconds, and users silently logged out after tab refocus. Beta.25 is the fix. The CookieCodec and refresh-lock changes require redeploying the Infrastructure and App API stacks in order — see 🚀 Deployment notes at the bottom.
BFF Middleware Event-Loop Blocking & Fan-Out Amplification
The middleware introduced in beta.24 ran three independent classes of work on the uvicorn event loop that weren't safe to run there: synchronous boto3 for DynamoDB + Cognito, an inline-awaited sliding-session write on the response path, and a refresh-coalescing lock that only wrapped the Cognito exchange instead of the full resolve path. Under Angular's ~8-endpoint page-load fan-out with a cold SessionCache window, a single cookie-bearing user produced ~16 serialized blocking AWS round-trips on one uvicorn worker running in a single ECS task — every slow call stalled every concurrent request on the same task. The observable symptoms were ALB 504s, TargetResponseTime p-max of 15.6s at 0.7% CPU, /files/quota outliers reaching ~80s, and endpoint p95s climbing into the hundreds of ms under trivial load. (#264)
How it works now
SessionRepository.{get,put,update_tokens,touch_last_seen,delete} and CognitoRefreshClient.refresh now offload every boto3 call via asyncio.to_thread, so the event loop keeps scheduling other coroutines for the full AWS round-trip duration. A new per-session single-flight primitive (apis/shared/sessions_bff/single_flight.py) wraps the whole cache.get → repository.get → needs_refresh → (maybe refresh) block in SessionRefreshMiddleware._resolve_session — the first caller per session_id runs the loader; N concurrent followers await a shared asyncio.Future and consume the leader's result. The existing get_session_lock(session_id) around the Cognito exchange is preserved end-to-end as defense in depth. _maybe_slide no longer awaits touch_last_seen inline — the DDB write dispatches as a detached asyncio.Task and the response returns the fresh Max-Age synchronously. The cache/throttle boundary alignment that forced a single request to pay both get_item and update_item on the cache-miss boundary has been de-aligned: _DEFAULT_SLIDING_RENEWAL_THROTTLE_SECONDS is now a strict multiple of _DEFAULT_REFRESH_LEEWAY_SECONDS (300s vs 60s).
Backend
apis/shared/sessions_bff/repository.py— every boto3 call now wrapped in a nested sync helper invoked viaawait asyncio.to_thread(helper, ...); method signatures, return types, and exception branches unchangedapis/shared/sessions_bff/refresh.py—refreshis nowasync def, callingawait asyncio.to_thread(self._refresh_sync, ...);CognitoRefreshErrorcontract andRefreshResultshape preserved verbatimapis/shared/sessions_bff/single_flight.py— new module.async def resolve_once(session_id, loader_coro_factory) -> tuple[Optional[SessionRecord], bool]. Leader registers anasyncio.Futureunder a thread-lock-guardeddict, runs the loader, sets the result/exception on the Future, removes the registry entry in afinallyblock. Followersawaitthe existing Future. Distinctsession_ids never share a Futureapis/shared/middleware/session_refresh.py—_resolve_sessionwraps the cache/repo/refresh block inresolve_once(session_id, _loader)._maybe_slideupdates the local cache synchronously and dispatchestouch_last_seenviaasyncio.create_task, keeping the task onself._slide_taskswith anadd_done_callback(self._slide_tasks.discard)— Python's asyncio docs explicitly warn that unreferenced tasks can be GC'd mid-flight, and our initial fix landed this footgun (caught by CI on Python 3.12)apis/shared/sessions_bff/config.py—_DEFAULT_SLIDING_RENEWAL_THROTTLE_SECONDSraised 60s → 300s. Strict multiple of the 60s leeway guarantees cache-miss and slide-throttle boundaries never coincide
Infrastructure
infrastructure/cdk.context.json—appApi.desiredCountraised 1 → 2 for concurrency slack. A single blocked event loop on one task can no longer halt all ingress
Test Coverage
~900 lines of new property-based tests. test_session_refresh_bug_condition.py encodes each of the seven sub-conditions as a hypothesis property that fails on unfixed code and passes on fixed code (Property 1 / Expected Behavior from the bugfix spec). test_session_refresh_preservation.py locks in the 11 preservation invariants that must stay unchanged for non-buggy inputs — dormant pass-through, no-cookie pass-through, unrecoverable-cookie clearing, Max-Age re-emit contract, refresh-storm coalescing, codec + client-secret singletons, CSRF decision unchanged, absolute-lifetime cap, fail-closed rotation, uniform CookieDecodeError handling. test_single_flight.py covers the primitive itself: concurrent callers share one loader invocation, exceptions propagate to every waiter, registry entries clean up after failure, distinct sessions are independent.
BFF Cross-Task Cookie & Refresh Correctness
The desiredCount: 1 → 2 bump in the event-loop fix immediately exposed two latent defects in beta.24's BFF design that were hidden when only one task existed. Both had to be fixed before the deployment was actually safe to run with more than one replica. (#273, #274, #275)
Shared AES-256 data key via Secrets Manager
CookieCodec in beta.24 called kms:GenerateDataKey on first use per process and cached the resulting plaintext AES-256 key in memory. The code's own docstring predicted what would happen with more than one task: "two codecs in one process can never decrypt each other's output." And that's what happened — Task A sealed a cookie with Key-A, the ALB routed the follow-up to Task B which had its own Key-B, unseal hit InvalidTag → CookieDecodeError → Discarding unrecoverable BFF cookie (bad seal) → 401. CloudWatch confirmed: three app-api streams each independently logged "BFF cookie codec initialized (KMS data key fetched)" and every subsequent /auth/session returned 401.
The fix moves the data key out of per-process state and into a single Secrets Manager secret, encrypted at rest by the existing BFFCookieSigningKey CMK:
- CDK creates
BFFCookieDataKeySecretwithgenerateSecretString(44-char alphanumeric, ~261 bits of entropy). On every deploy the secret already exists so the value is stable — cookies survive redeploys CookieCodec._ensure_cipherreads the secret string and applies SHA-256 to derive the 32-byte AES-256 key. Single-shot SHA-256 of a ≥256-bit-entropy random input is a sound KDF for AES-256 usage- Every app-api task decrypts the same secret and derives the same key → all codecs round-trip each other's seals. The
kms:GenerateDataKeypermission dropped from the runtime task role (least privilege);kms:Decryptstays because Secrets Manager invokes it on the caller's behalf when reading a CMK-encrypted secret
A previous attempt at this bootstrap (#273's initial chained AwsCustomResource flow with kms:GenerateDataKey → secretsmanager:PutSecretValue) failed stack create with Response object is too long. Root cause: the AwsCustomResource framework Lambda JSON-stringifies the AWS-SDK response before applying outputPaths, and KMS returns CiphertextBlob as a Uint8Array that serializes as {"0":233,"1":18,...} — ~1.5 KB for a 200-byte ciphertext, past CloudFormation's 4 KB response-object limit. The Secrets-Manager-native generateSecretString path in #274 removes the chained custom resources entirely (-153 lines net), no per-cold-start kms:Decrypt call, simpler runtime IAM surface.
Cross-task refresh lock via DDB conditional-write
The in-process single-flight and the existing get_session_lock only coalesce same-session callers within one Python process. Once the cookie-codec fix lands and both tasks can share cookies ...
Release 1.0.0-beta.24
Release Date: May 6, 2026
Previous Release: v1.0.0-beta.23 (April 29, 2026)
Highlights
This release lands the BFF Token Handler — a ground-up rewrite of the SPA's auth surface. localStorage Bearer tokens are replaced with server-side Cognito session storage keyed by an opaque session id in a KMS-sealed AES-GCM cookie, the public PKCE Cognito client is decommissioned in favor of a confidential client whose secret never leaves the server, and same-origin /api/* routing via CloudFront enables __Host- cookies, double-submit CSRF, and eliminates the CORS preflight from every chat turn. Voice mode returns via a WebSocket-ticket proxy on app-api. The chat view gains a per-conversation cost + context-window badge with write-time aggregation, and context compaction events now surface inline with refresh-survival. Anthropic extended thinking is wired end-to-end via per-model inference parameters. The backend finishes its architecture cleanup: cost, tools, storage, and API-keys modules now live under apis.shared with AST-enforced import boundaries.
BFF Token Handler — Cookie-Based Auth
The SPA's entire auth surface has been rewritten. Bearer tokens in localStorage are out; an opaque session id in a __Host-bff_session httpOnly cookie is in. The public PKCE Cognito client is decommissioned in favor of a confidential BFF client whose secret never leaves the server. Chat streams and voice WebSockets now transit same-origin /api/* through CloudFront, with app-api proxying to inference-api server-side. This closes the window where an XSS could exfiltrate a long-lived Cognito access token, removes the CORS preflight from every chat turn, and sets the foundation for the voice re-enablement below.
How authentication works now
A successful login goes: SPA → GET /auth/login → Cognito Hosted UI (with PKCE) → GET /auth/callback on app-api. The callback exchanges the code server-side using the confidential client secret, writes the Cognito access/refresh/ID tokens to BFFSessionsTable keyed by an opaque session id, and seals that id into an AES-GCM cookie whose data key is wrapped by KMS. The browser never sees a JWT. Subsequent requests carry only the cookie; SessionRefreshMiddleware unseals it, looks up the session row, silently refreshes the Cognito token when it's near expiry, and forwards the request. Unsafe methods require a double-submit CSRF header matching the __Host-bff_csrf cookie.
What shipped
Backend (apis/shared/sessions_bff/). CookieCodec (AES-GCM with version-byte associated data, promoted to a process-wide singleton so the /auth/callback seal and middleware unseal share the same KMS-derived key), BFFSessionRepository with conditional TTL writes, SessionRefreshMiddleware and CSRFMiddleware on app-api, per-session asyncio.Lock so multi-tab refresh storms drive exactly one Cognito exchange, and a Cognito refresh-token client that retries rotation writes three times before failing closed (an old refresh token dies the instant Cognito rotates it, so a silently-failed write would log users out on the next request).
BFF auth routes.
GET /auth/login— Cognito authorize with PKCE, optionalidentity_providerfor federated one-click SSO, optionalreturn_tofor deep-link preservation._sanitized_return_torejects all C0 control bytes (U+0000..U+001F), not just CR/LF, so browser URL-parser strip tricks like/\t/evil.comcan't pivot through the//check.GET /auth/callback— server-side code exchange, cookie seal, upsert of the Users row directly from ID-token claims (email,name,picture,custom:roles/cognito:groups); previously the per-request sync ran off the access token, which carries no email, so first-login users hademail=Noneand the Cognito provider-group string inrolesinstead of the IdP-mapped values.GET /auth/session— returns the session payload the SPA uses to bootstrap.POST /auth/logout— clears cookies, invalidates the DDB row, returns{post_logout_url}pointing at{cognito_domain}/logoutso the browser bounces through Cognito Hosted UI to clear the upstream session. Without this, Cognito silently re-issued a code on the next login without a credential prompt.
Sliding session lifetime. The cookie's Max-Age and the DDB row's TTL bump on every successful resolution, capped at created_at + BFF_SESSION_ABSOLUTE_LIFETIME_SECONDS (default 30 d) and throttled by BFF_SESSION_SLIDING_RENEWAL_THROTTLE_SECONDS. Without this, active users were getting logged out after 1 hour even though their refresh token was valid for 30 days.
Chat SSE proxy. POST /chat/stream on app-api is the cookie-authenticated proxy to {INFERENCE_API_URL}/invocations. It owns its httpx.AsyncClient lifecycle and closes it in the streaming generator's finally block — using async with would drain the upstream during __aexit__ and buffer the entire stream before headers flush. Forwards the SPA's OAuth2CallbackUrl header so AgentCoreContextMiddleware can scope tool-side OAuth consent landing URLs to the SPA origin. The AgentCore Runtime data-plane URL is built by _build_upstream_url(), which percent-encodes the ARN as a single path segment and appends ?qualifier=DEFAULT — without this the ARN's literal / split the path and AWS returned 404. Sets X-Accel-Buffering: no and Cache-Control: no-cache so late SSE events (notably oauth_required after message_stop) reach the browser. The same lifecycle fix was mirrored onto the API-key-authenticated /chat/api-converse proxy.
Frontend (SessionService). Bootstraps from GET {appApiUrl}/auth/session in a chained APP_INITIALIZER (migrated to Angular 19+ provideAppInitializer). On 401, navigates to the SPA's /auth/login page with returnUrl — not Cognito Hosted UI directly — so the user can pick a provider. The bootstrap promise hangs on the 401 path so APP_INITIALIZER stays pending until the browser tears the page down (previously the router could render / in the brief window before navigation landed). A new csrfInterceptor mirrors the CSRF header onto unsafe-method requests; a new withCredentialsInterceptor flips withCredentials: true on every HttpClient call to appApiUrl (local dev runs cross-origin; production is same-origin via CloudFront so the flag is a no-op, but without it cross-origin dev 401'd on every call after login). ChatHttpService and PreviewChatService target ${appApiUrl}/chat/stream with credentials: 'include' instead of hitting inference-api directly.
Legacy AuthService retired. auth.service.ts, auth.interceptor.ts, the SPA's /auth/callback page + callback.service.ts, and their specs are deleted. UserService.currentUser is derived from SessionService.user(). authGuard and adminGuard gate on SessionService.isAuthenticated(). The SPA /auth/callback route is gone — the BFF callback at ${appApiUrl}/auth/callback is the only OAuth landing.
Infrastructure. BFFSessionsTable (DynamoDB, TTL attribute), BFFCookieSigningKey (KMS), CognitoBFFAppClient (confidential, secret in Secrets Manager). CloudFront /api/* behavior on the frontend distribution forwards to the app-api ALB with a viewer-request Function that strips the /api prefix. Caching disabled, all-viewer-except-host-header policy, no compression (SSE must not be re-gzipped), readTimeout capped at CloudFront's 60 s default max. SPA fallback moved off distribution-wide errorResponses (which was rewriting /api/* 4xx into 200 + index.html, choking HttpClient JSON parsing) onto a viewer-request Function scoped to the S3 behavior. CognitoConfig.supportedIdentityProviders (env CDK_COGNITO_SUPPORTED_IDPS) wires federated IdPs onto the BFF client; previously only the now-deleted public client had them.
Public PKCE client decommissioned. The SPA-public appClient is gone, along with SSM parameters /auth/cognito/app-client-id and /oauth/callback-url. InferenceApiStack's runtime authorizer repoints to /auth/cognito/bff-app-client-id. AppApiStack's COGNITO_APP_CLIENT_ID also repoints to the BFF client, which keeps /chat/agent-stream Bearer validation alive for API-key and scripted callers.
/config.json retired. appApiUrl is baked into the bundle via Angular fileReplacements (dev → http://localhost:8000, prod → /api). version is generated from the monorepo root VERSION file by a scripts/gen-version.js prebuild hook. cognitoDomainUrl is fetched on demand from a new GET /admin/auth-providers/cognito-redirect-uri admin endpoint. ConfigService collapses to a thin signal accessor over environment.appApiUrl; APP_INITIALIZER drops the chained loadConfig step.
Breaking changes
Authorization: Beareris no longer accepted on SPA-facing routes. Cookie auth is required. External callers must migrate to the BFF session flow or hit/chat/agent-stream(Bearer-only) instead.POST /chat/streamis now the cookie-authenticated proxy. The legacy in-process agent loop moved toPOST /chat/agent-streamfor API-key and scripted callers.- SPA
/auth/callbackroute removed. Third-party tools that deep-linked there must use${appApiUrl}/auth/callback. - SSM parameters deleted:
/auth/cognito/app-client-idand/oauth/callback-url. Consumers must migrate to/auth/cognito/bff-app-client-idand register a per-system callback URL.
Voice Mode via WebSocket-Ticket Proxy
Voice returns on top of the new cookie flow. The SPA no longer holds a Cognito access token, so it can't authenticate the WebSocket upgrade against the AgentCore Runtime's customJwtAuthorizer directly. Instead the SPA mints a single-use HMAC ticket, opens a same-origin WS to /api/voice/stream, and app-api opens the upstream WS using the BFF-stored Cognito token (#211, #233).
How it works
POST /voice/ticket(cookie + CSRF au...
Release 1.0.0-beta.23
Release Date: April 29, 2026
Previous Release: v1.0.0-beta.22 (April 8, 2026)
Highlights
This release introduces WebSocket voice streaming with Nova Sonic bidirectional audio, a multi-agent architecture with pluggable agent types (Chat, Skill, Voice), external MCP connectors via AgentCore Identity replacing the bespoke OAuth token vault, per-tool approval gates for dangerous operations, and a full Playwright E2E testing suite. The agent layer has been refactored into a BaseAgent → ChatAgent hierarchy with a factory registry, enabling runtime agent-type selection. The legacy in-house OAuth flow (token vault, PKCE service, encryption layer) has been retired in favor of AgentCore Identity's managed credential providers. 252 files changed across 23,000+ lines of new code.
Voice Mode — Bidirectional Audio Streaming
Full-stack voice interaction using Amazon Nova Sonic 2 via the Strands BidiAgent. Users can speak to the agent and receive spoken responses in real time, with voice-text continuity that carries context from prior text conversations into voice sessions.
Backend
VoiceAgent(BaseAgent)wrapsBidiAgentwithBidiNovaSonicModelfor configurable voice, sample rate, and model selection- Voice-text continuity via
_load_text_history()— loads the text session's message history so the voice agent has full conversational context - Separate
agent_id("voice") prevents session state conflicts between text and voice turns - Voice-optimized system prompt with conversational guidelines
- WebSocket endpoint at
/voice/stream(inference API) with JWT auth from query params - Bidirectional protocol: audio/text input from client, agent event streaming back
- Accept-first WebSocket pattern aligned with the
sample-strands-agent-with-agentcorereference architecture — AgentCore validates auth at the proxy layer - Config message supplements missing params in cloud mode;
/voice/streamfor local dev,/wsalias for AgentCore Runtime - Debug endpoints:
GET /voice/sessions,DELETE /voice/sessions/{id} CancelledErrorhandling inVoiceAgent.stop()for clean teardown of Nova Sonic streams- Real-time cost calculation and token usage metadata for voice turns
- Log injection prevention via
_sanitize_log()for all user-provided values in voice routes
Frontend
Three-layer voice architecture in session/services/voice/:
pcm-utils.ts: Pure PCM encoding/decoding (Float32 ↔ Int16 ↔ base64)AudioRecorderService: Mic capture via Web Audio API → 16kHz PCM chunks using an AudioWorklet (pcm-capture.worklet.js)AudioPlayerService: Gapless base64 PCM playback with interruption supportVoiceChatService: WebSocket orchestration + state machine (idle → connecting → listening → speaking)VoiceOverlayComponent: Full-screen voice UI with visualizer orb and status badges- Chat input gains a voice toggle button with animated state indicators (pulsing red = listening, bouncing green = speaking, spinner = connecting)
- Live transcript overlay during voice mode
MessageMapService.addVoiceMessage()persists finalized voice transcripts to the message list
Infrastructure
strands-agents[bidi]optional dependency group added topyproject.toml- Inference API Dockerfile updated with
bididependency inuv synccommands InferenceApiStackgains HTTP protocol configuration for WebSocket support- Voice router registered in inference API
main.py
Test Coverage
16 new VoiceAgent unit tests, 14 voice route tests covering WebSocket auth, bidirectional streaming, and teardown.
Multi-Agent Architecture
The monolithic MainAgent has been decomposed into a pluggable agent hierarchy with a factory registry, enabling runtime selection of agent behavior without redeployment.
Agent Hierarchy
BaseAgent(ABC): Shared initialization for model config, tools, session management, streaming, and approval hooksChatAgent(BaseAgent): Strands Agent creation and text streaming — the standard conversational agentMainAgent(ChatAgent): Backward-compatible alias so all existing callers work unchangedSkillAgent(ChatAgent): Progressive skill disclosure (see below)VoiceAgent(BaseAgent): Bidirectional audio via BidiAgent (see Voice Mode above)
Agent Type Registry
agent_types.py provides a pluggable registry pattern:
create_agent(agent_type, **kwargs)→BaseAgentsubclassregister_agent_type(name, cls)for dynamic registrationChatAgentregistered as"chat",SkillAgentas"skill",VoiceAgentas"voice"(conditional onstrands-agents[bidi])
Factory Routing
The inference API now routes chat turns through create_agent(agent_type, ...) instead of hard-coding MainAgent. InvocationRequest gains an optional agent_type field, folded into the LRU cache key so chat/skill agents for the same session don't collide. PausedTurnSnapshot persists the resolved agent type so OAuth-paused turns rebuild on the correct factory variant after cache eviction.
Configuration Centralization
All environment variables and magic strings consolidated into agents/main_agent/config/constants.py with EnvVars, Defaults, and Prefixes classes. 13 modules updated to import from the centralized constants instead of inline os.getenv() with hardcoded strings.
Test Coverage
9 factory tests, 38 skill tests, 16 voice tests, plus existing 543 tests passing with zero behavior change.
Progressive Skill Disclosure
A three-level skill architecture adapted from the sample-strands-agent reference, allowing the agent to discover and load tool capabilities on demand rather than loading everything upfront.
How It Works
- Level 1: Lightweight skill catalog injected into the system prompt — the agent sees what skills exist without loading their full instructions
- Level 2:
skill_dispatcherloads the fullSKILL.mdinstructions on demand when the agent decides to use a skill - Level 3:
skill_executorruns the actual tool functions bound to the skill
New Modules
skills/skill_registry.py: DiscoversSKILL.mdfiles, binds tools, serves the catalogskills/skill_tools.py:skill_dispatcher+skill_executoras Strands@toolfunctionsskills/decorators.py:@skill()decorator andregister_skill()for tool taggingskill_agent.py:SkillAgent(ChatAgent)with progressive disclosure override
Skill Definitions
web-search/SKILL.md: Example skill definition for web search toolscanvas-morning-check/SKILL.md: Educator-facing morning course health check that surfaces submission rates, struggling students, and upcoming deadlines via the Canvas MCP server, with FERPA-aware anonymization guidance
External MCP Connectors via AgentCore Identity
The bespoke OAuth token vault (per-user DynamoDB encryption, KMS, Secrets Manager client credentials, manual refresh) has been replaced with AgentCore Identity's managed token vault and credential providers. This is a full-stack rewrite of how external MCP tools authenticate with third-party services.
AgentCore Identity Integration
AgentCoreContextMiddlewarecopies Runtime headers (WorkloadAccessToken,OAuth2CallbackUrl, session ID, request ID) intoBedrockAgentCoreContexton every invocation — required because the Inference API is a plain FastAPI app, not aBedrockAgentCoreAppAgentCoreIdentityClientwrapsIdentityClient.get_token()with a narrower surface forUSER_FEDERATION(3LO) flows, surfacing "user consent required" as a structuredTokenResult(authorization_url=...)rather than an exceptionAgentCoreCredentialProviderRegistrarwrapsbedrock-agentcore-controlfor admin-side OAuth2 credential provider CRUD with vendor mapping (Google/Microsoft/GitHub to native vendors; Canvas/Custom via OIDC discovery URL)
OAuth Consent Flow
When an external MCP tool needs OAuth consent, the authorization URL flows through the SSE stream as an oauth_required event:
OAuthConsentServiceorchestrates popup opening +postMessagereceiptOAuthConsentBannerrenders a "Connect" button inline in the chat/oauth-completelanding page handles the AgentCore callback redirect and signals consent completion to the opener tabPendingInterruptgains anoauth_consentvariant so the consent prompt rehydrates after a page refresh
Legacy OAuth Retirement
Deleted: OAuthService, OAuthTokenRepository, token_cache.py, encryption layer, user-facing /oauth/* routes, OAuthToolService, settings/connections page, settings/oauth-callback page. The admin UI has been rebranded from "OAuth Providers" to "Connectors" (admin/connectors/), with the form rewritten for the AgentCore-owned shape — credential rotation requires clientId + clientSecret together (AgentCore's update API is not partial), and the success screen displays the AgentCore callback URL with a copy button.
Shared Workload Identity
A CfnWorkloadIdentity (<projectPrefix>-platform-workload) is provisioned in InfrastructureStack and shared between inference-api and app-api. Both services mint user-scoped workload tokens against it via GetWorkloadAccessTokenForUserId, ensuring the OAuth token vault is keyed consistently — a user consents once and both code paths find the token. The runtime's auto-created identity stays in place but is no longer used for vault calls.
Infrastructure
InfrastructureStack: NewCfnWorkloadIdentity+ SSM exportsAppApiStack: IAM grants for Secrets Manager lifecycle (create/update/delete/get) onbedrock-agentcore-identity!default/oauth2/*, plusbedrock-agentcore:GetResourceOauth2TokenInferenceApiStack: Runtime workload identity lookup viaAwsCustomResource(SDKGetAgentRuntimecall) replacing the brokenFn::GetAtton nested attribute paths; IAM grants for OAuth secret read- CloudFront added to API CORS origins