diff --git a/CHANGELOG.md b/CHANGELOG.md index cab8e0de..6f7fc356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,87 @@ All notable changes to this project are documented in this file. Format follows For narrative release notes written for operators and product owners, see [RELEASE_NOTES.md](RELEASE_NOTES.md). +## [1.18.0] - 2026-09-06 + +Artifacts stop being a per-conversation curiosity and become a place users go. There is a library at `/artifacts` with previews, rename, delete and an in-app viewer; artifacts can be **shared** with named people or the whole tenant; and sharing a conversation now shares the artifacts in it, which previously left the recipient staring at nothing where the owner saw cards. Alongside it, two more surfaces the platform had no way to do at all: **feature announcements** โ€” an admin-authored What's New feed with a banner, a modal and per-announcement reach stats โ€” and **mid-turn steering**, which lets a follow-up typed while the model is still working land inside the running turn at the next tool boundary instead of interrupting it. The SPA gains a **single-file rebranding surface** so a fork can change app name, greeting, logo and the entire color system without touching a component. On the cost side the GPT-5.6 family (Sol / Terra / Luna) is curated with verified rates, and every published GPT-5.6 rate in the catalog is corrected. **Requires a CDK deploy** (new `{prefix}-announcements` table, new IAM grants); no GSI operations on any existing table. + +### ๐Ÿš€ Added + +- **Artifact sharing** โ€” owners can share an artifact with specific email addresses or with any authenticated tenant user, and revoke at any time. Share records live on the existing `{prefix}-user-artifacts` table under a `SHARE#` prefix (two rows in one `TransactWriteItems`); no new table and no GSI. Recipients open a minimal-chrome `/shared-artifact/{shareId}` view that renders the pinned version and never touches an owner endpoint (#919, #920, #922, #927, #928) +- **"Shared with you" inbox** โ€” `GET /shared-artifacts` lists artifacts shared with the caller, backed by fan-out pointer rows in the recipient's own partition (`PK=SHARED_WITH#{email}`). **Gated by `ARTIFACT_SHARE_INBOX_ENABLED`, default off.** The fan-out rows are written regardless of the flag, so turning it on shows a complete inbox with no backfill (#968) +- **All / Yours / Shared with you tabs** on the artifact library. The SPA discovers the inbox by calling it โ€” a 404 means "no inbox in this environment" and it renders the tab-less library it always did, so no separate frontend flag exists (#970) +- **Artifact library page** at `/artifacts` โ€” every artifact a user owns, list or grid, backed by `GET /artifacts/library`. No new index was needed: `user-artifacts` is already partitioned by user (#940, #950) +- **Grid-card previews** โ€” library grid cards render a live, scaled-down iframe of the artifact through the deployed render path at a fixed 1024px virtual viewport. Previews mount lazily on intersection and are never re-minted, because each one costs a mint plus a render-Lambda invocation (#967) +- **Rename and delete** for artifacts, from the library, the docked panel and the inline card. `PATCH /artifacts/{id}` and `DELETE /artifacts/{id}` (#952, #957) +- **In-app artifact viewer** โ€” artifacts open in a docked panel instead of depending on a pop-up window (#958) +- **Shared-conversation artifacts** โ€” sharing a conversation now shares the artifacts in it. `create_share` pins the session's artifacts at their current versions into the S3 snapshot body, which makes the snapshot both the point-in-time record and the allowlist; no artifact share records are created (#971, #973) +- **Session-delete cascade** โ€” deleting a conversation revokes the artifact shares created from it, on both the single and bulk delete routes (#931) +- **Feature announcements** โ€” admin-authored release notices with a full lifecycle (draft โ†’ publish โ†’ revise โ†’ archive), targeting by role, and per-user acknowledgements. New `{prefix}-announcements` table (no GSIs), `GET /announcements` + ack endpoint for users, and `/admin/announcements` CRUD (#948, #966, #969, #972) +- **What's New surfaces** โ€” a panel, a floating banner beside the chat composer, and a modal for high-priority announcements gated by the spec's ยงD8 rules (#976, #977, #979, #981) +- **Announcement reach stats** โ€” `GET /admin/announcements/{id}/stats` and a reach column on the admin list, driven by ack funnel counters (#978) +- **Mid-turn steering** โ€” a follow-up typed while a turn is still streaming is injected into that running turn at the next tool boundary, appended to the same user-role message that carries the tool results, so the agent reads it before choosing its next action. New `steering_applied` SSE event and `POST /sessions/{id}/steer`; transport is the session's existing single-flight lease row. Gated by `MID_TURN_STEERING_ENABLED` (default on) (#916, #921) +- **Single-file rebranding** โ€” `frontend/ai.client/src/branding/brand.config.ts` is now the only file to edit to change app name, page title, greeting text, logo paths and the brand color system. Prestart/prebuild generators derive the full theme (brand tokens, an OKLCH-banded neutral surface ramp, and favicons) from it, with golden-file and parity specs pinning the output (#933) +- **GPT-5.6 Sol, Terra and Luna** curated in the model catalog on the `bedrock-runtime` OpenAI-compatible endpoint (#980) +- **`bedrock-runtime` OpenAI Responses transport** (`provider="bedrock-responses"`) with the `bedrock:CallWithBearerToken` grant it needs (#949, #959) +- **Multi-modal fine-tuning** โ€” a task-type registry replaces the text-only assumption, adding image and image+text tasks in the API and the SPA, with a dollar-denominated quota (#944) +- Response-feedback spec (`docs/specs/`), a prompt-caching convergence watch, and the mid-turn steering spec (#942, #961, #921) + +### โœจ Improved + +- **Prompt-cache TTL is derived from the serving model** rather than assumed, so `cacheStatus` no longer misreads a hit as expired on models with a different TTL (#951) +- **Mantle models expose the caching controls** they were previously denied in the admin catalog (#963) +- **`supportsCaching` is forced on for providers that cache unconditionally**, so a model whose provider always caches is no longer reported as uncached (#960) +- **OpenAI-family token usage normalizes to disjoint buckets**, ending the double-count where cached tokens were included in the input total (#945) +- The library view toggle no longer stretches on narrow screens, and the grid card footer no longer overflows its card (#955) + +### โš ๏ธ Changed + +- **Explicit GPT-5.6 prompt-cache breakpoints ship OFF.** They were built, measured, and found **57% more expensive** than the provider's automatic caching, so the code stays and the default is off (#954, #956) +- An omitted `supported_param` is now treated as **unsupported**, not as pass-through โ€” an empty `supportedParams` previously bypassed the parameter guard entirely (#915) + +### ๐Ÿ› Fixed + +- **Artifact share cascade used `BatchWriteItem`, which the app-api task role cannot call** โ€” it failed closed in dev, leaving share links live after their conversation was deleted. `TransactWriteItems` authorizes against the underlying item actions; `BatchWriteItem` is its own IAM action. Replaced with per-row `DeleteItem` (#932) +- **An empty "Shared with you" tab said "No artifacts match your search"** with an empty search box, because the filtered-empty state gated on the library total rather than the tab's (#975) +- **"Pop-up blocked" was reported on every artifact open**, including successful ones (#953) +- A mid-turn steer rendered once per sync tick instead of once (#930), a follow-up typed while a turn was paused was dropped instead of queued (#934), and a steer bubble used a non-standard color (#935) +- A user bubble's overflow was measured once and latched; it is now re-measured (#937) +- A duplicate error toast fired alongside the shared-artifact page's own inline 404, and the artifact card's actions overlapped its title when the panel was docked โ€” fixed with a container query, since the card is sized by the chat column and not the viewport (#927) +- The new-announcement form's submit button could never enable (#974) +- **GPT-5.6 rates were wrong three ways**: derived from a 1000x-wrong multi-model blend, then published in the model cards all along. Every rate in the catalog is corrected (#980, and the derivation method in the same PR) +- The cache-write premium and the Global/Regional rate tier were both wrong in cost derivation (#914) +- Knowledge-base retrievability is confirmed with a filtered query and `TEXT_INDEXED` is classified correctly (#908) +- Generative VLMs are excluded from the dual-encoder fine-tuning task, and instance types are validated (#944) + +### ๐Ÿ”’ Security + +- **All 47 open Dependabot alerts cleared** across backend, frontend, infrastructure, docs-site and the backup/restore scripts (#924) +- **The custom HuggingFace model id is validated against an anchored repo-id pattern** before it is interpolated into a Hub request path or forwarded to the training container as `model_name_or_path`. The call site's comment had claimed this validation since before the release; only non-empty and length were actually checked. The host was always hard-coded, so this was never an arbitrary-host SSRF โ€” but a value carrying dot-segments, extra slashes, a query or a fragment could change the meaning of both sinks +- **CodeQL alerts remediated: 11 high, 20 medium, 9 note** โ€” log injection, unused imports and related findings across 18 backend modules and one SPA page. The nightly workflow is extended in the same pass (#925) + +### ๐Ÿ—๏ธ Infrastructure + +- **New `{prefix}-announcements` table** โ€” one table, two item shapes (announcement rows under a fixed `ANNOUNCEMENTS` partition, per-user ack rows under `USER#`). **No GSIs.** Table name published to SSM at `/{prefix}/admin/announcements-table-name` (#966) +- **`CDK_ARTIFACT_SHARE_INBOX_ENABLED`** โ€” new deploy variable, default off, threaded to the app-api container as `ARTIFACT_SHARE_INBOX_ENABLED`. Gates the inbox read only (#968) +- **`bedrock:CallWithBearerToken`** granted to the inference-api role. The `bedrock-runtime` OpenAI-compatible endpoint authenticates under the `bedrock` service namespace, not `bedrock-mantle` โ€” granting only the Mantle action returns a 401 (#959) +- `infrastructure/gsi-inventory.json` gains `announcements` with an empty index list. **No index operations on any existing table.** + +### ๐Ÿ“ฆ Dependencies + +- Backend: `cryptography` 48.0.1 โ†’ 50.0.1, `aiohttp` 3.14.1 โ†’ 3.14.3, `pandas` 2.3.3 added (fine-tuning dataset contract) +- Frontend: Angular 21.2.17 โ†’ 21.2.19, `mermaid` 11.15.0 โ†’ 11.16.1, `postcss` 8.5.12 โ†’ 8.5.28, `sharp` 0.33.0 and `tsx` 4.23.12 added (branding generators), `dompurify` โ‰ฅ3.4.13, `undici` โ‰ฅ7.29.0, `hono` โ‰ฅ4.12.34 +- Infrastructure: `aws-cdk-lib` 2.262.0 โ†’ 2.265.0, `brace-expansion` โ‰ฅ5.0.9 + +### ๐Ÿ”ง CI/CD + +- The SPA `prestart` and `prebuild` scripts now run the four branding generators (brand theme, surface theme, surface colors, favicons) before the app builds (#933) +- Nightly workflow extended alongside the CodeQL remediation (#925) + +### ๐Ÿ“š Docs + +- GPT-5.6 live verification, model-family findings, the prod gpt-5.4 cache-rate closure, and the corrected `global.*` SCP finding (dev only โ€” prod is unaffected) (#962, #964, #965) +- Kaizen research and review-prep for 2026-09-04, and a prompt-caching convergence watch (#929, #961) + ## [1.17.0] - 2026-09-02 Reliability, security and observability. Every CloudWatch alarm in the stack now notifies somebody โ€” before this the stack had 13 alarms and **none of them were routed**, two of which watched metric names that exist in no namespace and had read as healthy since the day they were created. A production outage post-mortem (session `5f34d2b0`) drives four chat-path changes: Bedrock's transient faults are retried, a retry and a long silence are both visible to the user, and attachments a failed turn never delivered are re-sent. Four security findings are closed, including a High-severity OIDC login CSRF in the BFF auth flow and a privilege-escalating stored XSS in skill resources. The Bedrock Managed Knowledge Base migration โ€” still off by default โ€” gets eleven defects fixed from its first real runs in dev. **Requires a CDK deploy**, and one manual step after it: subscribe your team to the new alarm topic (see [step-05-verify](.github/docs/deploy/step-05-verify.md#6-subscribe-to-platform-alarms-required--not-automated)). diff --git a/README.md b/README.md index 3abc44da..79c393cf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **An open-source, production-ready Generative AI platform for institutions** *Built by Boise State University, designed for everyone.* -[![Release](https://img.shields.io/badge/Release-v1.17.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) +[![Release](https://img.shields.io/badge/Release-v1.18.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) [![Nightly](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml/badge.svg)](https://github.com/Boise-State-Development/agentcore-public-stack/actions/workflows/nightly.yml) ![Python](https://img.shields.io/badge/Python-3.13+-3776AB?style=flat&logo=python&logoColor=white) @@ -296,7 +296,7 @@ agentcore-public-stack/ See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full changelog, including new features, bug fixes, platform upgrades, and deployment notes for each release. -**Current release:** v1.17.0 +**Current release:** v1.18.0 --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4ba4d936..43898427 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,250 @@ +# Release Notes โ€” v1.18.0 + +**Release Date:** September 6, 2026 +**Previous Release:** v1.17.0 (September 2, 2026) + +--- + +> ๐Ÿ—๏ธ **CDK deploy required.** One new DynamoDB table (`{prefix}-announcements`, **no GSIs**), new IAM grants for it, and `bedrock:CallWithBearerToken` on the inference-api role. `infrastructure/gsi-inventory.json` gains one entry with an empty index list โ€” **no index operations on any existing table**, so this release is not subject to the one-GSI-per-`UpdateTable` split. +> +> โš™๏ธ **One new deploy variable: `CDK_ARTIFACT_SHARE_INBOX_ENABLED`.** It gates the artifact library's "Shared with you" tab and nothing else. Default off. Set it to `"true"` on an environment *before* the deploy to have the tab appear there; the pointer rows behind it are written regardless, so flipping it later is complete and instant with no backfill. +> +> โšก **Two things change on deploy with no flag:** an omitted `supported_param` is now treated as unsupported rather than passed through, and the prompt-cache TTL is derived from the serving model instead of assumed. Both are corrections, but both change behavior on the first turn after the deploy. + +--- + +## Highlights + +A release about the things users *keep*, and about telling them what changed. + +**Artifacts became a place, not a side effect.** Before this release an artifact existed inside the conversation that made it and nowhere else. Now there is a library at `/artifacts` with live previews, rename and delete; artifacts can be **shared** with named recipients or the whole tenant and revoked at any time; and sharing a conversation shares the artifacts in it โ€” which until now silently showed the recipient nothing where the owner saw cards. Eleven PRs, all verified live on dev, and **zero new tables and zero new indexes**: the `user-artifacts` table was already partitioned by user, and share records ride the same partition under a `SHARE#` prefix. + +**The platform can now tell users what shipped.** Feature announcements are admin-authored, role-targeted notices with a real lifecycle (draft โ†’ publish โ†’ revise โ†’ archive), per-user acknowledgements, a What's New panel, a floating banner beside the chat composer, a modal for the high-priority ones, and reach stats on the admin list so an author can see whether anyone actually read it. + +**A follow-up typed mid-turn no longer has to interrupt the turn.** Type while the model is working and the text is injected into the *running* turn at the next tool boundary, appended to the same user-role message that carries the tool results โ€” so the agent reads it before choosing its next action, rather than after the turn it should have influenced has already finished. It is append-only against the cached prefix, so it costs nothing in prompt-cache terms. + +**The SPA is now rebrandable from one file.** `brand.config.ts` holds app name, page title, greeting text, logo paths and the brand colors; generators derive the entire theme โ€” brand tokens, an OKLCH-banded neutral surface ramp, and favicons โ€” at prestart and prebuild. This matters because this repo is forked: previously a fork had to hunt colors through components. + +**Cost correctness took another pass.** The GPT-5.6 family (Sol, Terra, Luna) is curated and every one of its rates is corrected โ€” they had been derived from a 1000x-wrong multi-model blend when the model cards published them all along. Explicit prompt-cache breakpoints for GPT-5.6 were built, measured at **57% more expensive** than the provider's automatic caching, and shipped **off**. + +**Every open Dependabot alert (47) and 40 CodeQL findings are closed.** + +--- + +## Artifacts: a library, and sharing + +The artifact feature shipped originally as a rendering surface attached to a conversation. This release makes it a user-level asset with an owner, a lifetime independent of the chat that produced it, and an access-control story. + +### The library + +`GET /artifacts/library` and a page at `/artifacts`, reachable from **Settings โ†’ Profile** next to My Files. + +**No new index was needed, and that was the finding that shaped the design.** `user-artifacts` is keyed `PK=USER#{uid}` / `SK=ARTIFACT#{aid}#HEAD|#V#{n}`, so a user's entire library is already one partition โ€” ownership rides the partition key. Production was measured before deciding: 1,494 artifacts across 340 users, 4,129 rows, 2.3MB; the heaviest user holds 64 artifacts in ~110KB, about 14 RCU. The content mix (`text/markdown` 2,769 vs `text/html` 1,352) is why the library defaults to **list** while the Agents surface defaults to grid โ€” most artifacts are documents, not visual pages. + +`GSI2PK`/`GSI2SK` are stamped on HEAD rows by a writer with **no index consuming them yet** (#940). A sparse GSI only ever contains rows that already carry its key attributes, so rows written before the attributes exist stay invisible to it forever โ€” silently, as a library listing only post-deploy artifacts with no error anywhere. Stamping early shrinks the eventual backfill to the pre-#940 rows, and costs nothing until the index exists. + +- **Grid previews** (#967) render the artifact itself โ€” the deployed render path in an iframe at `scale(cardWidth/1024)`, at a fixed 1024px virtual viewport so the miniature is a true reduction rather than a mobile reflow. Not a server-side screenshot, which would mean executing user HTML server-side in our own account. **Cost is the design constraint:** every mounted preview is a mint plus a render-Lambda invocation plus an S3 GetObject on a `CACHING_DISABLED` path, so previews mount lazily on intersection and a mounted one is never unmounted or re-minted. List stays the default deliberately โ€” flipping it would put every user into the expensive view. +- **Rename and delete** (#952, #957) via `PATCH` / `DELETE /artifacts/{id}` โ€” hard-delete in DynamoDB, tag in S3, no IAM change. Available from the library, the docked panel and the inline message card. +- **In-app viewer** (#958) โ€” artifacts open in a docked panel rather than depending on a pop-up window. Render tokens are ~120-second bearer credentials, so a grid cannot pre-mint; the previous flow minted per click and any await before `window.open` cost the active gesture and got the pop-up blocked. + +### Sharing + +`POST /artifacts/{id}/shares` and friends, with two share types: `specific` (an email allowlist) and `public` (any authenticated tenant user). Recipients land on `/shared-artifact/{shareId}` โ€” a minimal-chrome shell with no sidenav, since a recipient has no other business in the app. + +**Zero infrastructure.** A share is two rows on the existing artifacts table written in one `TransactWriteItems`: an owner row and a `SHARE#{id}`/`META` lookup row. No GSI, deliberately โ€” the table's index budget stays free for the `UserArtifactsIndex` the writer is already stamping keys for. `TransactWriteItems` needs no IAM action of its own; it authorizes against the underlying Put/Delete. + +> โš ๏ธ **The mint sets the token's `sub` to the *owner's* user id, not the viewer's โ€” and that is correct.** `sub` is the DynamoDB partition key the render Lambda builds (`PK = USER#{sub}`): an *address*, not an identity assertion. Setting it to the viewer points at the viewer's own partition and 404s. Viewer identity travels in the `vwr` claim and the grant in `shr`; the ACL check in app-api is what stands between sharing and reading any artifact by id. This is written down because it looks like a bug and "fixing" it would break the feature. + +The render Lambda was deliberately not modified โ€” `_verify_token` has no extras rejection, so `vwr`/`shr` are forward-compatible with what is already deployed. A test imports `_verify_token` directly to keep that a verified fact rather than a reading, and it was confirmed against the live Lambda in dev. + +**Session-delete cascade** (#931, #932): deleting a conversation revokes the artifact shares created from it, on both the single and bulk delete routes. The lookup row is deleted **before** the owner row โ€” the lookup row is the capability, so a half-finished cascade leaves an inert orphan rather than a live share the owner can no longer see to revoke. The test asserts the order, because both orders look identical when nothing fails. + +### The recipient inbox โ€” the one flagged surface + +`GET /shared-artifacts` lists what has been shared *with* you, and the library grows **All / Yours / Shared with you** tabs (#968, #970). + +**A GSI cannot index a list attribute.** `allowed_emails` is a list, so no index can turn one share row into N recipient entries; any recipient lookup needs a row per (share, recipient) regardless, and the only real question is which partition it lives in. So: fan-out pointer rows at `PK=SHARED_WITH#{email_lower}` / `SK=SHARE#{createdAt}#{shareId}`, in the recipient's own partition. Still no GSI. + +Three properties hold it together. The row is a **pointer** โ€” it carries no title, because copying one per recipient multiplies staleness by allowlist size. The read **never trusts it** โ€” it resolves through the share lookup row and re-runs the access check, so a stranded pointer lists nothing and grants nothing. And fan-out is **discovery, never authorization**, which is what allows it to be written best-effort per item *outside* the two-row transaction โ€” avoiding an allowlist cap of roughly 40 addresses that `TransactWriteItems`' 100-item limit would otherwise invent. + +> โš ๏ธ **`ARTIFACT_SHARE_INBOX_ENABLED` gates the READ only. The fan-out rows are written unconditionally.** That asymmetry is the point: if the writes were gated too, enabling the flag would reveal an inbox missing every share created while it was off โ€” a wrong answer rather than an empty one, and one nobody can see is wrong. Do not "optimise" the write path by wrapping it in the flag. + +The SPA needs no flag of its own. It requests the inbox in parallel with the owned list; a 404 means the surface does not exist in this environment and it renders the tab-less library it always did. An inbox that 503s is allowed to fail on its own without blanking the artifacts the user owns. + +### Sharing a conversation shares its artifacts + +Closing ยง8 of the spec (#971, #973). A recipient of a shared conversation used to see **nothing** where the owner sees artifact cards, with no error and no explanation. + +**The conversation share *is* the grant** โ€” no artifact share records are created. `create_share` pins the session's artifacts at their current versions into the S3 snapshot body, which simultaneously preserves point-in-time semantics and makes the snapshot the allowlist; `resolve_shared_artifact` checks the conversation ACL *and* snapshot membership before minting. Parallel artifact shares were rejected because each would need cascading on update, revoke and delete, and one missed cascade leaves an artifact readable after its conversation was locked down. + +The snapshot's `artifacts` key is **optional on read** โ€” conversation sharing is already in production, so pre-existing bodies read as `[]`. No migration. + +The recipient UI is its own card and dialog rather than a mode of the owner's: the owner components carry download, share, rename, delete, version picker and code view, all keyed on endpoints a conversation-share recipient has no handle for. The shared layer is `ArtifactViewerComponent`, which absorbed a third mint path with **no change** โ€” the sign the split was drawn in the right place. + +### Test coverage + +Over 6,700 lines of new tests across the share service, the cascade, the inbox fan-out, the library page and the recipient surfaces โ€” including a test that pins the DynamoDB **API surface** the cascade may use, mutation-checked by reintroducing the bug and confirming it fails. + +--- + +## Feature announcements + +Admins can now publish release notices to users, target them by role, and see whether anyone read them. + +### Backend + +- **`{prefix}-announcements`** โ€” one table, two item shapes: announcement rows under a fixed `ANNOUNCEMENTS` partition and each user's ack rows under `USER#`. No GSIs. +- `/admin/announcements` โ€” list, get, create, patch, `publish`, `archive`, `revise`, delete, and `{id}/stats` (#966, #972, #978). +- `GET /announcements` + an acknowledgement endpoint for users (#969). +- Gated by `ANNOUNCEMENTS_ENABLED`, **default on with a kill switch**. While off the routers are unmounted and the surface 404s; data and code remain intact. +- **Who may author** is the delegable `admin.announcements` scope. **Who sees** a published announcement is the announcement's own `targetRoles` โ€” a display filter, deliberately *not* an RBAC grant. + +### Frontend + +- **What's New panel** with the user's feed and ack state (#969). +- **Banner** (#976, #979, #981) โ€” floats rather than occupying layout, and sits beside the chat composer on the side the composer leaves free. Chat view only. +- **Modal** for high-priority announcements, gated by the spec's ยงD8 rules (#977). +- **Reach column** on the admin list, driven by ack funnel counters (#978). + +> โš ๏ธ Announcement ack counters are **incremented, never backfilled**. An announcement published before this release has no counters and will read as zero reach rather than as unknown. + +--- + +## Mid-turn steering + +Type a follow-up while a turn is still streaming and it now lands *inside* that turn. + +The text is injected at the next tool boundary as a `{"text": ...}` block on the same user-role message that carries the tool results, so the agent reads it before choosing its next action. A new `steering_applied` SSE event is emitted after that batch's `tool_result` events, so the thread renders in the order the model will see. + +**The transport is the session's existing single-flight lease row** โ€” `steerQueue` + `steerFor`, owner-scoped exactly like `cancelRequestedFor`, armed by `POST /sessions/{id}/steer` and deleted with the lease at turn end. No new table, no new stream. + +Consumption is **commit-on-append**: the hook peeks at `AfterToolsEvent` and clears the inbox entry only on the `MessageAddedEvent` for that same message. `AfterToolsEvent` fires from a `finally`, so it also fires on the interrupt path where the mutated message is discarded โ€” a hook that consumed on read would destroy the user's words whenever a steer landed on the same tool batch as an OAuth consent. + +**Absence of the event is a fallback, not an error.** A turn that calls no tools has no boundary to inject at, and a steer can lose the race with the turn's end; in both cases the entry stays queued and is sent as a normal turn. + +It is **append-only against the cached prefix** โ€” the injection lands inside the segment the message-level cachePoint covers โ€” so it never rewrites the prefix and costs nothing in cache terms. A test locks that placement. + +Gated by `MID_TURN_STEERING_ENABLED`, default on with a kill switch; while off the steer endpoint 404s and the hook returns immediately. Spec: `docs/specs/mid-turn-steering.md`. + +Four defects were found by validating this on dev and fixed in the same release: a steer rendered once per sync tick instead of once (#930), a follow-up typed while a turn was *paused* was dropped rather than queued (#934), the steer bubble used a non-standard color (#935), and a user bubble's overflow was measured once and latched (#937). + +--- + +## Single-file rebranding + +This repo is forked by institutions that are not Boise State, and until now rebranding meant hunting colors through components. + +`frontend/ai.client/src/branding/brand.config.ts` is now the only file to edit for every non-logo brand value: app name, page title, greeting templates and fallbacks, logo paths, brand colors, and surface anchors. Consumers never import it directly โ€” they read through `BrandingService`, which normalizes and defends against missing or invalid values. + +- **Surface colors are validated, not merely accepted.** Each surface anchor must fall inside a per-role OKLCH band (`light` Lโ‰ฅ0.90 / Cโ‰ค0.04, `raised` Lโ‰ฅ0.95 and above `light` / Cโ‰ค0.03, `dark` Lโ‰ค0.32 / Cโ‰ค0.05) or it is rejected and reset to the default neutral for that role. The bands keep page and card backgrounds legible while still allowing a brand tint. +- **Four generators** run at `prestart` and `prebuild`: brand theme, surface theme, surface colors, and favicons (`sharp` + `tsx` are new dev dependencies for this). +- **Golden-file and parity specs** pin the generated output, including a spec that fails if the rebranding guide goes missing โ€” documentation kept honest by test. +- New token layers: `styles/tokens/identity.css` and `styles/tokens/state.css`, plus generated `brand-theme.css`, `surface-theme.css` and `surface-colors.ts`. + +--- + +## Models and cost correctness + +### GPT-5.6 Sol, Terra and Luna + +Curated in the model catalog on the `bedrock-runtime` OpenAI-compatible endpoint (`us.openai.gpt-5.6-*`), with a new `provider="bedrock-responses"` transport (#949) and the IAM grant it turned out to need (#959). + +> โš ๏ธ **The OpenAI-compatible endpoint on `bedrock-runtime` authenticates under the `bedrock` service namespace, not `bedrock-mantle`.** Granting only `bedrock-mantle:CallWithBearerToken` returns `401 ... is not authorized to perform: bedrock:CallWithBearerToken`. + +**Every published GPT-5.6 rate in the catalog is corrected** (#980). They had been *derived* โ€” from a blend across models that came out 1000x wrong โ€” when AWS had published them in the model cards the whole time. The rates now come from the model cards; the derivation, where still used, reads a single-model day rather than a blend. + +### Explicit prompt-cache breakpoints: built, measured, shipped off + +GPT-5.6 supports explicit cache breakpoints. They were implemented (#954), measured, and came out **57% more expensive** than the provider's automatic caching, so the default is off (#956). The code stays for the next model family that prices it differently. + +Verified separately: GPT-5.6 caching **works** on the automatic path, at 10.6x on warm turns (#962). + +### Other cost fixes + +- **Prompt-cache TTL is derived from the serving model** rather than assumed, so `cacheStatus` stops misreading a hit as expired on models with a different TTL (#951). +- **`supportsCaching` is forced on where the provider caches unconditionally** (#960), and **Mantle models expose the caching controls** they were previously denied (#963). +- **OpenAI-family usage normalizes to disjoint buckets** (#945) โ€” cached tokens were being counted inside the input total. +- **The cache-write premium and the Global/Regional rate tier were both wrong** in cost derivation (#914). +- **An omitted `supported_param` is now unsupported, not pass-through** (#915) โ€” an empty `supportedParams` previously bypassed the guard entirely. + +--- + +## Multi-modal fine-tuning + +The fine-tuning surface assumed text. A **task-type registry** replaces that assumption, adding image and image+text tasks through the API and the SPA, with a **dollar-denominated quota** rather than a job count (#944). Generative VLMs are excluded from the dual-encoder task, instance types are validated, and the dataset tests are no longer optional. `pandas==2.3.3` is pinned in the backend to exercise the dataset contract โ€” 2.x is what the py3.10 SageMaker text DLC resolves to and the only line compatible with the existing numpy pin. + +--- + +## ๐Ÿ› Bug fixes + +- **Artifact share links stayed live after their conversation was deleted.** The cascade used `table.batch_writer()`, and the app-api task role has no `dynamodb:BatchWriteItem` โ€” it has the individual item actions, which is why the rest of the feature needed no IAM change (`TransactWriteItems` authorizes against those; `BatchWriteItem` does not). It failed closed in dev with an `AccessDeniedException` that reached no client, because the cascade runs in a never-raising background task after the 204. Fixed with per-row `DeleteItem`, which also isolates failures and keeps the zero-IAM-change property (#932). + > **The generalizable rule: moto proves *shape*, never *permission*.** All 14 cascade tests passed against a call the deployed role cannot make. +- **An empty "Shared with you" tab claimed "No artifacts match your search"** โ€” with an empty search box. The filtered-empty state gated on the *library* total, so any non-empty library made an empty *tab* look like a failed search. Now three ordered states: nothing anywhere > nothing in this tab > nothing matching the filter. The specs missed it because every empty-state test asserted which rows rendered, never which sentence appeared when none did (#975). +- **"Pop-up blocked" was reported on every artifact open**, successful ones included (#953). +- **A duplicate error toast** fired alongside the shared-artifact page's own inline 404 โ€” share calls now set `SUPPRESS_ERROR_TOAST`. `listShares` deliberately keeps its toast, because it degrades silently and the toast is its only signal (#927). +- **Artifact card actions overlapped the title** when the panel was docked and the chat column halved. Fixed with a **container query** โ€” the card is sized by the chat column, not the viewport, so a media query is the wrong instrument โ€” dropping labels to icons below 26rem with the labels visually hidden rather than removed (#927). +- The library view toggle stretched on narrow screens, and the grid card footer overflowed its card (#955). +- The new-announcement form's submit button could never enable (#974). +- Knowledge-base retrievability is now confirmed with a filtered query, and `TEXT_INDEXED` is classified correctly (#908). + +--- + +## ๐Ÿ”’ Security + +- **The custom HuggingFace model id reached a URL unvalidated.** The fine-tuning call site's comment has claimed to validate the id format since before this release; the code only checked non-empty and length. That was latent on `main` โ€” the multi-modal work made it reachable by interpolating the value into a Hub request path, which CodeQL flags as a critical `py/partial-ssrf`. The host was always hard-coded, so this was never an arbitrary-host SSRF; what it allowed was a value carrying URL structure changing the meaning of two sinks โ€” the pre-flight path, and `model_name_or_path` as forwarded to the training container. Now validated against an anchored repo-id pattern at **both** sinks, rather than one relying on the other's branch having run. + > The pattern uses `\Z`, not `$` โ€” `$` also matches immediately before a trailing newline, so an otherwise-anchored pattern accepts `org/model\n`. A test pins that. +- **All 47 open Dependabot alerts cleared** (#924), across the backend, the SPA, infrastructure, the docs site, and the backup/restore scripts. Notable: `cryptography` 48.0.1 โ†’ 50.0.1, `dompurify` โ‰ฅ3.4.13, `undici` โ‰ฅ7.29.0, `hono` โ‰ฅ4.12.34, `brace-expansion` โ‰ฅ5.0.9, `postcss` 8.5.12 โ†’ 8.5.28. +- **CodeQL: 11 high, 20 medium and 9 note findings remediated** (#925) โ€” principally log injection, across 18 backend modules and one SPA page. The nightly workflow is extended in the same pass. + +--- + +## ๐Ÿ—๏ธ Infrastructure + +- **`{prefix}-announcements`** DynamoDB table, **no GSIs**, name published to SSM at `/{prefix}/admin/announcements-table-name`. App-api gains `AnnouncementsTableAccess` (`GetItem`/`PutItem`/`UpdateItem`/`DeleteItem`/`Query`/`Scan`). +- **`bedrock:CallWithBearerToken`** on the inference-api role, for the `bedrock-runtime` OpenAI-compatible endpoint. +- **`CDK_ARTIFACT_SHARE_INBOX_ENABLED`** deploy variable โ†’ `ARTIFACT_SHARE_INBOX_ENABLED` on the app-api container. Default off. +- `infrastructure/gsi-inventory.json` gains `"announcements": []`. **No GSI operations on any existing table** โ€” this release is not subject to the one-index-per-`UpdateTable` split. + +--- + +## ๐Ÿ“ฆ Dependencies + +| Component | Package | From | To | +|---|---|---|---| +| Backend | `cryptography` | 48.0.1 | 50.0.1 | +| Backend | `aiohttp` | 3.14.1 | 3.14.3 | +| Backend | `pandas` | โ€” | 2.3.3 (added) | +| Frontend | `@angular/*` | 21.2.17 | 21.2.19 | +| Frontend | `mermaid` | 11.15.0 | 11.16.1 | +| Frontend | `postcss` | 8.5.12 | 8.5.28 | +| Frontend | `sharp` | โ€” | 0.33.0 (added) | +| Frontend | `tsx` | โ€” | 4.23.12 (added) | +| Frontend | `dompurify` | โ‰ฅ3.4.0 | โ‰ฅ3.4.13 | +| Frontend | `undici` | โ‰ฅ7.28.0 | โ‰ฅ7.29.0 | +| Frontend | `hono` | โ‰ฅ4.12.25 | โ‰ฅ4.12.34 | +| Infrastructure | `aws-cdk-lib` | 2.262.0 | 2.265.0 | +| Infrastructure | `brace-expansion` | โ€” | โ‰ฅ5.0.9 | + +--- + +## ๐Ÿš€ Deployment notes + +1. **Deploy order is `platform.yml` โ†’ `backend.yml` โ†’ `frontend-deploy.yml`.** This release changes `infrastructure/lib/constructs/**` and `config.ts`, so the push to `main` triggers `platform.yml` automatically. That order is **not enforced by the workflows** โ€” they share a concurrency group and queue, but nothing guarantees CDK wins the slot. If `backend.yml` runs first, the announcements routes will 500 on a missing table until the CDK deploy lands. Watch both runs. + +2. **Decide `CDK_ARTIFACT_SHARE_INBOX_ENABLED` before the deploy, not after.** It reaches the running service only through a `platform.yml` deploy (CDK writes it into the ECS task definition), and `platform.yml` is path-filtered to infra changes โ€” so a later backend- or frontend-only merge will *not* pick up a variable change. Set it now and it rides this release's CDK deploy; set it afterwards and you must dispatch `platform.yml` manually. + - Off (default): owner-side sharing, the library, previews, rename/delete and shared-conversation artifacts all ship. Only the "Shared with you" tab is absent, and its absence is silent by design. + - On: the tab appears, already populated โ€” the pointer rows have been accumulating since a share was first created. + +3. **No data migration.** The conversation-share snapshot's `artifacts` key is optional on read, so pre-existing shares read as `[]`. `GSI2PK`/`GSI2SK` stamping is inert until an index consumes it โ€” DynamoDB charges no index write when no index exists. + +4. **Two unflagged behavior changes.** An omitted `supported_param` is now treated as unsupported rather than passed through, and the prompt-cache TTL is derived from the serving model rather than assumed. Both are corrections; both take effect on the first turn after the deploy. + +5. **Announcement reach counters are not backfilled.** Anything published before this release reads as zero reach. + +6. **Post-deploy verification.** Open `/artifacts` and confirm the library lists and previews. Create a share, open it from a second account, then revoke it and confirm the link dies. Publish a test announcement and confirm the banner appears on the chat view. If the inbox flag is on, confirm the third tab appears and โ€” with nothing shared โ€” says "nothing in this tab" rather than "no artifacts match your search". + +--- + # Release Notes โ€” v1.17.0 **Release Date:** September 2, 2026 diff --git a/VERSION b/VERSION index 092afa15..84cc5294 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.0 +1.18.0 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 78b9e7ec..a2e25c75 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-stack" -version = "1.17.0" +version = "1.18.0" requires-python = ">=3.10" description = "Multi-agent conversational AI system with AWS Bedrock AgentCore" readme = "README.md" diff --git a/backend/src/apis/app_api/fine_tuning/routes.py b/backend/src/apis/app_api/fine_tuning/routes.py index 921dd3b8..f634fb86 100644 --- a/backend/src/apis/app_api/fine_tuning/routes.py +++ b/backend/src/apis/app_api/fine_tuning/routes.py @@ -2,6 +2,7 @@ import math import os +import re import uuid import logging from datetime import datetime, timezone, timedelta @@ -232,6 +233,38 @@ async def _fetch_tag(tag: str): raise HTTPException(status_code=502, detail="Failed to search HuggingFace models") +# A HuggingFace repo id is ``name`` or ``org/name``, each segment limited to +# word characters, dots and hyphens. Anchored, single optional slash, no +# percent-encoding and no dot-segments โ€” which is what keeps an id out of the +# URL structure when it is interpolated into a Hub request path below, and out +# of ``model_name_or_path`` in the training job's hyperparameters. +# ``\Z`` and not ``$`` โ€” ``$`` also matches immediately before a trailing +# newline, so "org/model\n" would pass an otherwise-anchored pattern. +_HF_MODEL_ID = re.compile( + r"\A[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._-]*)?\Z" +) + + +def validate_huggingface_model_id(raw: str) -> str: + """Return ``raw`` stripped, or raise 400 if it is not a repo id. + + The length ceiling alone was never enough: the value is interpolated into + a Hub URL path and forwarded to SageMaker as ``model_name_or_path``, so a + value carrying ``/``-heavy or dot-segment structure changes the meaning of + both sinks rather than merely failing to resolve. + """ + hf_id = raw.strip() + if not hf_id or len(hf_id) > 200 or not _HF_MODEL_ID.match(hf_id): + raise HTTPException( + status_code=400, + detail=( + "Invalid HuggingFace model ID. Use 'model' or 'org/model' โ€” " + "letters, digits, dots, hyphens and underscores only." + ), + ) + return hf_id + + async def preflight_huggingface_model(hf_id: str, spec) -> None: """Reject a custom HuggingFace model that cannot serve ``spec``. @@ -497,10 +530,7 @@ async def create_job( ) if request.custom_huggingface_model_id: - # Validate the custom HuggingFace model ID format (org/model or just model) - hf_id = request.custom_huggingface_model_id.strip() - if not hf_id or len(hf_id) > 200: - raise HTTPException(status_code=400, detail="Invalid HuggingFace model ID.") + hf_id = validate_huggingface_model_id(request.custom_huggingface_model_id) # Ask the Hub whether this model can actually serve the task before a # GPU is provisioned for it. await preflight_huggingface_model(hf_id, spec) @@ -521,7 +551,13 @@ async def create_job( # Custom HuggingFace model โ€” fall back to the task's own defaults. instance_type = request.instance_type or spec.default_instance_type hyperparameters = {**spec.default_hyperparameters} - huggingface_id = request.custom_huggingface_model_id.strip() + # Re-validate rather than reuse the value from the pre-flight block: + # this is the sink that reaches the training container as + # `model_name_or_path`, and it should be safe on its own terms rather + # than because of where an earlier branch happened to run. + huggingface_id = validate_huggingface_model_id( + request.custom_huggingface_model_id + ) model_name = huggingface_id _validate_instance_type(instance_type) diff --git a/backend/tests/fine_tuning/test_job_guards.py b/backend/tests/fine_tuning/test_job_guards.py index 585ee7fb..4dbdcdd5 100644 --- a/backend/tests/fine_tuning/test_job_guards.py +++ b/backend/tests/fine_tuning/test_job_guards.py @@ -16,6 +16,7 @@ _validate_dataset_format, _validate_instance_type, preflight_huggingface_model, + validate_huggingface_model_id, ) TEXT_SPEC = task_types.get_task_spec(task_types.TEXT_CLASSIFICATION) @@ -277,3 +278,57 @@ async def test_an_untagged_model_is_allowed_through(self, monkeypatch): ), ) await preflight_huggingface_model("someone/untagged", TEXT_SPEC) + + +class TestValidateHuggingFaceModelId: + """The id reaches two sinks: a Hub URL path, and the training job's + ``model_name_or_path``. A length ceiling alone let a value carrying URL + structure change the meaning of both.""" + + @pytest.mark.parametrize( + "hf_id", + [ + "bert-base-uncased", + "openai/clip-vit-base-patch32", + "meta-llama/Llama-3.2-1B", + "org/model.with.dots", + "org/model_with_underscores", + ], + ) + def test_accepts_real_repo_ids(self, hf_id): + assert validate_huggingface_model_id(hf_id) == hf_id + + def test_strips_surrounding_whitespace(self): + assert validate_huggingface_model_id(" org/model ") == "org/model" + + @pytest.mark.parametrize( + "hf_id", + [ + "../../etc/passwd", + "org/../../admin", + "..%2f..%2fadmin", + "org/model?redirect=https://evil.example", + "org/model#frag", + "org/model/extra", + "//evil.example/path", + "https://evil.example/model", + "org model", + "org/mo\ndel", + "", + " ", + "/leading-slash/model", + ], + ) + def test_rejects_anything_carrying_url_structure(self, hf_id): + with pytest.raises(HTTPException) as excinfo: + validate_huggingface_model_id(hf_id) + assert excinfo.value.status_code == 400 + + def test_a_trailing_newline_is_stripped_not_smuggled(self): + """`$` would match before a trailing newline; the pattern uses `\\Z`.""" + assert validate_huggingface_model_id("org/model\n") == "org/model" + + def test_rejects_an_overlong_id(self): + with pytest.raises(HTTPException) as excinfo: + validate_huggingface_model_id("a" * 201) + assert excinfo.value.status_code == 400 diff --git a/backend/uv.lock b/backend/uv.lock index 7cd72a95..29576f9d 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ [[package]] name = "agentcore-stack" -version = "1.17.0" +version = "1.18.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, diff --git a/frontend/ai.client/package-lock.json b/frontend/ai.client/package-lock.json index 0e162a0a..76fb3551 100644 --- a/frontend/ai.client/package-lock.json +++ b/frontend/ai.client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai.client", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai.client", - "version": "1.17.0", + "version": "1.18.0", "dependencies": { "@angular/cdk": "21.2.14", "@angular/common": "21.2.19", diff --git a/frontend/ai.client/package.json b/frontend/ai.client/package.json index b2c39e91..9aceb5a6 100644 --- a/frontend/ai.client/package.json +++ b/frontend/ai.client/package.json @@ -1,6 +1,6 @@ { "name": "ai.client", - "version": "1.17.0", + "version": "1.18.0", "scripts": { "ng": "ng", "prestart": "tsx scripts/branding/generate-brand-theme.ts && tsx scripts/branding/generate-surface-theme.ts && tsx scripts/branding/generate-surface-colors.ts && tsx scripts/branding/generate-favicons.ts", diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json index 710247a3..8d1a46d6 100644 --- a/infrastructure/package-lock.json +++ b/infrastructure/package-lock.json @@ -1,12 +1,12 @@ { "name": "infrastructure", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "infrastructure", - "version": "1.17.0", + "version": "1.18.0", "dependencies": { "aws-cdk-lib": "2.265.0", "constructs": "10.6.0" diff --git a/infrastructure/package.json b/infrastructure/package.json index bd630a53..f2e3814d 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -1,6 +1,6 @@ { "name": "infrastructure", - "version": "1.17.0", + "version": "1.18.0", "bin": { "infrastructure": "bin/infrastructure.js" }, diff --git a/tui/pyproject.toml b/tui/pyproject.toml index 4860651a..7ec85aad 100644 --- a/tui/pyproject.toml +++ b/tui/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-tui" -version = "1.17.0" +version = "1.18.0" requires-python = ">=3.11" description = "Terminal client for the AgentCore Public Stack โ€” streaming AI chat in your terminal" readme = "README.md" diff --git a/tui/src/agentcore_tui/__init__.py b/tui/src/agentcore_tui/__init__.py index 4e2436ce..fa6ceda2 100644 --- a/tui/src/agentcore_tui/__init__.py +++ b/tui/src/agentcore_tui/__init__.py @@ -7,6 +7,6 @@ from __future__ import annotations -__version__ = "1.17.0" +__version__ = "1.18.0" __all__ = ["__version__"] diff --git a/tui/uv.lock b/tui/uv.lock index 3b882887..5fb69093 100644 --- a/tui/uv.lock +++ b/tui/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentcore-tui" -version = "1.17.0" +version = "1.18.0" source = { editable = "." } dependencies = [ { name = "httpx" },