diff --git a/.claude/skills/kaizen-research/SKILL.md b/.claude/skills/kaizen-research/SKILL.md index 2e5b6bceb..e05314be9 100644 --- a/.claude/skills/kaizen-research/SKILL.md +++ b/.claude/skills/kaizen-research/SKILL.md @@ -1,6 +1,6 @@ --- name: kaizen-research -description: Weekly Friday early-morning external + internal scan for emerging functionality, agentic trends, tools, and feature/UX improvements in the AgentCore Public Stack repo. Tracks AWS Bedrock + AgentCore announcements, Strands Agents releases, FastMCP (used by externally hosted MCP servers), the aws-samples/sample-strands-agent-with-agentcore reference repo, the MCP ecosystem (including MCP Apps + extensions), frontier model announcements, agent-harness patterns (including opencode (anomalyco/opencode) as an open-source coding-agent harness reference scanned through tooling, cost-effectiveness, and context-engineering lenses — releases-first, light touch), agentic UI/UX patterns (MCP Apps, Vercel AI SDK, assistant-ui, NN/g AI research, Linear/Cursor/Anthropic product blogs), and LibreChat as a parallel open-source agentic-platform reference (releases-first, light touch). Audits internal signals (recent commits, open PRs, CI failures, version-pin lag, dormant skills). Outputs a dated research doc + queues ideas in `docs/kaizen/review-queue.md` for that same morning's `kaizen-review-prep` (runs ~2 hours later) to rank into decisions. Opens a PR into `develop`. **Out of scope**: security advisories / Dependabot / CodeQL — those have dedicated tooling and don't need a weekly kaizen lens. Triggers: "kaizen research", "weekly research scan", "external scan", "what should we look at this week". +description: Weekly Friday early-morning external + internal scan for emerging functionality, agentic trends, tools, and feature/UX improvements in the AgentCore Public Stack repo. Tracks AWS Bedrock + AgentCore announcements, Strands Agents releases, FastMCP (used by externally hosted MCP servers), the aws-samples/sample-strands-agent-with-agentcore reference repo, the MCP ecosystem (including MCP Apps + extensions), frontier model announcements, agent-harness patterns (including opencode (anomalyco/opencode) as an open-source coding-agent harness reference scanned through tooling, cost-effectiveness, and context-engineering lenses — releases-first, light touch), agentic UI/UX patterns (MCP Apps, Vercel AI SDK, assistant-ui, NN/g AI research, Linear/Cursor/Anthropic product blogs), and LibreChat as a parallel open-source agentic-platform reference (releases-first, light touch). Audits internal signals (recent commits, open PRs, CI failures, version-pin lag, dormant skills). Outputs a dated research doc + queues ideas in `docs/kaizen/review-queue.md` for that same morning's `kaizen-review-prep` (runs ~4 hours later) to rank into decisions. Opens a PR into `develop`. **Out of scope**: security advisories / Dependabot / CodeQL — those have dedicated tooling and don't need a weekly kaizen lens. Triggers: "kaizen research", "weekly research scan", "external scan", "what should we look at this week". --- # Kaizen Research @@ -18,7 +18,7 @@ Friday early morning. The "what's the rest of the world learning that we should ## When to run -Friday early morning (~6am MT). `kaizen-review-prep` runs ~2 hours later (~8am MT) so both docs are waiting when Phil sits down Friday morning. Phil reviews, picks 1–3 to ship over the coming week, and POCs additional items over the weekend. Last weekend's POC findings surface in *this* run's review-prep as Carried Over items (lifted from comments on the previous week's research PR). +Friday early morning (~6am MT). `kaizen-review-prep` runs ~4 hours later (~10am MT), a gap sized to this scan's own runtime (2h18m on 2026-09-11) rather than to the 2 hours the two skills used to claim — on 2026-09-18 the prep ran 18 minutes after this scan started and produced a review with no research doc. Both docs are waiting when Phil sits down Friday late morning; he reviews, picks 1–3 to ship over the coming week, and the *outcomes* of those picks are what review-prep reads the following Friday. ## Sources @@ -436,8 +436,7 @@ gh pr create --base develop --head "$BRANCH" \ ## Review - Read the research doc. -- Comment on the PR with reactions and any weekend POC findings — these become first-class signal for *next* Friday's `kaizen-review-prep`. -- POC promising ideas over the weekend. +- Ship, decline or defer individual ideas in their own PRs — a merged PR is the signal `kaizen-review-prep` reads next Friday. Deep analysis belongs in a `docs/kaizen/review-queue.md` entry on the PR branch that produced it, where the next review will find it. ## Decision Ship the doc to `develop`. Ranking into decisions happens in the kaizen-review-prep PR opened later this morning. Action on individual ideas happens in separate PRs the following week. diff --git a/.claude/skills/kaizen-review-prep/SKILL.md b/.claude/skills/kaizen-review-prep/SKILL.md index a986b46f9..42a3b3656 100644 --- a/.claude/skills/kaizen-review-prep/SKILL.md +++ b/.claude/skills/kaizen-review-prep/SKILL.md @@ -1,11 +1,11 @@ --- name: kaizen-review-prep -description: Friday late-morning synthesis. Runs ~2 hours after `kaizen-research` the same morning. Consumes this week's research doc, open items in `docs/kaizen/review-queue.md`, last weekend's POC findings (from comments on the previous week's research PR), and recent merges/reverts/CI signal — produces a ranked, decision-oriented agenda. Every item has a Ship / Decline / Defer recommendation. Opens a PR into `develop`. Triggers: "kaizen review prep", "weekly review prep", "friday review", "rank kaizen ideas". +description: Friday late-morning synthesis. Runs ~4 hours after `kaizen-research` the same morning. Consumes this week's research doc, open items in `docs/kaizen/review-queue.md`, the outcomes of the prior review's proposals (read from merged PRs and from open PR branches), and recent merges/reverts/CI signal — produces a ranked, decision-oriented agenda. Every item has a Ship / Decline / Defer recommendation. Opens a PR into `develop`. Triggers: "kaizen review prep", "weekly review prep", "friday review", "rank kaizen ideas". --- # Kaizen Review Prep -Friday late morning, after `kaizen-research` ran earlier the same morning. This skill consolidates this week's research + open queue items + last weekend's POC findings (lifted from PR comments on the previous week's research PR) + recent repo state into a ranked decision agenda. Phil reviews Friday morning, marks ✅/❌/⏸ on each item, ships 1–3 the following week, and POCs the next batch over the weekend. +Friday late morning, after `kaizen-research` ran earlier the same morning. This skill consolidates this week's research + open queue items + the outcomes of the *prior* review's proposals + recent repo state into a ranked decision agenda. Phil reviews Friday morning, marks ✅/❌/⏸ on each item, and ships 1–3 the following week. What those 1–3 did is the feedback signal the next run opens with. ## Philosophy @@ -13,19 +13,22 @@ Friday late morning, after `kaizen-research` ran earlier the same morning. This - **Subtraction first.** Every proposal ranks against "do nothing" and "retire something instead." If a proposal adds anything, it must explain what existing thing it either replaces or simplifies. - **Dual lens — impact + capability-unlock.** Rank proposals through *two* lenses, not one: (a) **impact on existing code** (does this change, simplify, or obsolete something we already have?) and (b) **capability unlock** (what *new* product capability or UX enhancement does this enable that we couldn't easily build before?). Subtraction-first applies to lens (a). But proposals that genuinely unlock new product surface — code-interpreter sandboxes, persistent agent state, multi-agent UI attribution, new SSE event types that enable inline UI, etc. — must be evaluated on their strategic merit, *not* auto-deferred because they don't intersect existing code. A proposal with no `Subtracts` value but a substantive `Unlocks` value can rank above a low-impact dep-bump. Don't penalize net-new capability for not being a cleanup. - **Multiple cycles.** Kaizen is small changes, weekly, compounding. If this week's review touches 3 things, next week's will touch 3 different things. Phil doesn't need a grand plan — he needs a reliable weekly cadence. -- **One-week feedback lag is intentional.** Phil reviews Friday → POCs over the weekend → those POC findings surface in the *next* Friday's review-prep as Carried Over items. Don't try to fold same-day POC findings in — they don't exist yet. +- **Outcomes are the feedback channel, not comments.** Open every run with a scorecard of the prior review's proposals: which shipped, which didn't, and what the merged PRs found. Shipping a proposal is the only reaction that has ever been recorded — see `decisions.md` [2026-09-21]. Do not ask for, wait on, or rank by PR comments. +- **Read open PR branches, not just `develop`.** This repo's deepest analysis is written as `docs/kaizen/review-queue.md` entries *inside the feature PR that produced it*. A review that reads only `develop` misses them, re-derives them worse, and gets corrected by a branch that was sitting there the whole time. Before ranking, `gh pr list --state open` and diff each against `develop` for `docs/kaizen/` and any spec the review cites. Those entries are authoritative over anything re-derived; cite them rather than restating them, and flag the merge order when two PRs touch the top of `## Open`. - **No edits outside `docs/kaizen/`.** This skill writes one Markdown file under `docs/kaizen/reviews/` and updates `docs/kaizen/review-queue.md` (moves Open → Resolved post-review). It never touches source code, `CLAUDE.md`, or skill files. Those changes happen in separate PRs after the review. ## When to run -Friday late morning (~8am MT), ~2 hours after `kaizen-research` runs. Phil reviews both docs Friday morning, picks 1–3 to ship over the coming week, and POCs additional items over the weekend. POC findings from last weekend's POC session surface here as Carried Over items (lifted from PR comments on the *previous* week's research PR — not this week's, which Phil hasn't seen yet). +Friday late morning (~10am MT), ~4 hours after `kaizen-research` runs. Phil reviews both docs Friday, picks 1–3 to ship over the coming week; what those picks did is the scorecard this skill opens with the following Friday. + +**Preflight — check that this morning's research doc exists before doing anything else.** The gap is sized to the scan's runtime (2h18m on 2026-09-11), not guaranteed by it. If `docs/kaizen/research/.md` is absent or the `kaizen-research` run is still in flight, say so **in the review doc's header** and proceed queue-first — the `## Open` entries are the primary source and research is one input. Degrading is fine; degrading silently is not. This failed on 2026-09-18, when prep ran 18 minutes after the scan started. ## Inputs 1. **Most recent `docs/kaizen/research/YYYY-MM-DD.md`** — Friday's scan. Its Top 5 ideas are the primary candidate list. 2. **`docs/kaizen/review-queue.md`** — `## Open` entries. Includes both this week's ideas (just appended by `kaizen-research`) and any prior-week items that weren't resolved. 3. **Last 1–2 `docs/kaizen/reviews/*.md`** — what was proposed before, what was decided, anything deferred to "revisit by [date]". -4. **PR comments on the *previous* week's kaizen-research PR.** `gh pr view --comments` — Phil's reactions and weekend POC findings are first-class signal. The PR opened *this* morning by `kaizen-research` is too fresh; comments accumulate over the week as Phil POCs ideas. Pick the research PR from one week ago (or the most recent merged/closed kaizen-research PR), not today's. +4. **Outcomes of the prior review's proposals.** For each proposal in the most recent `docs/kaizen/reviews/*.md`, establish on disk or in git what happened: shipped (name the commit/PR), not started, or superseded. This is the feedback channel — see the Philosophy bullet. Include **open PR branches**, which routinely carry queue entries that refute or narrow what `develop` alone would suggest. 5. **`docs/kaizen/decisions.md`** (if it exists) — declined items with reasons. Don't re-propose without materially new context. 6. **Recent activity since last review:** - `git log develop --since="" --oneline --no-merges` — what shipped. @@ -78,7 +81,7 @@ check before decisions.] - **Unlocks** (if applicable): [net-new product capability, UX pattern, or enhancement this enables — bulleted if multiple. Required for proposals where `Subtracts: no — addition only`; the unlock is the justification. Omit when purely a cleanup/dep-bump and not applicable.] - **Effort**: Low / Med / High - **Impact**: Low / Med / High -- **POC findings (if Phil tried it)**: [summary or "not POCed"] +- **Evidence**: [measured — cite the number and where it came from | verified on disk — cite file:line | asserted — say so plainly] - **Ship means**: [specific action — "open PR updating X to do Y" or "retire skill Z"] - **Decline means**: [what happens instead — usually "keep current behavior, revisit in N weeks"] - **Recommendation**: Ship / Decline / Defer N weeks — [one-sentence why] @@ -158,12 +161,12 @@ After Phil reviews and the decisions are logged in the review doc, this skill (o - Last ~14 days of `CHANGELOG.md` and `RELEASE_NOTES.md` - `CLAUDE.md` (read-only — for context, not edits) -3. **Pull PR comments on the latest research PR** (parallel with step 4): +3. **Score the prior review's proposals, and read open PR branches** (parallel with step 4): ``` - gh pr list --base develop --state all --search "kaizen/research" --limit 1 --json number,url - gh pr view --comments + gh pr list --state open --json number,title,headRefName + git fetch origin && git diff origin/develop...origin/ -- docs/kaizen/ ``` - Capture Phil's reactions. POC findings he mentions get folded into proposal entries. + For each proposal in the last review doc, resolve shipped / not started / superseded against git and disk — that scorecard opens the new review. Queue entries found on open branches are authoritative over anything re-derived here. 4. **Pull recent activity** (parallel Bash): - `git log develop --since="" --oneline --no-merges` @@ -183,7 +186,7 @@ After Phil reviews and the decisions are logged in the review doc, this skill (o - Low-effort × High-impact first. - **Retirement candidates** get a +1 boost (subtraction bias). - **Capability-unlock items** (proposals with a substantive `Unlocks` field — new product capability, UX surface, or platform primitive adoption) rank on their strategic merit. Do not auto-defer just because `Subtracts: no`. A High-impact unlock can rank above a Low-impact subtraction. - - Items with **POC findings** rank above untested items at the same effort/impact. + - Items with **measured evidence** rank above asserted ones at the same effort/impact. 8. **Cap the proposal count at 10.** If more than 10 candidates, defer the lowest-ranked to next week with a note. The review is supposed to take 10-15 minutes, not be exhaustive. @@ -213,7 +216,7 @@ gh pr create --base develop --head "$BRANCH" \ - N proposals ranked Effort × Impact (retirement candidates boosted). - Friction patterns from the week's commits, PRs, and CI surfaced. - Carried-over deferred items now due for re-decision. -- POC findings (from kaizen-research PR comments) folded into proposals where Phil tried something. +- Scorecard of the prior review's proposals: what shipped, what didn't, and what the merged PRs found. ## Review 1. Read Friction (2 min). diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index bc27be554..79db3840b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -8,6 +8,10 @@ on: paths: - 'backend/**' - 'scripts/build/**' + # recover-evicted-deploy.sh runs in THIS workflow. A change to the + # guard that re-dispatches an evicted peer deploy must itself be + # exercised by a real run. + - 'scripts/ci/**' - '.github/workflows/backend.yml' permissions: @@ -19,7 +23,13 @@ env: # Shared "deploy-" group with platform.yml so a CFN deploy and these # API-driven code deploys never mutate the same ECS service / AgentCore -# Runtime / Lambda concurrently — they queue instead. +# Runtime / Lambda concurrently. +# +# ⚠️ The group does NOT queue — it is a latch with a queue depth of one, so +# a third run entering it cancels whichever run was pending, before that run +# executes a job. See the longer note in platform.yml and the +# `recover-evicted-peer` job below, which re-dispatches a peer deploy this +# run evicted. Enforced by backend/tests/supply_chain/test_concurrency_config.py. concurrency: group: deploy-${{ github.ref }} cancel-in-progress: false @@ -40,6 +50,45 @@ concurrency: # ------------------------------------------------------------ jobs: + # Recover a peer deploy that GitHub silently evicted from the shared + # "deploy-" concurrency group when THIS run claimed the pending slot. + # A concurrency group is a latch with a queue depth of ONE, not a queue: + # "any existing pending job or workflow in the same concurrency group will + # be canceled and the new queued job or workflow will take its place." So a + # third run entering an occupied group kills whichever run was waiting, + # before it executes a single job, leaving a `cancelled` conclusion that is + # indistinguishable from a human pressing Cancel. + # + # That is harmless when the evicted run is superseded by a LATER run of the + # SAME workflow (both deploy the tree at HEAD, and git is cumulative). It is + # destructive across workflows: a Platform Stack run does not ship + # container images or Lambda code, so when it evicts a pending Backend + # Deploy run that code is simply never shipped and CI stays green. + # + # No `needs`, so the recovery dispatch goes out at the top of the run rather + # than behind the deploy it is unrelated to. The re-dispatched run queues + # behind this one, so the concurrency guarantee is preserved. + # See scripts/ci/recover-evicted-deploy.sh. + # Enforced by backend/tests/supply_chain/test_concurrency_config.py. + recover-evicted-peer: + name: Recover evicted peer deploy + runs-on: ubuntu-24.04 + + permissions: + contents: read + # `gh workflow run` re-dispatches the peer workflow. + actions: write + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Detect and recover an evicted Platform Stack run + env: + GH_TOKEN: ${{ github.token }} + run: bash scripts/ci/recover-evicted-deploy.sh platform.yml + test-infra: name: Test infrastructure (jest) permissions: diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index d650dc8ce..038e32ec0 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -19,6 +19,10 @@ on: - 'scripts/platform/**' # fetch-dcv-sdk.sh runs in THIS workflow, before synth. - 'scripts/build/**' + # recover-evicted-deploy.sh runs in THIS workflow. A change to the + # guard that re-dispatches an evicted peer deploy must itself be + # exercised by a real run. + - 'scripts/ci/**' - '.github/workflows/platform.yml' permissions: @@ -30,15 +34,68 @@ env: # Shared "deploy-" group with backend.yml so a CFN deploy and the # API-driven backend code deploys never mutate the same ECS service / -# AgentCore Runtime / Lambda concurrently — they queue instead. +# AgentCore Runtime / Lambda concurrently. # cancel-in-progress: false avoids leaving CloudFormation in # ROLLBACK_IN_PROGRESS / UPDATE_ROLLBACK_FAILED after a mid-deploy cancel. -# Enforced by tests/supply_chain/test_concurrency_config.py. +# +# ⚠️ This group does NOT queue. It is a latch with a queue depth of one: +# "any existing pending job or workflow in the same concurrency group will +# be canceled and the new queued job or workflow will take its place." A +# third run entering an occupied group therefore KILLS the run that was +# waiting, with zero jobs executed and a `cancelled` conclusion that looks +# exactly like a human cancel. One merge is enough — a commit touching both +# infrastructure/ and backend/ starts two runs that compete with each other +# and with the previous merge's still-running deploy. +# +# Losing a pending run is harmless when a LATER run of the SAME workflow +# supersedes it, and silently skips a CDK deploy when the evictor is +# backend.yml. The `recover-evicted-peer` job below closes that gap; do not +# remove it while this group is shared. +# Enforced by backend/tests/supply_chain/test_concurrency_config.py. concurrency: group: deploy-${{ github.ref }} cancel-in-progress: false jobs: + # Recover a peer deploy that GitHub silently evicted from the shared + # "deploy-" concurrency group when THIS run claimed the pending slot. + # A concurrency group is a latch with a queue depth of ONE, not a queue: + # "any existing pending job or workflow in the same concurrency group will + # be canceled and the new queued job or workflow will take its place." So a + # third run entering an occupied group kills whichever run was waiting, + # before it executes a single job, leaving a `cancelled` conclusion that is + # indistinguishable from a human pressing Cancel. + # + # That is harmless when the evicted run is superseded by a LATER run of the + # SAME workflow (both deploy the tree at HEAD, and git is cumulative). It is + # destructive across workflows: a Backend Deploy run does not run + # `cdk deploy`, so when it evicts a pending Platform Stack run the + # infrastructure change is simply never applied and CI stays green. + # + # No `needs`, so the recovery dispatch goes out at the top of the run rather + # than behind the deploy it is unrelated to. The re-dispatched run queues + # behind this one, so the concurrency guarantee is preserved. + # See scripts/ci/recover-evicted-deploy.sh. + # Enforced by backend/tests/supply_chain/test_concurrency_config.py. + recover-evicted-peer: + name: Recover evicted peer deploy + runs-on: ubuntu-24.04 + + permissions: + contents: read + # `gh workflow run` re-dispatches the peer workflow. + actions: write + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Detect and recover an evicted Backend Deploy run + env: + GH_TOKEN: ${{ github.token }} + run: bash scripts/ci/recover-evicted-deploy.sh backend.yml + test-infra: name: Test infrastructure (jest) permissions: @@ -119,6 +176,11 @@ jobs: CDK_COGNITO_CALLBACK_URLS: ${{ vars.CDK_COGNITO_CALLBACK_URLS }} CDK_COGNITO_LOGOUT_URLS: ${{ vars.CDK_COGNITO_LOGOUT_URLS }} CDK_COGNITO_SUPPORTED_IDPS: ${{ vars.CDK_COGNITO_SUPPORTED_IDPS }} + # Public "Sign up" link on the Cognito Hosted UI. Unset/empty means + # CLOSED — users arrive via a federated IdP or an administrator creates + # them. Set to "true" only for an environment that wants open public + # registration. Federated sign-in works either way. + CDK_COGNITO_SELF_SIGNUP_ENABLED: ${{ vars.CDK_COGNITO_SELF_SIGNUP_ENABLED }} CDK_ARTIFACTS_CERTIFICATE_ARN: ${{ vars.CDK_ARTIFACTS_CERTIFICATE_ARN }} # Extra origins allowed to frame the artifacts iframe via CSP # frame-ancestors (beyond the deployed SPA origin) — e.g. @@ -205,6 +267,13 @@ jobs: # `CDK_AGENTS_API_ENABLED` variable to "true" in an environment (e.g. dev) # to enable it there for dogfooding. CDK_AGENTS_API_ENABLED: ${{ vars.CDK_AGENTS_API_ENABLED }} + # Platform cost sync from AWS Cost Explorer (docs/specs/platform-cost-visibility.md). + # Default OFF (opt-in): only the literal "true" enables it. Unset resolves + # to empty string, which config.ts treats as "not set" and falls back to + # the `platformCosts.enabled` cdk.json context (itself default off). Set + # the `CDK_PLATFORM_COSTS_ENABLED` variable to "true" in an environment to + # deploy the daily sync Lambda there. + CDK_PLATFORM_COSTS_ENABLED: ${{ vars.CDK_PLATFORM_COSTS_ENABLED }} # MCP user-identity forwarding — optional Cognito Pre-Token-Generation v2 # Lambda that copies user attributes into access-token claims so # personalized MCP tools can identify the caller diff --git a/CHANGELOG.md b/CHANGELOG.md index 42aace095..91cdcbcf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,67 @@ 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.24.0] - 2026-09-22 + +Admins get a say over the toolset, and everyone gets a clearer view of what things cost. **Admin-managed always-on tools** pin a tool into every granted user's turn, enforced server-side and shown as locked in the picker, with no effect on deploy until an admin flags a tool. **Tool retirement** gains a staged runbook, one-way picker guards, and a replacement note plus stop date shown on five surfaces. The cost surfaces fill in: users now see their **quota limit** next to their spend, the session cost badge **counts up** each turn, and the admin cost dashboard is **tabbed** and can report **all-in platform cost from the AWS bill** (opt-in). **Unsent composer state is kept per conversation**, agents gain **per-agent citation and download controls**, and **Kimi K3** joins the catalog. Two fixes matter more than their size: the **tool-result offloader shipped in 1.23.0 had never run** and now does, and **Cognito self-signup now defaults to closed**. **A CDK deploy is required**, and in any environment that has not set `CDK_COGNITO_SELF_SIGNUP_ENABLED` this deploy closes public self-registration. + +### 🚀 Added + +- **Admin-managed always-on tools** — a catalog flag (`alwaysOn`, per tool or per `MCPToolEntry`) that pins a tool into every turn for users whose roles grant it. Enforced server-side at the same seam as attachment auto-enable, so the main turn, MCP App dispatch and voice compute the same toolset; it enables and never grants. An Agent that binds its own toolset is exempt; an unbound Agent is not. Gated by `ADMIN_ALWAYS_ON_TOOLS_ENABLED` (default on with a kill switch) (#1215, #1217, #1220, #1223, #1227, #1229, #1231) +- **Locked always-on tools in the user's tool picker** — `GET /tools` carries the effective lock; a stored opt-out is kept but not honoured while the pin is in force, and a preference `PUT` that contradicts a pin is normalized rather than rejected (#1226) +- **MCP server retirement** — a staged runbook (`docs/specs/mcp-server-retirement.md`) and picker guards on six surfaces: a retiring tool can be turned off but not on, and nothing is filtered out of any list (#1230) +- **Retirement replacement and stop date** — `retirementNote` (≤300 chars) and `retiresOn` (validated ISO date) on the tool record, rendered as one shared sentence across the Agent Designer, Customize → Tools, the tool detail page and the scheduled-run form. Display-only; neither reaches `toolConfig` (#1237) +- **Quota limit shown with usage** — read-only `GET /costs/quota-status` (caller-scoped, records no enforcement events), a quota progress bar on the Usage page for every period, and a quota tooltip on the composer cost counter (#1210) +- **All-in platform cost on the admin dashboard** — a daily Cost Explorer sync writes `PLATFORM#*` rows into the existing system cost rollup table, scoped to this deployment by the `Project` tag, falling back to account scope and labelling it as such. Opt-in via the `CDK_PLATFORM_COSTS_ENABLED` GitHub variable or `platformCosts.enabled` context (#1235, #1245) +- **Per-agent source citations and document download** — `showCitations` and `allowDocumentDownload` on the agent record, both default `true` with no migration. Citations off suppresses the SSE event and its persistence; the download endpoint 403s unless both are on (#1240) +- **Kimi K3** curated on `bedrock-responses`, with explicit prompt caching required per model — its default implicit caching measured 25% more expensive than no caching at all (#1212) +- **Unsent composer state kept per conversation** — text, the bound `@`-mention, unarmed queued follow-ups and attachments are saved in `localStorage` per conversation and restored on return; attachments reconcile against the server before they are trusted. Cleared on sign-out and wiped at sign-in when the owner differs (#1228) + +### ✨ Improved + +- **Tabbed admin cost analytics** — Model Usage, Cost Trends, Top Users and Conversations each get the full console width; the period KPIs stay pinned above the tabs (#1235) +- **The admin console takes over the sidenav** — admin navigation replaces the chat list in the sidebar, and admin pages drop the `max-w-7xl` cap, driven by `data.chrome: 'admin'` on the route (#1232) +- **Agent Composer tools as a searchable, category-grouped list** instead of a chip cloud; descriptions visible, per-tool scoping nested under its own row (#1233) +- **Session cost badge counts up** to each turn's new total; reduced motion snaps (#1234) +- **Quota warning and cost badge share one row** above the composer, and dismissing a warning holds until a higher threshold fires, across reloads and new conversations (#1243) +- **Model-family logos** (Claude, Kimi, Qwen) preferred over company marks, matched on `modelId` (#1238) +- **Context window reconciled against Strands' model table** — the catalog value wins, Strands fills an absence, and a disagreement is logged. Both the context badge and the compaction policy now read one resolver (#1221) + +### 🐛 Fixed + +- **The tool-result offloader never offloaded anything.** An undecorated override hid Strands' `@hook` registration, so `BoundedToolResultOffloader` published `retrieve_offloaded_content` every turn but never saw a tool result. One production session carried a 107,802-token calendar result for 15 calls, about 75% of its cost (#1239) +- **Browsing failed permanently after a sign-in handback** — the service closes the CDP socket when the automation stream is disabled, and the pool kept returning the dead socket. It now reconnects to the session the user signed into (#1208) +- **Managed-KB retrieval returned duplicate content** to both the model and the citation cards; repeated sentences within a chunk and near-duplicate chunks are now removed at answer time (#1241) +- **Managed-KB citations read "Unknown Source"** — the builder now reads `metadata["filename"]` before `source` (#1240) +- **Attaching an image or spreadsheet injected `document_read`** and re-wrote the prompt-cache prefix, sometimes flapping per microVM. The gate now classifies the turn's uploads (#1218) +- **Broken thumbnails for files with non-ASCII names** (e.g. macOS screenshots) — S3 rejected the `response-content-disposition`. A shared RFC 6266/5987 header builder now serves three call sites (#1214) +- **Changing a quota assignment's tier returned 400** (#1225) +- **Context attribution reported unstable tool token counts** on `bedrock-responses`/mantle models, where token counting is a heuristic; the split is now withheld there (#1216) +- **`prefixTokens` splits larger than the prompt they sit inside** are dropped rather than rendered (#1222) +- **A partial tool update could persist an incoherent `alwaysOn`/`enabledByDefault` pair** — patched state is now re-validated before persisting (#1231) +- **`asyncio.get_event_loop()` raised on Python 3.12** in the external-MCP tool build when no loop was running (#1213) + +### 🔒 Security + +- **Cognito self-signup defaults to closed**, set by `CDK_COGNITO_SELF_SIGNUP_ENABLED` (only an explicit `true` opens it). The runtime `disable_self_signup()` call is removed: CDK re-rendered the setting on every deploy, so it never held. First-boot (`AdminCreateUser`) and federated sign-in are unaffected (#1211) + +### 🏗️ Infrastructure + +- **New `PlatformCostSyncConstruct`** — Lambda + daily EventBridge rule (07:10 UTC) with `ce:GetCostAndUsage` / `ce:GetDimensionValues`, created only when `platformCosts.enabled` is true (#1235) +- **app-api ECS tasks now receive the service's tags** (`propagateTags: SERVICE`), so Fargate appears in tag-scoped cost queries (#1235) +- **Cognito user pool `selfSignUpEnabled` read from config** (#1211) + +### 🔧 CI/CD + +- **Deploy runs pushed out of the shared concurrency group are re-dispatched** — a `recover-evicted-peer` job in `platform.yml` and `backend.yml` finds a peer run cancelled before it ran any jobs and dispatches it again (#1224) +- **CSP tests** now cover the `/api/*` edge policy's `sandbox` omission and the artifact CSP parity between the edge and the render Lambda (#1219) +- **`platform.yml` forwards `CDK_PLATFORM_COSTS_ENABLED`** — the variable `config.ts` reads for platform cost sync was never passed to the CDK deploy, so setting it in GitHub had no effect (#1245) + +### 📚 Docs + +- **Shared Projects implementation plan** (`docs/specs/shared-projects.md`) — spec only (#1242) +- **Kaizen**: POC comment loop retired, quality vetoes waived with written triggers (#1221) + ## [1.23.0] - 2026-09-20 The agent stops working in silence, and stops paying to re-read what it already knows. **Live turn narration** replaces the cycling placeholder with what the agent is actually doing — thinking, which tool is running, how long it took, and a one-line model-written summary of each tool batch — drained concurrently so "Using list_assignments" arrives *while* that tool runs rather than after it. **Document context offload** stops an attached PDF being re-sent on every turn: a digest replaces the bytes and a `document_read` tool pulls back the pages the model actually asks for, measured on a 60-page canary at a **109.1K → ~15K prefix drop**. A five-part **compaction overhaul** makes thresholds model-relative, bounds the summary at 8k tokens, parks cuts until the re-write is free, and offloads oversized tool results at intake. **Browser sign-in handover** lets the agent pause and hand the user a live, interactive browser to sign in to a site it cannot reach, then continue in the authenticated session — gated behind its own RBAC tool id and a MANAGED Chromium URL blocklist. **Response feedback** ships thumbs, retry-with-correction, implicit copy/continue signals and fleet-level attribution by config arm. Plus **.docx / .pptx / .csv / .xlsx previews** in a docked pane, **Agent Templates**, and a performance pass worth ~500ms off the pre-stream window. **A CDK deploy is required**, and operators who use the browser tool must set `CDK_BROWSER_URL_BLOCKLIST`. diff --git a/CLAUDE.MD b/CLAUDE.MD index 409f4ac71..cbc9a3a75 100644 --- a/CLAUDE.MD +++ b/CLAUDE.MD @@ -39,7 +39,7 @@ npx cdk deploy {prefix}-PlatformStack - **Admin endpoints** go under `/admin//`, user-facing under `//` - **Errors stream as assistant messages** via SSE (not HTTP error codes) - **Signal-based state** throughout frontend (`signal()`, `computed()`) -- **Prompt-cache stability is a contract.** Bedrock prompt caching is exact-prefix-match: any list that reaches the system prompt or `toolConfig` (skills, tools, models, MCP tool listings) must be deterministically ordered at its source, and restored conversation history must be byte-stable between compaction-state changes (see the truncation anchor in `TurnBasedSessionManager`). An order flip or history mutation between turns silently re-writes a 30k–150k-token prefix at the cache-write premium, which is **1.25× the model's own base input rate** (2× at 1h TTL; a cache read is 0.1×) — there is no flat per-MTok figure, so price it against the model actually in play: $1.375/MTok on our default Haiku 4.5, $4.125 on Sonnet 4.6. Our `us.*` ids are **Regional (CRIS)** inference profiles and price ~10% above `global.*` for the same model; rates live in `curated-models.ts`, and which source is authoritative depends on the vendor: per-model AWS **model cards** are authoritative for Claude, Nova and the OpenAI/Mantle family; the **Price List API** is authoritative for xAI, Google and AgentCore. Do not re-derive Claude rates from the Price List API — a full enumeration of the `AmazonBedrock` offer file returns 10 Claude SKUs, **none newer than Claude 3**, and no us-west-2 SKU at all for Haiku 4.5, Sonnet 4.6, Fable 5.1, GPT-5.4 or Nova Micro (verified twice, a week apart). The 1.100× Regional premium *does* reproduce there, on the vendors the API does carry. `PROMPT_CACHE_OBSERVABILITY_ENABLED=false` disables the observability layer (fingerprint hook, cacheStatus derivation, EMF metrics) — the caching itself stays on +- **Prompt-cache stability is a contract.** Bedrock prompt caching is exact-prefix-match: any list that reaches the system prompt or `toolConfig` (skills, tools, models, MCP tool listings) must be deterministically ordered at its source, and restored conversation history must be byte-stable between compaction-state changes (see the truncation anchor in `TurnBasedSessionManager`). An order flip or history mutation between turns silently re-writes a 30k–150k-token prefix at the cache-write premium, which is **1.25× the model's own base input rate** (2× at 1h TTL; a cache read is 0.1×) — there is no flat per-MTok figure, so price it against the model actually in play: $1.375/MTok on our default Haiku 4.5, $4.125 on Sonnet 4.6. Our `us.*` ids are **Regional (CRIS)** inference profiles and price ~10% above `global.*` for the same model; rates live in `curated-models.ts`. Per-model AWS **model cards** stay the primary source — they carry context windows, caching support, parameter tables and cutoffs, which no pricing API does — but the **Price List API now corroborates almost all of them**, and a rate worth trusting should agree in both. **Which offer file matters more than which vendor:** `--service-code AmazonBedrockFoundationModels` (372 us-west-2 SKUs) carries Claude, Cohere, Palmyra, TwelveLabs, Luma, Stability; `--service-code AmazonBedrock` (1052) carries Nova, xAI, Google, DeepSeek, Qwen and Moonshot. Querying the wrong one returns nothing and reads exactly like an unpublished model — which is how an earlier revision of this note concluded the API held "10 Claude SKUs, none newer than Claude 3." **That is no longer true** (re-verified 2026-09-21): every Claude model we curate is present and matches its card to the cent — Haiku 4.5 $1.10/$5.50 Regional and $1.00/$5.00 Global, Sonnet 4.6 $3.30/$16.50, Opus 4.7 $5.50/$27.50, Sonnet 5 $2.00/$10.00 Global, Fable 5.1 $10/$50 Global with a cache read of $0.25 that independently confirms its 0.025× multiplier. The 1.100× Regional premium reproduces throughout. **Two traps remain.** The `model` attribute is gone from the product schema — match on `servicename` (FoundationModels) or `usagetype` (AmazonBedrock), or every filter silently returns zero. And the hosted OpenAI family (`us.openai.gpt-5.6-*`, `us.openai.gpt-6-astra`, `openai.gpt-5.4`) is genuinely absent from **both** offer files, so its cards remain the only source; the old explanation that Marketplace billing is uncovered is wrong — Claude's rows are `MP:` usagetypes and are covered. `PROMPT_CACHE_OBSERVABILITY_ENABLED=false` disables the observability layer (fingerprint hook, cacheStatus derivation, EMF metrics) — the caching itself stays on - **Token cost effectiveness is a design tenet — engineer against waste, not against context.** Before merging a change that touches the model call path, answer: what does this add to the prompt, on every turn, for the life of every session? (1) Anything in the cacheable prefix (system prompt, `toolConfig`, restored history) must be deterministic and append-only — see the prompt-cache contract above. (2) Per-turn payloads (tool results, MCP responses, retrieved documents) should be bounded or offloaded, never unbounded pass-through. (3) Don't guess — verify: `cacheStatus` + fingerprint hashes on the session's `C#` rows, `GET /admin/costs/sessions/{id}/calls`, and the `AgentCoreStack/PromptCache` EMF metrics exist to prove a change's cost impact. The balance: "cost effective" means eliminating waste (avoidable cache re-writes, duplicated context, oversized payloads) — it never means stripping context the model needs for a quality answer. When cost and answer quality genuinely conflict, quality wins; look for the cheaper path to the *same* quality, not a cheaper answer - **One session can be served by more than one agent — never cache session state on an agent instance.** The agent cache keys on *configuration* (system prompt, tools, model, skills), so an `@`-mention turn builds a second `Agent`, and each `Agent` builds its own `TurnBasedSessionManager`. Both write the same DynamoDB session row and neither knows the other exists, while `initialize()` never re-runs on a cache hit — so anything a manager loads once and holds goes stale silently. This has bitten twice: conversation history (#741, fixed by aliasing the message list in `_adopt_session_conversation`) and compaction state (#751, fixed by re-reading it every turn in `_adopt_persisted_compaction_state`). Per-session state must be aliased across instances or re-read per turn, and must never move backwards — a clobbered checkpoint or truncation anchor is a prompt-cache **cost** bug before it is a correctness one - **All dependencies use exact version pins** — no `^`, `~`, or `>=` @@ -123,6 +123,26 @@ The SPA sends an httpOnly session cookie — not `Authorization: Bearer`. A rout Do not reintroduce a Bearer-only `Depends(...)` on any user-facing route. If you find one in older code, migrate it to `get_current_user_from_session`. +### App shell chrome: `data.chrome` on the route + +The app shell (`app.ts` / `app.html`) decides what surrounds a page by reading `data.chrome` off the active route, never by testing the URL. Three states: + +| `chrome` | Sidenav | Content box | +|----------|---------|-------------| +| *(unset)* | chat nav | `mx-auto max-w-7xl` | +| `'minimal'` | none | none — page fills the shell (the artifact viewers, `artifacts/:id` and `shared-artifact/:shareId`) | +| `'admin'` | **admin nav** (`AdminNav` replaces the sidenav's body) | none — `AdminLayout` owns its own padding | + +**The walk is deepest-*declared*-wins, not leaf-only** (`shared/utils/route-chrome.ts`). `'admin'` is declared once on the parent `/admin` route and every child declares nothing; with the default `paramsInheritanceStrategy` (`'emptyOnly'`) Angular does **not** copy `data` onto a child that has its own path and component, which every admin page has. A leaf-only read therefore sees nothing on any of them. A leaf can still opt out by declaring something else (`'full'`). + +**Rule:** The admin console's navigation is the *sidenav's body*, not a column inside the page. Never re-add an `