Conversation
#517 (headline) — assistants can use tools again, reverting the 1.0.0 KB-only restriction (#382). Filed under ✨ Improved, with a deployment-note callout since it's a user-visible behavior change. - #521 — CodeQL sweep (2 HIGH URL/host findings, 24-site log-injection pass, hardened CI checkout) → 🔒 Security. - #520 — 6 Dependabot CVEs (Astro, esbuild, pydantic-settings) → 📦 Dependencies table. - #518 — nightly pipeline script-path fix → 🐛 Fixed.
philmerrell
added a commit
that referenced
this pull request
Jul 14, 2026
* fix(deps): remediate 6 open Dependabot alerts (#520)
docs-site (npm):
- astro 6.3.1 -> 6.4.8: fixes reflected XSS via slot name (#241,
GHSA-8hv8-536x-4wqp), host-header SSRF in prerendered error page
(#242, GHSA-2pvr-wf23-7pc7), and XSS via unescaped spread attribute
names (#243, GHSA-jrpj-wcv7-9fh9).
- esbuild -> 0.28.1 via overrides: fixes dev-server arbitrary file read
(#240, GHSA-g7r4-m6w7-qqqr).
frontend/ai.client (npm):
- esbuild -> 0.28.1 via overrides (transitive through @angular/build
21.2.16): fixes dev-server arbitrary file read (#149,
GHSA-g7r4-m6w7-qqqr).
backend (pip/uv):
- pydantic-settings 2.13.1 -> 2.14.2 (transitive): fixes
NestedSecretsSettingsSource symlink traversal / local file read
(#234, GHSA-4xgf-cpjx-pc3j).
Verified: docs-site astro build OK; frontend prod build + 1239 unit
tests OK; backend 4003 tests OK; npm audit clean for both npm projects.
* fix(security): remediate CodeQL alerts (HIGH/MEDIUM + NOTE cleanups) (#521)
Security fixes (with regression tests):
- HIGH py/incomplete-url-substring-sanitization: external_mcp_client now
parses the URL host (urlparse) and matches an anchored suffix instead of
substring-checking the whole URL, so a marker in a path/query/userinfo
can't trick SigV4 signing into attaching IAM creds to a non-AWS host.
Added TestAwsUrlHostSanitization (adversarial URLs).
- HIGH js/regex/missing-regexp-anchor: admin-tool.model parses the host
(new URL) and anchors the AWS-endpoint regexes ($). Added
admin-tool.model.spec.ts (13 cases incl. spoofed hosts).
- MEDIUM py/log-injection (24 sites/16 files): new
apis.shared.security.scrub_log() neutralizes CR/LF/control chars; wrapped
user-controlled values at every flagged log call. Added test_log_sanitize.py.
- MEDIUM actions/untrusted-checkout: added persist-credentials:false to all
inputs.ref checkouts in nightly-deploy-pipeline.yml.
- WARNING py/regex/duplicate-in-character-class: removed a bare '[' from a
re.VERBOSE comment that the regex parser misread as a character class.
NOTE cleanups: removed unused imports (py + 17 TS constructs), a dead local,
redundant '...' after abstractmethod docstrings, unused test imports; added
explanatory comments to 6 intentional empty-except blocks; reworded
commented-out code.
Left intentionally: 4 py/unused-global-variable alerts are FALSE POSITIVES
(_cached_signing_key/_cached_bucket/_client_secret_cache are working lazy
caches — verified read+written); recommend dismissing in the UI.
Verified: backend 4045 passed (3 pre-existing test_backup_coverage failures
are unrelated — a skill-resources backup-coverage gap on develop); frontend
build OK + 1252 unit tests; infra tsc + 406 jest tests; npm/ruff clean for
touched files.
* Release/1.0.2 (#522) (#523)
#517 (headline) — assistants can use tools again, reverting the 1.0.0 KB-only restriction (#382). Filed under ✨ Improved, with a deployment-note callout since
it's a user-visible behavior change.
- #521 — CodeQL sweep (2 HIGH URL/host findings, 24-site log-injection pass, hardened CI checkout) → 🔒 Security.
- #520 — 6 Dependabot CVEs (Astro, esbuild, pydantic-settings) → 📦 Dependencies table.
- #518 — nightly pipeline script-path fix → 🐛 Fixed.
* chore: prune dead workflow tracks, dedupe test gates, fix/fork-gate d… (#524)
* chore: prune dead workflow tracks, dedupe test gates, fix/fork-gate docs deploy
- nightly: remove never-useful AI coverage analysis + merge-validation
tracks (relevant only to the old multi-stack era); delete orphaned
ai-coverage-analysis.py
- remove dead source-project-prefix input from nightly-deploy-pipeline
and the now-orphaned promote-ecr-image.sh it fed
- extract duplicated test gates into a reusable tests.yml; wire into
ci, platform, backend, frontend-deploy, and nightly-deploy-pipeline
- docs-deploy: publish from main (was develop); fork-gate so forks don't
auto-publish
- release: fork-gate so forks syncing main don't auto-create releases
Verified: repo-shape jest 49/49, supply_chain pytest 31/31.
* ci: re-enable push-triggered deploys, path-scoped
Restore auto-deploy on push to develop/main for the platform, backend, and
frontend workflows (was workflow_dispatch-only since v1.0.0). Each push
trigger is scoped to that workflow's own surface so unrelated changes don't
redeploy:
- backend: backend/**, scripts/build/**, the workflow file
- platform: infrastructure/lib (stack/constructs/config), bin, bootstrap-assets, scripts/platform/**, the workflow file
- frontend: frontend/**, scripts/frontend/**, the workflow file
develop -> development env, main -> production env (existing per-job mapping).
Forks without AWS secrets fail safely at the credential step; forks that
configured their own secrets get the intended fork-and-deploy behavior, and
manual workflow_dispatch remains available everywhere. docs-deploy was
already scoped (docs-site/** + its workflow file).
* docs: trim verbose/misleading workflow comments
Cut the trigger and fork-gate comments down to one-liners and drop the
inaccurate 'secrets make it fork-safe' framing (forks must explicitly
enable Actions regardless).
* ci: serialize platform + backend deploys via shared concurrency group (#525)
Put platform.yml and backend.yml in one repo-global concurrency group
(deploy-<ref>) so a CloudFormation deploy and the API-driven backend code
deploys can't run at the same time and stomp on the same ECS service /
AgentCore Runtime / Lambda. They queue instead; order doesn't matter
post-initial-deploy since either order converges. Frontend stays
independent. cancel-in-progress stays false.
* Chore/cleanup dependabot codeql 2026 06 (#526)
* fix(deps,codeql): bump joserfc to 1.7.2; drop unused imports
- joserfc 1.6.3->1.7.2 (backend/uv.lock), 1.6.5->1.7.2 (scripts/backup-data/uv.lock)
remediates Dependabot GHSA-wphv-vfrh-23q5 / CVE-2026-48990 (#244, #245).
Targeted 'uv lock --upgrade-package joserfc'; no other packages changed.
- Remove unused 'Optional' import in agents/main_agent/agent_types.py (CodeQL #721).
- Remove unused 'ssm' import in app-api/app-api-environment.ts (CodeQL #722).
* fix(ci): render reusable test-gate job names statically
The reusable tests.yml jobs used an inline label expression in their
'name:'. GitHub does not evaluate name expressions for SKIPPED jobs, so
callers that run only one suite (backend.yml, platform.yml,
frontend-deploy.yml, nightly-deploy-pipeline.yml) showed the raw
${{ ... }} text as job labels for the unselected (skipped) jobs.
Make the three job names static and relocate the nightly track-label
prefix onto the (never-skipped) caller jobs in nightly-deploy-pipeline.yml,
which matches the existing '[label] Deploy PlatformStack' convention and
evaluates reliably. Drops the now-unused 'label' input from tests.yml.
* Release/1.0.3
Patch release: CI/CD pipeline cleanup, re-enabled path-scoped auto-deploys,
serialized platform+backend deploys, and a joserfc CVE / CodeQL dependency sweep.
No application code or user-facing behavior changes.
- Bump VERSION to 1.0.3; sync manifests + lockfiles
- Brief RELEASE_NOTES.md and CHANGELOG.md entries (#524, #525, #526)
- Steering: scale release-notes depth to release size (brief patches, deep features)
* fix: add missing iam permission for agent core gateway
* fix: add lambda:InvokeFunction to gateway role for MCP targets
AWS docs require an identity-based policy on the gateway service role
in addition to the per-target resource policy (lambda:AddPermission).
Without this the Gateway gets 403 when trying to invoke Lambda targets.
* fix(iam): grant bedrock-agentcore:GetMemory to app-api + runtime roles
AgentCore Memory strategy discovery (MemoryClient.get_memory_strategies)
calls bedrock-agentcore:GetMemory, but neither the App API task role nor the
AgentCore Runtime execution role allowed it — every other memory data-plane
action was granted. The denied GetMemory made strategy discovery fail
silently:
- App API: GET /memory returned empty facts/preferences with 200, so the
Settings memories/preferences page rendered blank despite stored records.
- Runtime: _discover_strategy_ids() failed, leaving retrieval config empty, so
the agent kept writing events (CreateEvent) but never recalled long-term
memories.
Add bedrock-agentcore:GetMemory to the AgentCoreMemoryAccess statement on both
roles (app-api scoped to the memory ARN; runtime scoped to memory/*). No other
actions changed. Validated against the AWS Service Authorization Reference
(GetMemory = Read on the memory resource type) and the live AccessDenied in
app-api logs.
Bumps VERSION to 1.0.4 + brief RELEASE_NOTES/CHANGELOG entries. Infra (IAM)
change — deploys via the platform (CDK) pipeline.
* fix: keep tool card after OAuth/tool-approval resume
An interrupt-resume turn (OAuth consent or tool approval) has no new user
message, and the resumed Strands stream does not replay the interrupted
tool_use block — it emits only the tool_result plus a fresh assistant
message with the final text. The default sync truncates everything after
the last user message and replaces it with the resumed stream, discarding
the assistant message that holds the paused tool card. The reset parser
also drops the incoming tool_result (no matching tool_use in its fresh
builder). Net: the tool card disappears even though the call succeeded.
Both resume paths now:
- use beginContinuationStreaming() instead of startStreaming(), pinning the
existing messages (including the tool card) as a prefix and appending the
resume after them — same pattern as the max_tokens "Continue" flow.
- reconcile from persisted memory after the resume stream closes via the new
MessageMapService.reloadMessagesForSession(), so the card flips from
"Running..." to its completed result (memory holds tool_use + tool_result).
reloadMessagesForSession force re-fetches (bypasses the "already loaded"
guard) without flipping the skeleton loading state; the shared fetch/reconcile
logic is extracted into fetchAndApplyMessages(). Removed the now-unused
StreamParserService injection from ChatRequestService.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(kaizen): weekly research scan 2026-07-03
Generated by the kaizen-research skill. Top 5 ideas appended to
docs/kaizen/review-queue.md for the kaizen-review-prep run later this morning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(kaizen): weekly review prep 2026-07-03
Catch-up review consuming this morning's fresh research/2026-07-03.md
(no 06-12/06-19 reviews ran). 10 proposals ranked; top item is the
bedrock-agentcore 1.17.0 bump (closes the #482 SSE deadlock we're
exposed to today + retires a queued guard). Queue trimmed: superseded
Strands/agentcore bump duplicates consolidated into the two 07-03
bumps, Fable 5 un-withdrawn (reinstated Jul 1), starlette + PR-gate
marked shipped. Stacked on the research PR (#534).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: isolate chat streaming per conversation
The chat streaming layer used global singletons that broke when two
conversations streamed concurrently (ask in A, navigate to B, ask again).
Make streaming state per-session end to end, and fix the UX gaps that
surfaced once background streaming actually worked.
Per-session streaming:
- ChatStateService holds per-session state (loading, stop reason,
cost/context aggregates, Continue affordance, AbortController) behind a
viewed-session facade, so existing consumers are unchanged.
- StreamParserService keeps a Map<sessionId, ParserSessionState> with a
per-stream id; ChatHttpService captures it per request and drops late
events from a superseded stream (same-session resubmit guard).
- MessageMapService runs one sync effect per active stream; endStreaming,
abort, and loading are all keyed by session. Stop and double-submit only
affect their own conversation; navigating away no longer aborts the
in-flight stream (backend still completes and persists the turn).
Navigation scroll policy:
- New ScrollPositionService remembers per-conversation scroll offset for the
SPA session. Returning restores where you were; first open (or after a
reload) anchors the latest turn instead of the top.
Streaming-text replay fix:
- StreamingTextComponent seeds its display from whatever text already exists
at mount, so navigating back to a streaming conversation shows the current
state instead of re-typing the whole response from character zero.
Sidebar in-progress indicator:
- Conversation rows show a pulsing dot while a response streams (reads the
same per-session loading state), replacing the hover ellipsis menu while
in progress so the two never overlap and actions can't target a
mid-response conversation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: default export to messages only
Include only user and assistant messages by default in conversation exports. Tool calls, images, and citations must now be explicitly selected by the user.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* feat: expose session options menu on top-nav title
Add a dropdown to the top-nav session title (arrow/chevron trigger) that
offers the same actions as the sidenav ellipsis menu — Rename, Share,
Save to…, and Delete — reusing SessionService and the existing dialogs.
Also fixes title-transition polish surfaced while building this:
- Consolidate the two <app-topnav> instances (skeleton vs loaded branch)
in chat-container into a single persistent instance so the fixed
.chat-topnav-wrapper no longer remounts on skeleton→loaded, which was
restarting its left-transition and sweeping the whole bar ~288px.
- Optimistically set currentSession on sidenav click so the title updates
instantly instead of lingering until metadata loads.
- Optimistic inline rename (revert on failure) to remove the save lag, and
reload the sessions resource so the sidenav list reflects the new title.
- Gentle keyed slide+fade title entry animation (component-scoped).
- Title skeleton while the session metadata is loading (hard refresh) or a
brand-new chat's title is still generating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: update onSessionClick spec for optimistic currentSession set
onSessionClick now takes the clicked session and optimistically sets it as
currentSession; update the spec to pass a session and assert the set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: clear OAuth consent state on session switch
The OAuth "Authorization needed" banner is held in a root-singleton
OAuthConsentService keyed by providerId (not sessionId), and its
pending() signal drives the banner regardless of the originating
session. Every other per-session UI surface (compaction, artifacts,
MCP app frames/cards/consent) is reset in the route subscription on
conversation change, but OAuthConsentService was not — so a prompt
raised in one session persisted onto the next, including the blank
welcome screen.
Clear it fail-closed alongside the other resets. This runs before
loadMessagesForSession, which re-seeds the new session's own pending
interrupts from persisted server metadata, so a session that
legitimately needs authorization still shows its banner. Clearing
seenInterruptIds also fixes a latent bug where a genuinely-needed
re-prompt for the same provider in a later session was silently
suppressed by the dedup guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: push session title mid-stream via session_title SSE event
New sessions were only renamed from "New Conversation" after the agent
stream closed, even though the backend already generates the title
concurrently with the stream. This surfaces that title while the response
is still pending.
- inference-api keeps the concurrent title-generation task handle and
interleaves a one-shot `session_title` SSE event between agent events
once it resolves (non-blocking done-check, same drain pattern as the
MCP Apps broker). Never emits the "New Conversation" placeholder.
- Fix latent bug: generate_conversation_title ran sync boto3 converse on
the event loop, stalling the live agent stream for the whole Nova Micro
round-trip. Now runs via asyncio.to_thread.
- SPA parses session_title, applying it to both the sidebar cache and the
currentSession signal (new SessionService.applyServerTitle) so the
top-nav header renames too. Allowlisted past Completed-state gating
since it can arrive just after `done`.
- Post-close refreshTitleFromServer remains the fallback for streams that
outrun generation.
Documented the new event in the CLAUDE.md SSE contract table. Adds
backend + frontend test coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: skeleton + fallback for pending session titles in sidebar and top-nav
Surfaces the "title generating" state cleanly now that titles arrive
mid-stream via the session_title SSE event.
Sidebar (session-list):
- Show a shimmer skeleton instead of "Untitled Session" while a new
conversation's title is still generating (titleless + streaming).
- Darken the shimmer only on the selected row, whose active highlight
(bg-gray-200 / dark:bg-white/5) would otherwise hide a light skeleton.
Keyed off a plain .session-row--active sentinel added to the same
routerLinkActive that paints the highlight, coloured in component CSS
(no arbitrary Tailwind variant), with class-based dark handled via
:host-context(html.dark).
- Row is now flex/min-h-8 so the skeleton→title swap causes no vertical
jank, and the generated title reveals with the top-nav's slide+fade
(session-title-enter), kept truncatable via min-w-0.
Top-nav:
- Gate the title skeleton on a proper titlePending signal (metadata
loading OR the session actively streaming) so it can no longer shimmer
forever; once resolved with no title it falls back to "Untitled
Session" instead of an eternal skeleton.
Adds/updates topnav + session-list specs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: persist interrupted-turn context so aborted responses aren't orphaned
When a response is interrupted (user Stop, refresh, or dropped socket) the
user turn was already persisted but the assistant reply was not, leaving a
dangling user turn — no context on reload and malformed history the model
must paper over.
Backend:
- set/clear_interrupted_turn markers (metadata.py) with race-safe reason
precedence: user_stopped (authoritative client signal) always wins over
the connection_lost cancellation fallback. clear is an atomic pop
(ReturnValues=UPDATED_OLD) returning the settled reason.
- stream_coordinator: (CancelledError, GeneratorExit) backstop persists the
IN-FLIGHT partial assistant text (accumulator scoped to the current
message so already-persisted mid-turn messages aren't duplicated) via
asyncio.shield; empty-partial placeholder gated on a user-tail to repair
role alternation only where needed.
- app-api POST /sessions/{id}/interrupt: authoritative user_stopped carrier
(cookie auth; fetch keepalive + X-CSRF-Token, not sendBeacon). Lives on
app-api because the AgentCore Runtime data plane only proxies
/invocations + /ping.
- inference-api: pop the marker at turn start and prepend a reason-specific
ephemeral interruption note at the pending_ctx_block seam (invisible to
the user via the displayText split; ages out via compaction).
Remaining: SPA stop signal in cancelChatRequest + reload chip/Continue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(spa): surface interrupted-turn state — stop signal + reload chip
Completes the interrupted-turn feature on the SPA side:
- cancelChatRequest fires a best-effort keepalive fetch to
POST /sessions/{id}/interrupt with {reason:'user_stopped'} + X-CSRF-Token
(not sendBeacon — it can't set the CSRF header) before aborting, and
reflects the interruption locally so the chip shows without a reload.
- lastTurnInterrupted + reason are per-session ChatState signals (mirroring
lastTurnContinuable): hydrated from session metadata on reload (set-true
only), cleared beside every continuable clear (new send / continuation /
new streamed turn).
- message-actions renders a chip on the last message: connection_lost →
"Response interrupted" + Continue (reuses continueTruncatedTurn against
the persisted partial); user_stopped → "You stopped this response", no
Continue. Gated on last-message + not-loading; max_tokens Continue wins.
Specs cover both chip variants, Continue emit + precedence, the keepalive
signal shape, local reflect, failure isolation, and continuation clears.
Full app build + affected unit specs green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: add assistant KB sync (scheduled re-index) design spec
Sweeper-not-scheduler architecture with layered runaway guards; all five
open questions resolved against AgentCore Identity docs, Drive API docs,
and code inspection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): add SyncPolicy data layer, DueSyncIndex GSI, delete cascades
PR-1 of the KB sync feature (docs/specs/assistant-kb-sync.md):
- SyncPolicy model + repository in apis.shared.sync_policies (adjacency
list SYNCPOL# items on the assistants table)
- Sparse DueSyncIndex (GSI4) — keys present only while state=active, so
paused policies are physically invisible to the dispatcher sweep
- Conditional re-arm for idempotent dispatch; breaker counters
(consecutive failures / source-gone) maintained by record_sync_result
- Assistant and document delete paths eagerly cascade sync policies so
no schedule outlives its source
- Document gains contentHash/lastSyncedAt/syncPolicyId; Assistant gains
lastUsedAt (inactivity-pause input)
Inert until the PR-2 dispatcher exists: nothing reads DueSyncIndex yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): dispatcher + worker Lambdas, EventBridge sweep, kb-sync image pipeline
PR-2 of the KB sync feature (docs/specs/assistant-kb-sync.md):
Backend:
- kb-sync dispatcher (apis/app_api/kb_sync/dispatcher.py) — sweeps the
sparse DueSyncIndex each tick and applies the runaway guards in
order: kill switch, liveness (orphan policies hard-deleted),
circuit breaker (failure/not-found streaks -> paused_error),
30-day inactivity pause, in-flight skip via syncRunStartedAt,
re-arm-with-backoff BEFORE async-invoking the worker
- worker stub (records "skipped", clears the run stamp); Drive sync
lands in PR-3, web re-crawl in PR-4
- rearm_policy gains mark_run_started so claiming the in-flight slot
is atomic with winning the re-arm
- dispatcher reads assistant/source records by raw adjacency-list key
(keeps the image surface to apis.shared.sync_policies + kb_sync);
tests create them through the real services so schema drift breaks
Infra:
- KbSyncConstruct: two DockerImageFunctions sharing one image with
ImageConfig.Command overrides; rate(15 min) EventBridge rule —
DISABLED unless CDK_KB_SYNC_ENABLED=true (dark by default), env
kill switch mirrors the flag; namespace-conditioned PutMetricData;
error alarms; function-name SSM params for the code-deploy step
- platform-as-bootstrap: byte-stable bootstrap-assets/kb-sync stub
Pipeline:
- backend/Dockerfile.kb-sync (lightweight: boto3+pydantic, no ML deps)
- build-one.sh kb-sync case, deploy-image-lambda-one.sh
kb-sync-dispatcher/worker cases sharing one image tag
- deploy-image-lambda-one.sh: first-deploy grace skip when the
function-name SSM param doesn't exist yet (backend.yml runs before
the platform deploy on the introducing PR)
- backend.yml build+deploy jobs; nightly scan-images + supply-chain
test lists include the new Dockerfile
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): Drive-file sync path — vault token, change detection, staged re-ingest
PR-3 of the KB sync feature (docs/specs/assistant-kb-sync.md §6.1):
Worker (replaces PR-2 stub for drive_file policies):
- resolves the policy creator's stored Google token from the AgentCore
Identity vault (GetWorkloadAccessTokenForUserId -> GetResourceOauth2Token,
no live user session; same customParameters as consent — vault-key rule)
- two-gate change detection: Drive files.get version vs stored sourceEtag
(continuous with import provenance), then sha256 vs contentHash —
identical bytes never reach Docling/Titan
- changed bytes staged to the document's EXISTING S3 key -> the untouched
ingestion pipeline re-chunks/re-embeds via its S3 event
- pause semantics per spec §7: requires_consent / Google 401 ->
paused_reauth (not a failure streak); Drive 404 (deleted-or-unshared,
indistinguishable) -> not_found strike, dispatcher pauses at 2;
trashed=true -> grace skip; provider/adapter/provenance gone ->
paused_error; every path records the run so the stamp always clears
Shrinkage cleanup:
- worker stashes previousChunkCount before staging; ingestion completion
atomically pops it (REMOVE + UPDATED_OLD — duplicate S3 events can't
double-delete) and deletes stale tail vectors {doc}#{new..prev-1} via
new delete_vector_tail; uses post-split len(chunks), the true vector
count this run wrote
Adapter/image/infra:
- GoogleDriveAdapter.get_file_metadata (cheap files.get, trashed +
version/md5Checksum/modifiedTime fields)
- kb-sync image gains apis.shared.oauth + file_sources (FastAPI-free,
verified by import-surface simulation); httpx + bedrock-agentcore pins
- worker IAM: vault token read (two Get* actions only — no consent
completion, no provider CRUD), oauth-providers table read, documents
bucket put; env: workload identity name + callback URL (same values
app-api uses)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): web re-crawl path — refresh/upsert crawls, miss accounting, TTL fix
PR-4 of the KB sync feature (docs/specs/assistant-kb-sync.md §6.2):
Crawler refresh mode (opt-in via RefreshState; normal crawls unchanged):
- upsert-by-URL: known pages reuse their document records — a re-crawl
never duplicates documents
- conditional GET with the stored ETag (304 = no body, no re-extract)
plus a content-hash gate; identical bytes never re-embed
- 'changed' emitted BEFORE the S3 overwrite so the worker stashes the
previous chunk count ahead of the ingestion event (shrinkage cleanup)
- transient fetch errors never flip an existing indexed doc to failed;
its last-good content keeps serving
- link-enqueue block deduplicated into a nested helper
CrawlJob TTL fix (the silent-kill trap): terminal crawl jobs carry a
30-day TTL — a sync-covered job auto-expiring would trip the
dispatcher's liveness check and delete the policy. finalize_crawl gains
set_ttl; sync re-crawls finalize with the ttl REMOVED, and
reset_crawl_for_refresh rearms the job (running, zeroed counters, no
ttl) before each run.
Worker web path:
- re-runs the policy's crawl with its stored (already-capped) settings,
13-min crawler budget inside the 15-min Lambda so finalize + miss
accounting always complete
- miss accounting: pages absent from 2 CONSECUTIVE re-crawls are
soft-deleted with cleanup run inline; fetch failures count as seen
(outage != gone), robots-disallowed pages do not; misses reset on
reappearance; missing root doc is recreated route-style
Image: web_sources + documents + embeddings trees; beautifulsoup4,
trafilatura, lxml pins. Worker Lambda timeout 10 -> 15 min.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): sync-policy API + resume hooks — CRUD routes, run-now, reauth/inactivity resume
- New /assistants/{id}/sync-policies router (edit-gated like documents):
create/list/patch/delete + POST run-now (202, atomic 10-min cooldown on
lastManualRunAt). Resume of paused_reauth is 409 — only a fresh OAuth
consent resumes it.
- Reauth markers: worker pause writes USER#/SYNCREAUTH# marker so
complete_consent() resumes exactly the right policies with one query;
markers are advisory (resume re-verifies state) and self-clean.
- Inactivity resume: throttled lastUsedAt bump on chat use (conditional
write, one winner/day) wakes paused_inactive policies due-immediately.
- restore_crawl_ttl: deleting a crawl's policy puts the job back on
normal 30-day expiry (running jobs untouched).
- drive_file policies keep a syncPolicyId back-pointer on the document,
cleared on policy delete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(kb-sync): SPA sync-policy controls on assistant knowledge page
PR-6 (final) of the KB-sync series. Per-source "Keep in sync" controls
on the assistant knowledge editor for imported Drive documents and web
crawls: interval select (Manual only/Daily/Weekly/Monthly), status line
(state, reason, last/next sync), pause/resume, Sync now (cooldown-aware),
and a Reconnect affordance for paused_reauth policies that routes through
the existing OAuth consent popup (a fresh consent auto-resumes server-side).
Controls are owner/editor-only; device uploads show no control.
Backend (additive, same-PR per cross-package contract):
- DocumentResponse now exposes sourceConnectorId/sourceAdapterKey/
sourceFileId/syncPolicyId/lastSyncedAt so the SPA can tell imported
docs from device uploads and route reconnect
- GET /web-sources/crawls without ?active=true returns full history
(list_all_crawls) — completed crawls are the syncable web sources
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(interrupted-turn): persist stopped-turn metadata so cost + message badges survive
A Stop aborts the fetch before the stream's terminal `metadata` SSE (usage /
cost / context) can arrive, so both the per-message metadata badges and the
session cost badge stayed blank for that turn — and blank even after reload,
because the cancellation path persisted only the partial text + interrupted
marker, never any metadata.
The abort is load-bearing (killing the socket is what stops generation and
billing), so the backend can't push the metadata to the dead socket. Instead:
- Backend: `_persist_interruption` now also calls `_store_message_metadata`
(the same call the `done` path uses), which writes the per-message row AND
bumps the denormalized session aggregates that hydrate the cost badge on
reload. Keyed to the interrupted message's odd-position index. A cut
generation often never delivered Bedrock's terminal usage event, so new
`_projected_input_usage` falls back to the context-attribution projection
for input-side tokens/cost (output unknown → priced at zero).
- Frontend: `refreshAggregatesAfterStop` (delayed + one retry, mirroring
refreshTitleFromServer, since the write races the abort) re-pulls session
metadata and re-seeds the cost badge live. Per-message badges hydrate from
the same persisted row on the next reload.
Known limitation: the per-message contextBreakdown partition badge is not a
persisted field (live-only even for normal turns), so it stays blank on a
stopped/reloaded turn — a separate change affecting all turns.
Tests: backend test_interrupted_turn_persistence.py (+2) and full streaming
dir (175) green; frontend chat-http.service.spec.ts (+2) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kb-sync): grant worker read on the vault's backing OAuth secrets
The KB sync worker resolves the policy creator's Google token from the
AgentCore Identity vault via GetResourceOauth2Token, but that call reads
the refresh token *through* the Secrets Manager secret the vault
auto-creates per provider (bedrock-agentcore-identity!default/oauth2/<id>).
The worker role granted only the two bedrock-agentcore:* actions, so the
call failed with AccessDenied on secretsmanager:GetSecretValue and every
Drive sync returned "failed".
Add the read-only AgentCoreIdentityOAuthSecrets grant (GetSecretValue +
DescribeSecret on ...!default/oauth2/*) — the same grant app-api and
inference-api already carry, minus the write lifecycle a background
fetcher never needs. Covered by a new assertion in kb-sync.test.ts.
Verified live in dev-ai: with the grant, the worker gets past the vault
read (the remaining paused_reauth is a genuine expired-refresh-token,
not this IAM gap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(oauth): forward admin customParameters, drop hardcoded vendor baseline
`custom_parameters_for` previously injected vendor-specific OAuth params
(Google `access_type=offline`, plus `prompt=consent` on the force-auth
path) on top of the connector's admin-configured extras, keyed off
`provider_type`. That hid a documented requirement inside the code and
made the `customParameters` map depend on the call site (grant vs
retrieval), which AgentCore factors into its vault key.
Simplify to a single rule: every call site forwards the connector's
admin-configured `customParameters` verbatim via
`custom_parameters_for(admin_extras)`. Admins set `access_type=offline`
and `prompt=consent` in the connector's "Custom OAuth Parameters" field
(the seeded Google connector already carries both), so the same map is
sent on consent and on every retrieval — no baseline merge, no
`provider_type`/`force_authentication` branching, no per-call-site drift.
Removes `_vendor_baseline_params`, the `provider_type_lookup` plumbing on
the consent hook, and the `force_authentication=True` customParameters
argument at all read sites (the SDK-level `force_authentication` flag is
unchanged). Behaviour for the existing Google connector is identical
(same resulting map); the win is that the vault key is now provably
consistent and there are no hidden customizations in the token path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: add tool-search token-bloat + per-user markdown memory specs
Two design specs drafted alongside recent exploration work:
- tool-search-token-bloat-strategy: cross-source tool-search plan for
MCP token bloat (tiered discovery, AWS Agent Registry tier).
- user-markdown-memory: per-user markdown "second brain" memory,
re-scoped skills reference-file mechanism, prompt-cache injection.
Docs only; no code or behaviour change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(kb-sync): log workload identity + userId on a reauth pause
A reauth pause was opaque — "credentials need re-consent" gave no hint
why. The vault token is keyed by (workload identity, userId), so the
overwhelmingly common cause is that the token was vaulted under a
DIFFERENT workload identity than the worker queries: e.g. a consent done
through local dev (AGENTCORE_RUNTIME_WORKLOAD_NAME=local_dev_inference)
can't be read by the deployed worker (platform-workload), and vice
versa. That exact mismatch cost hours to diagnose.
Surface the workload name, userId, and provider the lookup used in the
pause warning so the mismatch is obvious in one log line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(kb-sync): plumb CDK_KB_SYNC_ENABLED into the platform deploy
KB sync ships dark: the EventBridge rule is created disabled and the
KB_SYNC_ENABLED kill switch is false unless CDK synths with
CDK_KB_SYNC_ENABLED=true (config.ts). platform.yml never forwarded that
var, so a CDK deploy always synthed the feature off — the only way to
enable it was an out-of-band CLI flip that reverts on the next deploy.
Forward it as an environment-scoped `${{ vars.CDK_KB_SYNC_ENABLED }}`
like every other CDK_ var. The development environment variable is set
to "true" (enables the rule + kill switch in dev-ai durably); production
leaves it unset, so it stays dark until validated there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(kb-sync): default the feature ON with a kill switch
KB sync shipped opt-in (default off), a holdover from ship-dark
incremental development. The feature is complete and guarded, so a
deployer/cloner of the public stack should get it working by default —
a hidden default-off flag is bad DX. Invert to opt-out: enabled unless
CDK_KB_SYNC_ENABLED=false (or a `kbSync.enabled: false` cdk.json context).
The workflow forwards `${{ vars.CDK_KB_SYNC_ENABLED }}`, which is an empty
string when unset — a plain `?? false` fallback wouldn't flip the default,
so config.ts treats empty/unset as "use the default (on)" and only the
literal "false" as the kill switch. Adds config-resolution tests covering
unset / empty / "true" / "false" / context-disable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(kb-sync): clarify the auto-sync control and always show last-synced
The knowledge-base sync control was a bare "Manual only" dropdown that
never explained what syncing does, and its status line only appeared
while a schedule was active — a manual-only file showed nothing.
- Lead the control with an "Auto-sync from <source>" label (with a
descriptive title tooltip) so every row states what it is and where it
pulls from.
- Rename the options to self-describing verbs: "Don't auto-sync",
"Sync daily/weekly/monthly".
- Always surface a "Last synced" line, including manual-only sources, via
a new lastSyncedAt input fed from Document.lastSyncedAt.
- Pair a relative age with an absolute date/time ("Synced 2h ago ·
Jul 3, 2:14 PM") for both "how long ago" and "exactly when".
Display-only; the timestamps already exist in the data model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kb-sync): tidy the sync row layout and repair blank sync timestamps
Aligns the knowledge-base sync control with the approved mockup and fixes
a bug that rendered the status line as a bare "Synced · next sync".
Layout:
- Move the "Auto-sync from <source>" context out of the control's button
row (where it forced "Pause" to wrap) and into the file's meta line.
- Compact the control to just the schedule select + actions.
- Add a status-line dot (green healthy / amber attention / grey idle).
- Top-align the status icon and download/trash actions with the filename
(items-start) instead of floating against the taller row.
Timestamp bug: several generators built ISO strings as
`datetime.isoformat() + "Z"`, yielding "…+00:00Z" (offset AND Z). That is
invalid ISO 8601 and parses to Invalid Date in strict engines (Safari),
so last-sync / next-sync rendered blank. Normalize to a single trailing Z
in service, dispatcher, and worker; harden the dispatcher parser to always
return a UTC-aware datetime; and harden the SPA to tolerate the legacy
"+00:00Z" so already-persisted policies still render.
Download and delete controls on documents are preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kb-sync): give web sources the same sync treatment as documents
The web-source (crawl) row shared the sync control with documents but was
missing the parity bindings the document row got: no source-context line
and no persistent last-synced timestamp, so a manual web source rendered
as a bare dropdown while an identical document showed full context.
- Add the meta-line context "Auto-sync from the web" (guarded by
isCrawlSyncable), mirroring the document's "Auto-sync from <provider>".
- Feed the control lastSyncedAt from crawl.completedAt so a manual web
source still shows when it last refreshed (the component already prefers
the policy's lastSyncAt when a schedule is active).
- Make the crawl meta line wrap-safe (flex-wrap), matching the doc row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(kb-sync): unified skeleton loading for the knowledge base lists
The Web sources list had no loading flag, so it popped into existence
after its network round-trip, while Uploaded Documents showed a lone
centered spinner — two async lists with inconsistent, jarring loads.
Introduce one initial-load gate for both lists:
- Add isLoadingCrawls, set from loadSyncData (cleared on the viewer
early-return and in finally), mirroring isLoadingDocuments.
- isLoadingKnowledge computed gates both lists so they reveal together;
raise both flags synchronously in ngOnInit so the first paint is the
skeleton, not an empty flash.
- Replace the documents spinner with a skeleton list (pulsing icon + two
text bars, varied widths) that mirrors the final row shape — no layout
shift, and consistent with the existing connector-button skeleton.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(kb-sync): show a "Saving…" indicator while a sync change applies
Changing the schedule (e.g. Don't auto-sync → Sync weekly) held the busy
state for the whole create/update/delete round-trip but only disabled the
controls — there was no positive sign anything was happening, and the
select eagerly reverts to its old value until the mutation confirms, so it
read as "nothing happened".
While busy, the status slot now shows a spinner + "Saving…" (role=status),
taking precedence over the sync status line — including for a manual-only
source being enabled, where no policy or status exists yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(users): normalize sync timestamps to strict ISO 8601 (single Z)
UserSyncService built timestamps as `datetime.isoformat() + "Z"`, yielding
"…+00:00Z" — both an offset AND a Z. That is invalid ISO 8601 and parses to
Invalid Date in strict engines (Safari), so the admin user-list "Last login"
and user-detail "Created"/"Last login" dates (rendered via `new Date()`)
showed "Never".
Same class of bug just fixed across KB-sync (service/dispatcher/worker);
this applies the identical normalization to the users domain.
- Write path: add `_iso()` helper, normalize `created_at`/`last_login_at`.
- Read path: add `_heal_iso()` and apply in `_item_to_profile` /
`_item_to_list_item` so legacy "+00:00Z" rows render correctly. Necessary
because `created_at` is preserved across logins forever (never rewritten),
so pre-fix users would otherwise show "Never" in Safari permanently.
- Regression tests for the write-path invariant and read-path heal.
`last_login_at` also backs GSI2SK/GSI3SK (sort-by-last-login); the suffix
change is lexicographically safe — ordering is dominated by the microsecond
datetime prefix and a transient mix of "+00:00Z"/"Z" rows does not corrupt it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(assistant-editor): group knowledge base into a contrasted inset panel
Wrap the Knowledge base section on the Assistant editor in a rounded,
bordered inset with a subtle gray-100/60 fill instead of the flat
border-t divider shared by other sections. Against the gray-50 form
column this reads as a mildly contrasted group, and the white inner
lists (Web sources / Uploaded Documents) now float on the tint to
reinforce the grouping. Drops the border-t/pt-8 divider since the card
provides its own separation; the form's space-y-8 preserves the top gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(connectors): add Google connector logo SVGs
Add Drive, Docs, Gmail, and Calendar icon assets under public/logos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(specs): agentic platform primitives plan + scheduled-runs design + spike brief
Reframe the proactive-agent effort from a single "Oliver" feature into a
primitive-enablement plan, mapped onto the Harness (headless run entrypoint)
and Registry (catalog + governance) explorations.
- agentic-platform-primitives.md: primitive maturity + gap ledger, the six
fundamentals (F1 headless run entrypoint .. F6 registry/governance),
phased plan, and Harness/Registry overlap. Oliver demoted to one
validation use case among several.
- scheduled-agent-runs.md: detailed design for F1+F2+F3 (renamed from the
earlier Oliver draft; generalized so any config/prompt/cadence works).
- harness-entrypoint-spike-brief.md: tight spike brief for the F1 keystone
(unattended-as-user auth + server-side SSE), to run in dev-ai.
Resolved decisions: F1 = minimal internal run_agent_headless(), A2A-ready;
governance floor (F6a) pulled forward into Phase A, Registry discovery (F6b)
deferred. Open: KB decoupling appetite, floor depth, sequencing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(harness): spike headless agent-run entrypoint (F1) — proven in dev-ai
run_agent_headless in apis/shared/harness: per-owner Cognito bearer mint
(workload-token + SigV4 front-door paths proven dead at the runtime
gateway), server-side SSE drain pinned to live wire shapes, F6a audit
records + guardrails/classification seams, delivery via the runtime's own
session materialization + title override. Driver script reproduces the
dev-ai proof and the negative auth probes. Findings + Phase A design in
docs/specs/harness-entrypoint-spike-findings.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(specs): record F1 spike findings + lock decision-gate calls
The headless-run entrypoint spike (Fable 5) is GO — proven end-to-end in
dev-ai. Bring its findings doc onto develop as the decision record and lock
the four gate decisions into the plan of record.
- Add harness-entrypoint-spike-findings.md (design deliverables + Phase A
punch list; full spike code lives on branch spike/harness-headless-entrypoint).
- agentic-platform-primitives.md §6: resolve act-as-user auth policy (Cognito
per-owner token behind an explicit headless-grant record), F6a floor depth
(audit fail-closed now; PII checkpoint required before unattended schedules),
KB decoupling (defer), sequencing (proactive-spine-first confirmed).
- scheduled-agent-runs.md §8: mark unattended-auth resolved with the chosen path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(harness): headless-grant record + production hardening of the F1 primitive
Replace the spike's BFF-table Scan with an explicit headless-grant record
(apis/shared/harness/grants.py): create-on-enable from an attended session,
per-owner lookup via the sparse HeadlessGrantUserIndex GSI, revocation that
deletes the stored credential, and a documented "must have logged in within
30 days" policy (TTL anchored to the login that issued the pinned refresh
token, matching the Cognito refresh-token validity).
CognitoRefreshBearerAuth now mints from the grant (rotation-aware: a rotated
refresh token is persisted back before the mint returns — punch-list #5).
Dedupe build_invocations_url into the harness as the single canonical
resolver; the chat proxy imports it (punch-list #4). Document the
enabled_tools=None = all-RBAC-allowed semantic and the schedule-snapshot
rule (punch-list #7). Governance docstrings updated to the locked F6a
decision: audit-only fail-closed + wired no-op guardrail/classification
seams, implementations gated to the scheduled phase. Add an
_build_http_client seam and tests covering the grant lifecycle,
grant-backed minting, audit fail-closed ordering, and stream outcomes
against a MockTransport runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(runs): cookie-authed "Run now" surface behind flag + RBAC capability
POST /runs/now executes one agent turn through the exact unattended path a
scheduled run will use (create-on-enable grant -> per-owner Cognito mint ->
runtime /invocations -> server-side SSE drain -> governance floor -> session
materialization) — the PR-1 validation surface from
docs/specs/scheduled-agent-runs.md §7. GET/DELETE /runs/grant expose grant
status and total revocation.
Gating is two independent controls (spec §6): the SCHEDULED_RUNS_ENABLED
kill switch (default ON; only the literal "false" disables — empty workflow
vars can't dark-stop prod) and a new `scheduled-runs` RBAC capability
resolved through the mature tools grant axis
(apis/shared/rbac/capabilities.py) — GA = grant the id to the default role.
Auth is the standard SPA cookie dependency per the CLAUDE.md app-api rule;
mint failures surface as 409, never 401, so the SPA is not bounced through
the login redirect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(infra): SCHEDULED_RUNS_ENABLED flag + HeadlessGrantUserIndex GSI
Thread the scheduled-runs kill switch through CDK: scheduledRuns.enabled in
config.ts (the CDK_KB_SYNC_ENABLED empty-string-safe ternary, copied
exactly) -> SCHEDULED_RUNS_ENABLED on the app-api container ->
CDK_SCHEDULED_RUNS_ENABLED forwarded by platform.yml. Default ON with a
kill switch; nightly inherits the default.
Add the sparse HeadlessGrantUserIndex GSI (grant_user_id / created_at) to
the BFF sessions table backing apis/shared/harness/grants.py — only
HEADLESS-GRANT# items carry the partition attribute, so session rows never
project into it. App-api's existing table grant already covers index/*, so
no IAM change. Tests mirror the kbSync flag matrix and pin the GSI shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(specs): Phase B scoping brief — scheduler + PII-ordering prerequisite
Records the work breakdown, model tiering, and the one design fork gating
scheduled delivery: the F6a classify_output checkpoint must run in-loop (or
as a post-hoc scrub), not pre-delivery, because the runtime turn persists
the session during the turn. B1 (inert schedule CRUD) is safe to start now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(specs): governance for scheduled runs is role/auth-based; drop B0 PII gate
A headless run executes as the owner with the owner's RBAC and delivers
only to the owner's own session list, so it crosses no new access boundary
and introduces no new recipient. Governance = RBAC (run-as-user) + grant
lifecycle + quota + fail-closed audit — all already built. The content
classification pass adds nothing for deliver-to-self, so B0 collapses and
B2 delivery is unblocked. Revises primitives §6-4 and the Phase B brief §1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(schedules): B1 — schedule data model + CRUD (inert)
Add ScheduledPrompt model + ScheduledPromptService (apis/shared, mirroring
sync_policies) and app-api CRUD under /schedules — create/list/get/update/
pause/resume/delete, gated by SCHEDULED_RUNS_ENABLED + the scheduled-runs
RBAC capability. Cadence (daily/weekday/weekly) -> next_run_at is computed
timezone-aware in the service so the future dispatcher stays a dumb "who's
due" query. enabled_tools is snapshotted at creation (Phase A punch #7).
Storage rides the existing sessions-metadata table (PK=USER#{user_id},
SK=SCHEDPROMPT#{schedule_id}), with a new sparse DueScheduleIndex GSI
(GSI3_PK/GSI3_SK, distinct from SessionLookupIndex's GSI_PK/GSI_SK to avoid
attribute collision) projected only while state=="active". app-api already
holds CRUD+index/* IAM grants on this table, so no IAM changes were needed.
Deliberately inert: nothing fires yet. The dispatcher/worker that reads
DueScheduleIndex and calls run_agent_headless is B2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(schedules): B3 — schedule management SPA + enablement
Adds the user-facing schedules feature: a signal-based list/create/edit
page under frontend/ai.client/src/app/schedules/ (bounded cadence UI —
daily/weekday/weekly + hour + IANA timezone, optional assistant + tool
snapshot, pause/resume/delete with confirm), the headless-grant
enablement UX (status banner, "Enable scheduled runs", paused_error
reauth_required / oauth_required affordances), and capability-gated nav
visibility that rides the /schedules list call's 403/404 rather than a
dedicated client signal.
Backend: adds POST /runs/grant to apis/app_api/runs/routes.py, sharing
the existing _resolve_grant create-on-enable logic with /runs/now so a
user can turn on scheduled runs without running a prompt first. Same
kill-switch + scheduled-runs capability gating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(schedules): B2 — scheduler dispatcher + worker
EventBridge rate(5m) -> dispatcher (sweeps the sparse DueScheduleIndex,
runaway guard, conditional re-arm before fire-and-forget) -> worker (mints a
per-owner Cognito bearer, run_agent_headless(trigger="schedule"), records the
outcome, pauses on reauth_required / oauth_required / repeated_failures).
Two Docker Lambdas share one image (Dockerfile.scheduled-runs) via
ImageConfig.Command; bootstrap-stub + out-of-band image deploy mirroring
kb-sync. IAM scoped to sessions-metadata RW, BFF-grant table RW, app-client
secret read, and AgentCore vault token + oauth-secret read; no
SigV4/InvokeAgentRuntime (the minted bearer is the front door, matching the
Run-now path). Delivery flows straight through — governance is role/auth-based.
Fixes the failure breaker: added a persistent consecutive_failures counter
(model + record_run_result, mirroring sync_policies) so repeated_failures
pauses at the production default of 3. The original last-status proxy capped
the streak at 2 and could never trip at the default; unified the worker's two
error paths on the returned streak and added regression tests at the default
threshold.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(schedules): register nav routes in specs to fix CI NG04002
The schedule-form and schedules-list specs used provideRouter([]), but the
components navigate programmatically (router.navigate(['/schedules']) etc.)
on save/cancel/create/edit. With no matching route the navigation rejects
(NG04002) AFTER the test body, surfacing as unhandled rejections that fail
the whole vitest process even though all 1378 tests pass. Register the
target routes so the real router resolves them. Scoped ng test: 46 passed,
0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(schedules): make scheduled-runs worker image importable
The worker Lambda crashed at INIT with ImportModuleError: the lean
scheduled-runs image (Dockerfile.scheduled-runs) never installed
cryptography or cachetools, which the worker pulls in transitively via
apis.shared.harness -> apis.shared.sessions_bff (cookie.py AESGCM,
cache.py TTLCache). Even past that, apis/shared/sessions/metadata.py
imported agents.main_agent...is_preview_session at module top level,
dragging the agents+strands packages (deliberately absent from the lean
image) into the worker's runtime delivery path.
- Defer the preview-session import in sessions/metadata.py to call time.
The two functions the headless delivery path uses
(ensure_session_metadata_exists / update_session_title) never call it,
so the agents import is never triggered in a headless run. Public name
and behavior unchanged for live app_api/inference_api callers.
- Add cryptography==48.0.1 + cachetools==6.2.4 to the shared image
requirements (dispatcher requirements.txt is the file the Dockerfile
installs; worker kept in sync).
Verified: worker + dispatcher import cleanly in an isolated lean-image
simulation (image pins + bundled modules only, no agents/strands).
Surfaced by dogfooding a real scheduled run in dev-ai.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(schedules): make preview-session check importable in lean worker image
Follow-up to the worker-image import fix (#566). Dogfooding the deployed
worker surfaced a second, deferred failure: the headless delivery path
(runner -> ensure_session_metadata_exists, metadata.py:773) DOES call
is_preview_session, so the previous lazy-import wrapper only moved the
ModuleNotFoundError: No module named 'agents' from INIT to run time. The
run still 'completed' (the runtime materializes the session during the
turn), but the idempotent session-row ensure + title override were
skipped ('result delivery failed' in the worker log).
is_preview_session is a trivial startswith('preview-') check; its weight
came only from agents/strands imports in
agents.main_agent.session.preview_session_manager. Move a dependency-free
copy into apis.shared.sessions.preview (mirroring what
apis.inference_api.chat.routes already does with its own local copy) and
import it in metadata.py. A drift-guard test keeps the literal in lockstep
with agents...Prefixes.PREVIEW_SESSION.
Verified: lean-image simulation now imports metadata + runs
is_preview_session with no agents/strands; 77 targeted tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(scheduled-runs): intersect client enabled_tools with RBAC on headless paths
Client-supplied `enabled_tools` was trusted end to end on the headless
surfaces: schedule create/update and "Run now" froze/forwarded the request
body verbatim, and inference-api's tool filter performs no RBAC check of its
own (registry membership is its only gate). A user holding the scheduled-runs
capability could therefore craft a request enabling a tool outside their
AppRole, and — for schedules — persist it into an unattended, repeating run.
Add `AppRoleService.filter_requested_tools`, a narrow-never-grant intersection
of a requested tool list against the caller's resolved RBAC grant (honors the
`*` wildcard; admits a scoped `base::tool` id when its base server is granted),
mirroring the existing `_apply_enabled_skills_filter` contract on the skills
axis. Apply it at the three app-api write/entry points where a full User (with
roles) is in hand: schedule create, schedule update (PATCH must not bypass the
create-time check), and run-now. `None` is preserved as "resolve to defaults".
Note: fire-time re-intersection against *current* RBAC (so a later role
revocation disarms a sleeping schedule) is deferred — the worker/dispatcher
only carry a bare user_id, not the owner's live roles. Tracked as follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(schedules): guard the scheduled-runs lean image against agents/strands imports
Add an AST-based test asserting the modules bundled into
backend/Dockerfile.scheduled-runs (harness/, scheduled_prompts/,
sessions_bff/, sessions/ + the two lambda handlers) never import agents/
or strands — top level OR lazy, since a deferred import still crashes at
call time in the lean image. This is the guard that would have caught the
ModuleNotFoundError shipped in the first cut of the worker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(kaizen): scope managed-Harness build-vs-adopt spike for the headless lane
Surfaced while dogfooding scheduled runs: we use AgentCore Runtime (BYO
container), not the newly-GA managed Harness. Brief evaluates adopting the
managed Harness as the backing for the headless/scheduled/proactive lane
only (interactive chat stays build — it needs hooks/custom-loop/MCP-Apps
the managed Harness forbids). Captures pros (managed memory fixing the
write-only-in-cloud gap, versioned endpoints, Step Functions, export escape
hatch), cons, and three gating spike questions. Queued for kaizen review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(schedules): let schedule edits clear assistantId/enabledTools
B1's update_scheduled_prompt skipped None, so a PATCH could never detach a
schedule's assistant or reset its tool restriction — the SPA's clear
checkboxes were inert (a bare null reads as 'leave unchanged').
Add an explicit clear contract: an UNSET sentinel in the service
distinguishes 'omitted' (leave) from None (clear -> REMOVE the attribute).
UpdateScheduleRequest gains clearAssistant/clearTools booleans (rejected if
combined with a value). clearAssistant reverts to the default agent;
clearTools re-snapshots the caller's current RBAC-allowed tools, mirroring
creation so a schedule never stores an unresolved None. SPA sends the flags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(app-api): grant bedrock:ListFoundationModels to task role
The admin GET /admin/bedrock/models endpoint calls the Bedrock control
plane's ListFoundationModels, but the App API Fargate task role only had
bedrock:InvokeModel. In deployed environments the call raised
AccessDeniedException, which the app-wide AWS-error handler maps to a
generic 502 ("Upstream service error."). It only worked locally because
local dev runs with the developer's broader AWS credentials.
Add a BedrockListFoundationModels statement granting
bedrock:ListFoundationModels and bedrock:GetFoundationModel. These are
account-level list/read actions that do not support resource-level
permissions, so they are granted on `*`.
Requires a platform.yml (CDK) deploy to take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sessions): unread indicator for scheduled-run deliveries
A scheduled (unattended) run delivers a session the user wasn't watching.
Surface it: the sidebar shows an unread dot until they open it.
- sessions/metadata.py: set_session_unread / mark_session_read — a targeted
single-attribute UpdateExpression on the row's current SK (GSI-resolved),
concurrent-safe with the title/activity writes; preview-session guarded;
best-effort (never raises).
- harness/runner.py: mark the delivered session unread only on a *completed*
run with trigger == "schedule" — attended "Run now" and failed/consent-
blocked runs never set the dot (the user is present / there's nothing to
read).
- app_api POST /sessions/{id}/read: clears the durable flag when the user
opens the session; idempotent, ownership-enforced via the GSI lookup.
- SPA: durable server-persisted unread on SessionMetadata (survives reload,
reaches other devices) ORed with ChatStateService's ephemeral in-tab
unread for interactive background completions; session-list renders the dot.
Backend 108 + frontend 25 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(kaizen): queue Bedrock Mantle endpoint watchlist item
Phil-initiated kaizen focus: scope a future bedrock-runtime (Converse) ->
bedrock-mantle endpoint migration. Watchlist/Defer — strategic alignment
with where Bedrock capability lands first, not near-term need; Claude-on-
Mantle is Messages-API-only today and lacks cross-region + native
CountTokens + Guardrails, so the primary chat path stays on bedrock-runtime.
Interim low-risk value = finishing the already-scaffolded non-Claude
OpenAI-compatible Mantle lane.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(kaizen): managed-Harness spike findings — 3 gating questions answered
Complete the #570 build-vs-adopt spike for the headless/scheduled lane.
Answered from the now-GA AWS managed Harness docs cross-checked against
our code and the proven F1 entrypoint spike:
- Q1 RBAC -> allowedTools: qualified yes (per-invoke globs; we already
snapshot the RBAC-narrowed set statically at the app-api boundary).
Non-membership gates relocate (quota/cost -> dispatcher; approval ->
exclude on headless; consent -> Identity outbound).
- Q2 per-user tokens: yes on mechanism (OAuth-inbound customJWTAuthorizer
== our authorizer; Gateway outbound == our USER_FEDERATION exchange).
SigV4 cannot do per-user identity. One residual: customParameters
vault-key pinning through the Gateway-managed exchange -> live probe.
- Q3 lose MCP Apps + SSE on headless: yes (interactive-only affordances;
SPA loads the delivered session, not the harness stream).
Recommendation: green-light a narrow InvokeHarness probe to close the Q2
residual; interactive inference-api untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sidenav): gate Scheduled Runs nav entry to system_admin
Hide the "Scheduled Runs" menu option from non-admins while the feature
is still maturing. Adds an isAdmin() check to the existing capability
gate, mirroring the admin-dashboard nav pattern. isAdmin is already
wired into the sidenav component from UserService; showSchedules keeps
its accessibility-probe behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(kaizen): managed-Harness Q2 live-probe result — GO-with-boundary
Records the live dev-ai probe (2026-07-06) that closes the Q2
customParameters residual from the spike findings.
Confirmed live (real CreateHarness/InvokeHarness via boto3): our exact
customJWTAuthorizer is accepted (harness READY); outboundAuth.oauth
customParameters is a first-class field persisted verbatim on GetHarness
(so we CAN pin the same params the consent flow uses); OAuth-inbound runs
as the owner (HTTP 200); the exchange calls the same GetResourceOauth2Token
our get_token_for_user uses; a failed exchange surfaces legibly as a typed
runtimeClientError stream event (maps to paused_reauth).
Boundary found: the managed Gateway 3LO (AUTHORIZATION_CODE) exchange
fails with "must provide a ResourceOauth2ReturnUrl" and does not source
that URL from defaultReturnUrl / OAuth2CallbackUrl header / workload
AllowedResourceOauth2ReturnUrl — a GA wiring gap. Cross-workload token
visibility (platform vs harness-own workload identity) unreached past it.
Decision: GO to adopt Harness on the headless lane, but keep
customParameters-sensitive / all 3LO connectors on our own
get_token_for_user until the return-URL wiring is resolved with AWS.
Aside: managed memory is on by default per harness (relevant to F5).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sessions): add mark-as-read/unread toggle with sidebar dot fixes
Adds a "Mark as read / Mark as unread" toggle to …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#517 (headline) — assistants can use tools again, reverting the 1.0.0 KB-only restriction (#382). Filed under ✨ Improved, with a deployment-note callout since
it's a user-visible behavior change.