diff --git a/CHANGELOG.md b/CHANGELOG.md index d65e9d16..46f158e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,69 @@ 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.16.0] - 2026-08-28 + +Minor release on knowledge bases, the marketplace review flow, and fine-tuning. The Bedrock Managed Knowledge Base migration lands **inert** โ€” every managed-KB flag is default OFF, and the owner-facing upgrade card only appears where `CDK_MANAGED_KB_MIGRATION_ENABLED=true`. Marketplace admins can finally read, test-drive and decline a submission instead of approving on a name and a category alone. Fine-tuning was unreachable in every deployed environment and now isn't โ€” it becomes **reachable by default** on this deploy, with `CDK_FINE_TUNING_ENABLED=false` as the kill switch. Two live RAG behaviours change regardless of flags: queries clamp at 10,000 characters on both backends, and the document-status filter now fails **closed**. **Requires a CDK deploy** โ€” `platform.yml` (set `CDK_TAG_ENVIRONMENT` first), then `backend.yml` (two new kb-migration jobs), then `frontend-deploy.yml`. + +### ๐Ÿš€ Added + +- **Bedrock Managed Knowledge Base migration**, shipped dormant behind nine `CDK_MANAGED_KB_*` flags. Adds a shared Bedrock KB service role, four `DockerImageFunction`s on one image (worker, dispatcher, reconciler, ingestion consumer) with a DLQ, three EventBridge rules, four CloudWatch alarms, and a sparse `KbWorkIndex` GSI (#884) +- Owner-facing knowledge-base upgrade card and `/assistants/{id}/knowledge-base/upgrade` router (`GET` status, `POST` enroll, `/retry`, `/notice`) with derived phases `none|available|in_progress|succeeded|failed`; also surfaces stranded documents the ordinary list hides (#884) +- Marketplace submission review โ€” `GET /admin/agents/{agent_id}/submission` returns the frozen `submittedVersion` snapshot (instructions, bound capabilities, model, starters, publisher, reachability) behind a new SPA route `/admin/marketplace/review/:agentId` (#882) +- Reviewer **test drive** โ€” a `review_preview` invocation resolves the reviewed snapshot and bypasses the PRIVATE check after re-resolving `admin.marketplace` against the caller's own roles (new `has_admin_scope` predicate); runs on a `preview-` session and skips bookkeeping writes (#882) +- New terminal listing state **`rejected`** โ€” admin-only from `in_review`, requires a reason, allows revise-and-resubmit; `rejected โ†’ private` keeps delete reachable. No edge to `published` (#882) +- `@`-mentions are set apart in the user's own message, matched against the known agent-name list rather than `@\w+` so names with spaces work and `@here`/npm scopes/emails aren't bolded (`mention-text.component.ts`) +- `scripts/teardown/managed-kb.sh` โ€” tag-scoped teardown for managed KBs, which are created at runtime and are not CloudFormation children; `destroy.sh` runs it as Phase 0 and aborts the teardown on failure (#884) +- `scripts/local-dev/refresh-env.py` โ€” rebuilds `backend/src/.env` from the deployed app-api task definition, preserving a `KEEP_LOCAL` set; dry-run by default (#884) + +### โœจ Improved + +- Submitting an agent no longer requires ticking a mandatory "make public" checkbox โ€” an amber disclosure states that submitting makes the agent public, and `makePublic` is always sent. Backend `_visibility_block` is unchanged, so a direct API caller that omits it is still refused +- The review test-drive panel is viewport-sized and sticky while the reviewer scrolls the instructions, with an expand control that spans both columns by class change only, so the reviewer's conversation survives the toggle. Empty summary/system prompt render explicit "none" text (#883) + +### ๐Ÿ› Fixed + +- **Fine-tuning was unreachable in every deployed environment** โ€” `/api/fine-tuning/access` returned 404 because `FINE_TUNING_ENABLED` was never set on the app-api container, and `CDK_FINE_TUNING_ENABLED` had had no reader since the single-stack migration (#396). `config.ts`, `app-api-service-construct.ts` and `platform.yml` now wire it, plus `CDK_FINE_TUNING_DEFAULT_QUOTA_HOURS` and the never-forwarded `CDK_FINE_TUNING_CORS_ORIGINS` +- **A JSONL dataset โ€” the format the upload page names first โ€” died ~5 billed GPU-minutes into training** with `No CSV file found`. `sagemaker_scripts/train.py` reads JSONL and JSON alongside CSV via a dispatch table and validates the promised `text`/`label` columns; unreadable formats are rejected at `/presign` and again at `POST /jobs`, the last gate before SageMaker provisions a GPU (#893) +- **The admin fine-tuning cost dashboard reported $0.00 and 0 jobs for every period** while jobs were billing. Three stacked faults: the `StatusIndex` PK was queried with SageMaker's `"Completed"`/`"Stopped"` casing against records stored `"COMPLETED"`/`"STOPPED"`; `FAILED` was excluded although AWS bills partial runs; and training/inference share the table and index, so training is now filtered on the `JOB#` sort-key prefix +- **An unpriced `instance_type` ran real GPUs and recorded $0.00** โ€” it arrived unvalidated off the request body on both create paths, and `calculate_cost` falls back to `0.0` outside its 11-entry map. The quota meters GPU-hours, not dollars, so a 10-hour allowance buys ~$14 on `ml.g5.xlarge` or several hundred on a larger unlisted type. Both paths validate the resolved type and 400 with the supported list (#894) +- **Arrow keys could not walk the `@`-mention menu** โ€” `keyup` re-ran `syncMentionToken`, resetting the highlight to row one on every press. The highlight now resets only when the token itself changed, and the active row scrolls into view (#895) +- **JWT role mappings rejected IdP group names containing spaces**, 400-ing the whole `PATCH /api/admin/roles/{role}` payload including untouched entries. The pattern now allows single internal spaces; the error names the offending entry with invisible characters escaped as ``, and `_FORBIDDEN_PROTECTED_MAPPINGS` compares case-folded with space/hyphen/underscore as one separator so `All Users` stays blocked now that it's typeable (#880) +- Image attachments showed the browser's broken-image glyph in long turns โ€” presigned GET URLs live 10 minutes and `loading="lazy"` tiles often fetch after expiry. All three render sites now re-mint once on `(error)`; the lightbox pre-refreshes within 30s of expiry (#879) +- The managed-KB dispatcher raised `KB_MIGRATION_WORKER_FUNCTION_NAME is not set` on every tick โ€” the construct published `MANAGED_KB_WORKER_FUNCTION_NAME`. A second mismatch silently replaced the operator's configured retention window with the 30-day code floor. A new contract test parses every `os.environ` read in the handlers and asserts the construct sets each and publishes nothing unread (#887) +- The first real migration failed with `AccessDeniedException` on `bedrock:TagResource` โ€” AWS authorizes tagging separately from `CreateKnowledgeBase`. Adds `bedrock:TagResource` and `bedrock:ListTagsForResource`; without the second, the daily orphan sweep would have reported a clean account forever (#888) +- A dev deploy tagged its managed KBs `ManagedKbEnvironment=prod` โ€” `config.production` is `true` everywhere and `config.tags` had no `Environment` key โ€” so the tag-scoped teardown would have matched nothing and reported success, leaving billed KBs alive. New `CDK_TAG_ENVIRONMENT` variable, forwarded as flat-dotted `--context tags.Environment=` (#885) +- A refused marketplace decision showed the backend message twice, inline and as a global toast. `SUPPRESS_ERROR_TOAST` is set on exactly the four calls that render inline; takedown on the Listings page has no inline region and keeps its toast, pinned by a test (#890) +- The inline decision error rendered 565px above the sticky decision bar โ€” off-screen at the moment of the click. Load failures stay at the top; a new `decisionError()` renders inside the sticky bar directly above Approve / Request changes / Decline + +### โš ๏ธ Changed + +- **The RAG document-status filter now fails CLOSED.** Both table-level fallbacks in `_filter_vectors_by_document_status` (unset `DYNAMODB_ASSISTANTS_TABLE_NAME`, and the outer `except`) drop every chunk instead of returning them unfiltered, log at ERROR and emit `KbStatusFilterFailClosed`. Any retrieval-serving service missing that variable now returns zero chunks (#884) +- **Retrieval queries clamp at 10,000 characters on both backends**, including the legacy S3 Vectors path that previously accepted ~32,000. Applied in the facade before dispatch, emits `KbQueryClamped`, never raises (#884) +- Retrieval runs under an explicit `kb_access.granted(...)` grant threaded from both call sites instead of the facade re-resolving. On the marketplace `review_preview` path the permission is deliberately `None`, so **a reviewer test-drives a RAG-backed agent with an empty knowledge base** โ€” fail-closed by design (#882, #884) +- `rag_service.py` is now a facade over a `KnowledgeBaseBackend` protocol with score direction normalized to `relevance` (higher-is-better) inside the S3 Vectors adapter; the `distance` key is still emitted for the existing HTTP consumer. Public signature and both call sites unchanged (#884) +- `.txt` dropped from the fine-tuning **training** upload copy โ€” it cannot express a label. It remains valid for inference input (#893) + +### ๐Ÿ—๏ธ Infrastructure + +- New GSI **`KbWorkIndex`** (`GSI7_PK`/`GSI7_SK`, projection ALL) on the existing rag-assistants table โ€” sparse, written exclusively by `kb_backend/records.py`, registered in `gsi-inventory.json`. This is the release's one GSI operation; anything else adding an index to that table must ship separately (#884) +- `ManagedKbRoleConstruct` โ€” one Bedrock KB service role with `aws:SourceAccount` + `ArnLike AWS:SourceArn` confused-deputy conditions, S3 read conditioned on `aws:ResourceAccount`, `bedrock:InvokeModel` pinned to `amazon.titan-embed-text-v2:0`, and `iam:PassRole` conditioned on `iam:PassedToService` (#884) +- `KbMigrationConstruct` โ€” four Lambdas + DLQ + log groups; dispatcher rule `rate(15 min)` created **disabled** unless `migrationEnabled`, reconciler rule `rate(1 day)` **always enabled** in report-only mode, documents rule enabled when either flag is on; four alarms in namespace `${projectPrefix}/ManagedKb`, all NOT_BREACHING on missing data; SSM parameters publishing the four function names (#884) +- `documentsBucket.enableEventBridgeNotification()` โ€” additive, because S3 rejects two overlapping-prefix notification configs and the existing rag-ingestion notification is untouched (#884) +- Nine new `CDK_MANAGED_KB_*` variables. The three booleans deliberately invert the repo's default-ON idiom โ€” managed storage is $5.00/GB-month against ~$0.15 โ€” using `parseBooleanEnv`, which maps unset *and* empty to `undefined` so an unset Actions variable cannot arm them (#884) +- `scripts/common/load-env.sh` forwards each `CDK_MANAGED_KB_*` only when non-empty, as **flat dotted** `--context managedKb.=` (`--context a.b=c` sets `context["a.b"]`; it does not build a nested object), and fails at deploy time naming any boolean that isn't `true|false|1|0|empty` (#884, #885) +- New app-api env vars: `MANAGED_KB_MIGRATION_ENABLED`, the byte caps, `MANAGED_KB_METRIC_NAMESPACE`, `FINE_TUNING_ENABLED`, `FINE_TUNING_DEFAULT_QUOTA_HOURS` + +### ๐Ÿ”ง CI/CD + +- `backend.yml` gains `build-kb-migration` (ubuntu-24.04-arm) and `deploy-kb-migration-code`, which points all four functions at the one image tag. Until they run, the Lambdas stay bootstrap no-ops (#886) +- `Dockerfile.kb-migration` pins `boto3==1.43.68` **functionally, not for hygiene** โ€” the base image's 1.40.4 has no `managedKnowledgeBaseConfiguration` shape and every `CreateKnowledgeBase` fails with `ParamValidationError`. Three tests guard the pin (#886) + +### ๐Ÿ“š Docs + +- Weekly kaizen research scan and review prep for 2026-08-28; review queue trimmed 39 โ†’ 38 open, retiring the tool-mutation-probe entry and striking two MCP Apps prerequisites and a stale Strands #3758 caveat (#891, #892) +- `.github/docs/deploy/step-03-github-config.md` documents the managed-KB flags and their deliberate default-OFF posture (#884) + ## [1.15.0] - 2026-08-17 Minor release on attachments and external tools. PowerPoint decks can now be uploaded and handed to the PowerPoint toolset, and attachment cards survive a reload โ€” a gap that also affected spreadsheets. OAuth-gated MCP servers no longer lose their tools permanently when the pre-flight runs on a cold token cache, and an abandoned consent prompt no longer bricks every later message in the conversation. Two IAM grants missing in production are fixed, and ALB access logs are enabled so a mid-stream disconnect can be attributed to whoever actually ended the connection. **Requires a CDK deploy** โ€” run `platform.yml`, then `backend.yml`, then `frontend-deploy.yml`. diff --git a/README.md b/README.md index 647e31a3..6539277a 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.15.0-6366f1?style=flat&logo=github&logoColor=white)](RELEASE_NOTES.md) +[![Release](https://img.shields.io/badge/Release-v1.16.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.15.0 +**Current release:** v1.16.0 --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 55fb1736..9b976bbd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,185 @@ +# Release Notes โ€” v1.16.0 + +**Release Date:** August 28, 2026 +**Previous Release:** v1.15.0 (August 17, 2026) + +--- + +> ๐Ÿ—๏ธ **CDK deploy required, and this one has prerequisites.** Set `CDK_TAG_ENVIRONMENT` (`dev` / `prod`) as a GitHub Environment variable **before** running `platform.yml`, then `backend.yml` โ€” which now carries two new kb-migration jobs that must run before the managed-KB Lambdas are anything but no-ops โ€” then `frontend-deploy.yml`. +> +> `infrastructure/gsi-inventory.json` adds **one** index, `KbWorkIndex`, to the existing rag-assistants table. One GSI operation per `UpdateTable` is the hard DynamoDB limit, so nothing else may add an index to that table in this release. +> +> **Two changes take effect on the next deploy whether or not you set any flag:** fine-tuning becomes reachable in every environment, and RAG retrieval's document-status filter now fails closed. Read the Deployment notes before shipping. + +--- + +## Highlights + +A minor release about knowledge bases, marketplace governance, and a feature that turned out never to have been switched on. + +**The Bedrock Managed Knowledge Base migration lands inert.** ~29,000 lines โ€” a Bedrock service role, four Lambdas, a migration saga, a reconciler, an owner-facing upgrade card โ€” all of it behind nine `CDK_MANAGED_KB_*` flags that default OFF. That inversion of the repo's usual default-ON idiom is deliberate: managed storage bills at $5.00/GB-month against roughly $0.15 for the S3 Vectors path, so an unset Actions variable must never be able to arm it. What *is* live from day one is the reconciler, in report-only mode, so you can read what it would have deleted before you let it delete anything. + +**Marketplace admins can finally review a submission.** Approving an agent โ€” publishing someone's instructions, tools and model to everyone โ€” previously showed a name, an author and a category. Reviewers can now read the frozen submitted snapshot, test-drive it in a live chat panel, and decline it with a reason. + +**Fine-tuning was unreachable in every deployed environment.** `FINE_TUNING_ENABLED` was never set on the app-api container, so `/api/fine-tuning/access` had been returning 404 everywhere since the single-stack migration. The wiring lands here, which means the feature **turns on with this deploy** unless you set `CDK_FINE_TUNING_ENABLED=false`. Three bugs behind that 404 are fixed alongside it: JSONL datasets died five billed GPU-minutes into training, the admin cost dashboard read $0.00 while jobs were billing, and an unpriced instance type ran real GPUs and recorded nothing. + +**Two live RAG behaviours change with no flag to gate them.** Queries clamp at 10,000 characters on both backends, and the document-status filter fails closed โ€” an environment missing `DYNAMODB_ASSISTANTS_TABLE_NAME` on a retrieval-serving service will now return zero chunks rather than unfiltered ones. + +--- + +## Managed Knowledge Base migration + +Assistant knowledge bases can move from the S3 Vectors pipeline onto Bedrock Managed Knowledge Bases. Everything needed to do that ships in this release and none of it runs yet. + +The shipped state is dormant on purpose. Managed storage is $5.00/GB-month; the existing path is about $0.15. A flag that defaults ON with a kill switch โ€” the repo's normal idiom โ€” would mean a forgotten variable costs money, so the three behavioural flags use `parseBooleanEnv`, which maps unset *and* empty string to `undefined`. An Actions variable that exists but is blank cannot arm the feature. + +### The flags + +| GitHub variable | Default | Effect | +|---|---|---| +| `CDK_MANAGED_KB_NEW_DEFAULT` | off | New knowledge bases provision managed instead of legacy | +| `CDK_MANAGED_KB_MIGRATION_ENABLED` | off | Creates the dispatcher rule enabled *and* un-no-ops the handler; also gates the owner-facing upgrade card | +| `CDK_MANAGED_KB_RECONCILER_ARMED` | off | Reconciler **deletes** orphans instead of reporting them | +| `CDK_MANAGED_KB_PER_OWNER_BYTES` | 100 MB | Per-owner storage allowance | +| `CDK_MANAGED_KB_PER_OWNER_ELEVATED_BYTES` | 1 GB | Elevated per-owner allowance | +| `CDK_MANAGED_KB_PER_KB_CEILING_BYTES` | 500 MB | Per-knowledge-base ceiling | +| `CDK_MANAGED_KB_RETENTION_WINDOW_DAYS` | 30 | Post-promotion retention; must stay โ‰ฅ 30 | +| `CDK_MANAGED_KB_STORAGE_ALARM_GB` | 500 | Total-storage alarm threshold | +| `CDK_MANAGED_KB_DAILY_COST_ALARM_USD` | 100 | Daily-cost alarm threshold | + +`scripts/common/load-env.sh` forwards each only when non-empty, and validates the three booleans at deploy time โ€” a typo fails the deploy naming the variable rather than silently reading as false. + +### Backend + +- `apis/app_api/kb_upgrade/` โ€” its own package, deliberately outside `kb_migration/` for image size. Router mounted at `/assistants/{assistant_id}/knowledge-base/upgrade`: `GET ""` (status, readable by any resolved permission, reports `canUpgrade: false` to viewers), `POST ""` (enroll), `POST "/retry"`, `POST "/notice"`. Clients see the derived phases `none|available|in_progress|succeeded|failed` and never learn about shadow/verify/promote +- Enrolment is **two conditional writes**, not one. `KbRecord.to_item` does not write the GSI7 work keys, so a single put would produce a record that reports an upgrade in progress and is invisible to the dispatcher forever +- `kb_backend/records.py` is the sole writer of the sparse `GSI7_*` keys; `GSI7_*` joins the generic assistant-update immutable-field guard +- `rag_service.py` becomes a facade over a `KnowledgeBaseBackend` protocol. Score direction is normalized to `relevance` (higher-is-better) by exact negation inside the S3 Vectors adapter, with the `distance` key still emitted for the existing HTTP consumer. Public signature and both call sites unchanged +- Four Lambda handlers plus `kb_backend`, shipped on one image (`backend/Dockerfile.kb-migration`) + +### Frontend + +- `knowledge-base/kb-upgrade.service.ts` and the upgrade card in `knowledge-base-section.component.*`. The card also surfaces stranded documents โ€” distinguishing an unsupported format from a processing failure, and showing `deleting` documents the ordinary list hides +- `app-api-environment.ts` now passes `MANAGED_KB_MIGRATION_ENABLED`, the byte caps and `MANAGED_KB_METRIC_NAMESPACE` to the app-api task. They were absent, so the card would have rendered nothing anywhere regardless of the flag + +### Infrastructure + +- **`ManagedKbRoleConstruct`** โ€” one Bedrock KB service role for all managed KBs, with `aws:SourceAccount` + `ArnLike AWS:SourceArn` confused-deputy conditions, S3 read conditioned on `aws:ResourceAccount`, `bedrock:InvokeModel` pinned to `amazon.titan-embed-text-v2:0`, caller grants split by SID (provisioning CRUD / direct ingestion / inference Retrieve), and `iam:PassRole` scoped and conditioned on `iam:PassedToService` +- **`KbMigrationConstruct`** โ€” worker (15 min / 1024 MB), dispatcher (2 min / 512 MB), reconciler (15 min / 512 MB), ingestion consumer (15 min / 1024 MB with a DLQ), all on one image; four log groups; SSM parameters publishing the four function names +- **EventBridge rules** โ€” dispatcher `rate(15 minutes)`, created disabled unless `migrationEnabled`; reconciler `rate(1 day)`, **enabled unconditionally**, because report-only is the point; documents rule enabled when either `newDefault` or `migrationEnabled` is set +- **Four alarms** in namespace `${projectPrefix}/ManagedKb` โ€” total storage, KB count at 80% of the 10,000 account quota, daily cost, orphan count. All NOT_BREACHING on missing data +- **`KbWorkIndex`** (`GSI7_PK`/`GSI7_SK`, projection ALL) on the rag-assistants table, sparse โ€” keys exist only while a KB is eligible for work +- `documentsBucket.enableEventBridgeNotification()` โ€” additive. S3 rejects two overlapping-prefix notification configurations, so the ingestion consumer is EventBridge-triggered and the existing rag-ingestion notification is left alone +- `infrastructure/lib/config.ts` gains `ManagedKbConfig`, four named default constants, and a resolution chain of `CDK_*` env var โ†’ flat dotted context โ†’ nested object โ†’ literal default + +### Teardown + +Managed knowledge bases are created at **runtime** by the provisioning saga, so they are not CloudFormation children: `delete-stack` leaves them billing at $5.00/GB-month and invisible in the CFN console. `scripts/teardown/managed-kb.sh` scopes **by tag**, never by name pattern, because two environments can share an account. It polls to 480s to match `tombstones.KB_DELETE_POLL_TIMEOUT_SECONDS`, clamps its tunables (an interval of 0 once spun for sixteen hours), and exits non-zero if any matched KB is not confirmed absent. + +`destroy.sh` runs it as **Phase 0**, before any stack, and aborts the whole teardown on failure โ€” the Bedrock service role lives in PlatformStack, and deleting it first is a plausible route into the terminal `DELETE_UNSUCCESSFUL` state. + +### Two operator traps found while shipping this + +Both were caught on a running stack rather than in review, and both are worth knowing about because the failure mode was silence, not an error. + +**The dev environment tagged its knowledge bases `prod`.** `config.production` is `true` in every environment and `config.tags` carried no `Environment` key, so the tag-scoped teardown script would have matched nothing, exited 0, and left billed KBs alive. Fixed by a new `CDK_TAG_ENVIRONMENT` variable forwarded as a flat-dotted `--context tags.Environment=` โ€” `config.production` deliberately untouched. + +**The dispatcher and the construct disagreed about two variable names.** Every tick raised `KB_MIGRATION_WORKER_FUNCTION_NAME is not set` because the construct published `MANAGED_KB_WORKER_FUNCTION_NAME`, so no knowledge base could have migrated. The same sweep found a second mismatch that would have been quieter and worse: the construct published `MANAGED_KB_RETENTION_WINDOW_DAYS` while `worker._retain_days()` reads `KB_MIGRATION_RETAIN_DAYS`, so an operator's configured retention window would have been silently replaced by the 30-day code floor. `backend/tests/supply_chain/test_kb_migration_env_contract.py` now parses every `os.environ` read in the handlers and `kb_backend` and asserts the construct sets each one โ€” and publishes nothing that is never read. + +A third was an IAM gap: AWS authorizes `bedrock:TagResource` separately from `CreateKnowledgeBase`, so the first real migration failed outright. `bedrock:ListTagsForResource` went in with it, and matters more quietly โ€” `tombstones.iter_project_knowledge_bases` reads tags to decide ownership and fails closed, so without it the daily orphan sweep would have reported a clean account forever. + +--- + +## Marketplace submission review + +Approving a marketplace submission publishes another person's instructions, bound tools and model choice to every user. Until now the reviewer saw a name, an author and a category. This release gives them the three things the decision actually needs. + +**Read it.** `GET /admin/agents/{agent_id}/submission` returns instructions, bound capabilities by name, model, conversation starters, publisher and reachability. It always serves the frozen `submittedVersion` snapshot, never the live draft โ€” the author cannot change what is under review while it is under review. New SPA route `/admin/marketplace/review/:agentId` (`submission-review.page.ts`). + +**Test-drive it.** A `review_preview` flag on the inference invocation payload resolves the reviewed snapshot and bypasses the PRIVATE visibility check โ€” after re-resolving `admin.marketplace` against the caller's own roles through a new `has_admin_scope` predicate in `shared/auth/rbac.py`. The preview runs on a `preview-` session and skips bookkeeping writes, so a review leaves no trace in the author's or the reviewer's history. The shared rule lives in `version_resolution.resolve_review_agent`. + +**Decline it.** New terminal listing state `rejected`, admin-only from `in_review`, requiring a reason and allowing revise-and-resubmit. `rejected โ†’ private` exists so the author can still delete the agent. There is deliberately no edge from `rejected` to `published`. + +### Submitting now discloses what it does + +`submit-listing-dialog.component.ts` drops the mandatory "Make this agent public" checkbox in favour of an amber disclosure โ€” *submitting makes this agent public* โ€” and always sets `makePublic` on the request. The relaxation is UI-only: the backend `_visibility_block` is unchanged, so a direct API caller that omits the field is still refused. + +### Three follow-ups from using it + +The test-drive panel was sized by its grid cell, which on a short submission left about 120px of usable chat. It is now viewport-sized and sticky while the reviewer scrolls the instructions, with an expand control that spans both columns **by a class change only** โ€” no DOM remount, so the reviewer's conversation survives the toggle. + +A refused Approve reported itself twice, inline and as a global toast. `SUPPRESS_ERROR_TOAST` now rides exactly four calls that render inline โ€” submission read, diff, review decision, withdrawal decision โ€” and deliberately not the whole service: takedown on the Listings page has no inline region, so its toast is kept and a test pins that. + +Then the surviving inline message turned out to render 565px above the sticky decision bar, i.e. off-screen at the moment of the click. Load failures stay at the top; a new `decisionError()` renders inside the sticky bar, directly above Approve / Request changes / Decline. + +### One thing to tell your reviewers + +A RAG-backed agent test-drives with an **empty knowledge base**. Retrieval now runs under an explicit `kb_access.granted(...)` grant threaded from the call sites, and on the `review_preview` path `assistant_permission` is deliberately `None` โ€” fail-closed by design, and called out in-code as an open policy decision. A reviewer who does not know this will read a degraded answer as a broken agent. + +--- + +## Fine-tuning becomes reachable โ€” and correct + +`/api/fine-tuning/access` returned 404 in every deployed environment. `FINE_TUNING_ENABLED` mounts both `/fine-tuning` and `/admin/fine-tuning` and defaults to `"false"` in Python, and nothing ever set it on the app-api container โ€” the existing `CDK_FINE_TUNING_ENABLED` repo variable had had no reader since the single-stack migration in #396. + +Three links close the gap: `config.ts` resolves `fineTuning.enabled` and `defaultQuotaHours`, `app-api-service-construct.ts` sets them on the container, and `platform.yml` forwards `CDK_FINE_TUNING_ENABLED`, `CDK_FINE_TUNING_DEFAULT_QUOTA_HOURS` and `CDK_FINE_TUNING_CORS_ORIGINS` โ€” the last also never forwarded before. This one follows the repo's normal idiom: **default ON, with only the literal `"false"` disabling it.** + +Three bugs the 404 had been hiding go with it. + +**A JSONL dataset died five billed GPU-minutes into training.** JSONL is the first format the upload page names. It uploaded fine, dispatched fine, and then `train.py` failed with `No CSV file found`. `sagemaker_scripts/train.py` now reads JSONL and JSON alongside CSV through a dispatch table and validates that the promised `text`/`label` columns exist. Unreadable formats are rejected at `/presign` and again at `POST /jobs` โ€” the second gate is the last point before SageMaker provisions a GPU. `.txt` is dropped from the training upload copy, since it cannot express a label; it remains valid for inference input. + +**The admin cost dashboard read $0.00 and 0 jobs for every period** while jobs were billing. Three faults stacked: the `StatusIndex` GSI partition key was queried with SageMaker's `"Completed"`/`"Stopped"` casing while records store `"COMPLETED"`/`"STOPPED"`; `FAILED` was excluded although AWS bills partial runs, which the user-facing quota counter was already charging for; and training and inference share the table and index, so fixing the casing alone would have 500'd on a `model_id` KeyError. Training is now filtered on the `JOB#` sort-key prefix. + +**An unpriced instance type ran real GPUs and recorded $0.00.** `instance_type` arrived unvalidated off the request body on both the training and inference create paths, and `calculate_cost` falls back to `0.0` for anything outside its 11-entry `INSTANCE_COST_PER_HOUR` map. The quota meters GPU-hours rather than dollars, so a 10-hour allowance buys roughly $14 on `ml.g5.xlarge` โ€” or several hundred on a larger unlisted type. Both paths now validate the **resolved** type, covering a bad value reaching inference from a stored job record, and 400 with the supported list. Not reachable from the SPA, where instance type is read-only; this was an API-level hole. + +--- + +## Chat and access-control fixes + +**Arrow keys can walk the `@`-mention menu.** `keyup` on the textarea re-ran `syncMentionToken`, which reset `mentionActiveIndex` to 0, so every ArrowDown snapped the highlight back to row one and the menu was unusable by keyboard. The highlight now resets only when the token itself โ€” query or start โ€” actually changed, and `agent-mention-menu.component.ts` scrolls the active row into view, which matters because the list scrolls at eight rows. Adds the composer's first spec. + +**`@`-mentions read as an address in your own message.** `@Brand Deck Builder` renders `font-semibold text-white` against the bubble's `text-white/90`. The new `mention-text.component.ts` matches against the known agent-name list from `AgentMentionService` โ€” the same session-cached list the composer's `@` menu warms โ€” rather than `@\w+`: agent names contain spaces, and a word pattern would also bold `@here`, npm scopes and email addresses. Stored message text is unchanged. + +**JWT role mappings accept IdP group names with spaces.** A mapping like `PSEmeriti Entra Sync` 400'd the whole `PATCH /api/admin/roles/{role}` payload, blocking even untouched entries. `_JWT_MAPPING_PATTERN` now allows single internal spaces (commas, edge whitespace, tab/NBSP/ZWSP still rejected), and the 400 body and log name the offending entry with invisible characters escaped as `` โ€” the failure that motivated this was invisible on screen. `_FORBIDDEN_PROTECTED_MAPPINGS` now compares case-folded with space, hyphen and underscore treated as one separator, so `All Users` and `Authenticated Users` stay blocked now that they are typeable at all. + +**Image attachments stop showing the broken-image glyph.** Presigned S3 GET URLs are minted once with a 10-minute lifetime, and `loading="lazy"` tiles in a long turn frequently fetch after expiry. All three render sites now handle `(error)` with a one-shot re-mint: `image-attachment-group.component.ts` restores the retry budget on `(load)` and pre-refreshes when the lightbox opens within 30s of expiry, `image-lightbox.component.ts` gains an `imageError` output and a guard against ``, and PDF page-1 thumbnails in `file-attachment-badge.component.ts` fall back to the skeleton. + +--- + +## โš ๏ธ Changed โ€” live RAG behaviour, ungated + +Both of these apply to the existing S3 Vectors path on the next backend deploy. Neither is behind a managed-KB flag. + +**The document-status filter fails closed.** Both table-level fallbacks in `_filter_vectors_by_document_status` โ€” an unset `DYNAMODB_ASSISTANTS_TABLE_NAME`, and the outer `except` โ€” now drop *every* chunk rather than returning them unfiltered. Each logs at ERROR and emits `KbStatusFilterFailClosed`. This supersedes reliable-document-deletion Req 3.4; the per-document handler is unchanged. Verify the variable is set on every service that serves retrieval, and watch the metric after deploy. + +**Queries clamp at 10,000 characters.** Applied in the facade before dispatch, so it also clamps the legacy path, which was previously unclamped up to Titan's roughly 32,000. It emits `KbQueryClamped` and never raises. + +--- + +## ๐Ÿงช Test coverage + +Roughly 2,600 lines of new infrastructure tests alone โ€” `kb-migration.test.ts` (1,032), `managed-kb.test.ts` (596), `config.test.ts` (367), `fine-tuning-runtime-flags.test.ts` (100) โ€” plus the backend env-contract test that parses every `os.environ` read in the kb-migration handlers and asserts the construct sets each one, three tests guarding the `boto3==1.43.68` pin, a test pinning takedown's toast against the four suppressed calls, and the chat composer's first spec. + +--- + +## ๐Ÿš€ Deployment notes + +Order: **`CDK_TAG_ENVIRONMENT` first**, then `platform.yml`, then `backend.yml`, then `frontend-deploy.yml`. + +1. **Set `CDK_TAG_ENVIRONMENT` before deploying** โ€” `dev` for the development environment, `prod` for production, as a GitHub Environment variable. Without it, managed knowledge bases are tagged `prod` even in dev, and the tag-scoped teardown script matches nothing while reporting success. +2. **One GSI, deliberately.** `KbWorkIndex` on the existing rag-assistants table is this release's single index operation. `UpdateTable` permits exactly one GSI create or delete per call and CloudFormation issues one per changed table, so any other change adding an index to that table must ship in a separate release โ€” the 1.12.0 outage shape. +3. **Run the new `backend.yml` jobs.** `build-kb-migration` โ†’ `deploy-kb-migration-code` must run at least once, or the four Lambdas stay bootstrap no-ops. Do not relax the `boto3==1.43.68` pin in `Dockerfile.kb-migration`: the base image's 1.40.4 cannot express `managedKnowledgeBaseConfiguration` and every `CreateKnowledgeBase` fails at runtime. +4. **Fine-tuning becomes reachable with this deploy.** `CDK_FINE_TUNING_ENABLED` is already `true` at repo level and the flag is default-ON regardless, so the routes mount; set it to `false` explicitly to keep the feature dark. Access stays gated: `CDK_FINE_TUNING_DEFAULT_QUOTA_HOURS` is `0`, which is **whitelist-only** โ€” a user with no explicit grant in the `fine-tuning-access` table gets a 403, and no grant is auto-provisioned. Any value above 0 flips it to open access and auto-grants that many monthly GPU-hours to every authenticated user on first use, so confirm the value before raising it. Disabling leaves storage intact, so no dataset or trained model is orphaned. +5. **The document-status filter now fails closed.** Confirm `DYNAMODB_ASSISTANTS_TABLE_NAME` is set on every retrieval-serving service before deploying, and watch `KbStatusFilterFailClosed`. A service missing it will return zero chunks where it previously returned unfiltered ones. +6. **Retrieval queries clamp at 10,000 characters** on both backends. Watch `KbQueryClamped`. +7. **The reconciler runs daily from day one, with every flag off.** Its rule is enabled in report-only mode, and four alarms are created in namespace `${projectPrefix}/ManagedKb`. Expect Lambda invocations, log volume and alarm state. Read its "would have deleted" logs before setting `CDK_MANAGED_KB_RECONCILER_ARMED`. +8. **Teardown order changed.** `destroy.sh` runs `managed-kb.sh` as Phase 0 and exits 1 before deleting any stack if it cannot confirm every matched knowledge base is gone. Scripted teardowns should expect that new failure mode. +9. **New listing state `rejected`.** Any external consumer enumerating marketplace listing statuses needs updating. There is no edge from `rejected` to `published`. +10. **Tell marketplace reviewers that a RAG-backed test drive has an empty knowledge base** โ€” fail-closed by design, easily misread as a broken agent. +11. **The nine `CDK_MANAGED_KB_*` variables are all safe to leave unset.** Unset is the shipped dormant state, and an empty value cannot arm the three behavioural flags. + +--- + # Release Notes โ€” v1.15.0 **Release Date:** August 17, 2026 diff --git a/VERSION b/VERSION index 141f2e80..15b989e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.0 +1.16.0 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 610bd436..4d96813d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-stack" -version = "1.15.0" +version = "1.16.0" requires-python = ">=3.10" description = "Multi-agent conversational AI system with AWS Bedrock AgentCore" readme = "README.md" diff --git a/backend/uv.lock b/backend/uv.lock index 7b769de8..761c68e5 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -12,7 +12,7 @@ resolution-markers = [ [[package]] name = "agentcore-stack" -version = "1.15.0" +version = "1.16.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, diff --git a/frontend/ai.client/package-lock.json b/frontend/ai.client/package-lock.json index 6dad8e1e..81a38e91 100644 --- a/frontend/ai.client/package-lock.json +++ b/frontend/ai.client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai.client", - "version": "1.15.0", + "version": "1.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai.client", - "version": "1.15.0", + "version": "1.16.0", "dependencies": { "@angular/cdk": "21.2.14", "@angular/common": "21.2.17", diff --git a/frontend/ai.client/package.json b/frontend/ai.client/package.json index e68f93c2..fd819be5 100644 --- a/frontend/ai.client/package.json +++ b/frontend/ai.client/package.json @@ -1,6 +1,6 @@ { "name": "ai.client", - "version": "1.15.0", + "version": "1.16.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json index 9adb6da2..193370b5 100644 --- a/infrastructure/package-lock.json +++ b/infrastructure/package-lock.json @@ -1,12 +1,12 @@ { "name": "infrastructure", - "version": "1.15.0", + "version": "1.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "infrastructure", - "version": "1.15.0", + "version": "1.16.0", "dependencies": { "aws-cdk-lib": "2.262.0", "constructs": "10.6.0" diff --git a/infrastructure/package.json b/infrastructure/package.json index 039c336a..a5b05450 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -1,6 +1,6 @@ { "name": "infrastructure", - "version": "1.15.0", + "version": "1.16.0", "bin": { "infrastructure": "bin/infrastructure.js" }, diff --git a/tui/pyproject.toml b/tui/pyproject.toml index 7fb7456a..e857dfeb 100644 --- a/tui/pyproject.toml +++ b/tui/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentcore-tui" -version = "1.15.0" +version = "1.16.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 6b73aa75..7269f922 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.15.0" +__version__ = "1.16.0" __all__ = ["__version__"] diff --git a/tui/uv.lock b/tui/uv.lock index 0b8e493c..0b07c0e1 100644 --- a/tui/uv.lock +++ b/tui/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agentcore-tui" -version = "1.15.0" +version = "1.16.0" source = { editable = "." } dependencies = [ { name = "httpx" },