Backmerge main into develop after 1.9.0#708
Merged
Merged
Conversation
* docs(memory): reframe spec as Memory Spaces — bindable, templated, shareable
Reframes the per-user markdown memory spec into the Memory Space primitive:
a named, first-class, bindable, templated, and shareable markdown wiki.
Chief-of-Staff template + a bound agent rather than a
special-cased feature.
- Adds the three-layer abstraction (Agent / Memory Space / declarative
binding) and the structural-config vs. semantic-MEMORY.md split.
- Space-keyed storage (SPACE#{id} + membership records + UserSpacesIndex
GSI) so sharing is expressible from day one.
- Entry types (entity / episodic / fact), Space Templates, and manifest-
indexed fields for relational/temporal queries ("who owes what").
- Sharing via owner/editor/viewer grants mirroring assistant collab-edit,
with run-as-user write attribution.
- Data governance is proportionate: same data class as sessions/artifacts
behind the same Entra JWT + RBAC — identity-based, no content-inspection
gate. Deletion-purge + inherited encryption are the only real items.
- 8-PR phasing; PR-1 (data layer) is the next buildout phase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(kaizen): recover resourceOauth2ReturnUrl shape section into harness findings
PR #576 merged from a state prior to commit 908f7e18, orphaning the
resourceOauth2ReturnUrl parameter-shape + harness-security cross-check
subsection. This re-applies it onto develop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(memory): bake in full-ownership zip export of a Memory Space
Add a first-class "download the entire space as a .zip of raw markdown"
capability (index + all entries, structure preserved, metadata.json),
framed as the user-ownership / zero-lock-in property. Promotes the stubbed
export endpoint into §9 with contents, access, streaming mechanics, and an
import-friendly round-trip note; folds the zip export into PR-5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): Memory Spaces data layer (PR-1)
Adds the F5 Memory Space primitive's data layer — no runtime wiring,
gated by MEMORY_SPACES_ENABLED (default off).
Backend (apis/shared/memory/):
- store.py: S3 content-addressed byte store (sibling of the skills store)
- models.py: MemorySpace / MemoryIndex / MemoryEntryRef / SpaceMember
- templates.py: Blank / Chief-of-Staff / Research-Notebook presets
- repository.py: dedicated memory-spaces table CRUD (META/INDEX/MEMBER
rows, OwnerIndex + MemberIndex GSIs, Decimal handling)
- service.py: permission-gated lifecycle + sharing + entry/index I/O,
resolve_permission chokepoint (viewer reads, editor writes, owner
shares/deletes), content-addressed writes with GC-on-replace
- feature_flags.py: memory_spaces_enabled() (default off)
- 47 moto-backed tests; import boundaries clean
Infrastructure:
- MemorySpacesConstruct: S3 bucket + dedicated memory-spaces DynamoDB
table (OwnerIndex/MemberIndex GSIs), a per-domain table matching the
project's actual pattern
- Threaded via PlatformComputeRefs to both compute roles (readwrite S3 +
DynamoDB); env vars S3_MEMORY_SPACES_BUCKET_NAME /
DYNAMODB_MEMORY_SPACES_TABLE_NAME / MEMORY_SPACES_ENABLED
- config.ts flag default-off; resource-count assertions updated
- tsc + 429 jest tests pass
Spec updated to reflect the dedicated-table + two-GSI decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(memory): re-slice phasing into primitive vs. agent-consumption workstreams
Splits the plan into two workstreams to keep the Memory Space a clean
bindable primitive: Workstream A (this epic) delivers the primitive + the
user-facing "own your data" surface (data layer, app-api CRUD, export,
sharing, SPA panel, consolidation); Workstream B (Agent/Harness layer)
delivers agent-consumption — the memory_* tools, declarative binding, and
system-prompt index injection — so any run surface can bind the same
primitive rather than welding it to inference-api.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): Memory Spaces user surface — /memory/spaces CRUD (A2)
Workstream A2 of the re-sliced memory epic: the user-facing "own your data"
surface over the Memory Space primitive. No agent-consumption (tools /
binding / prompt injection) — that's the Agent/Harness workstream.
app-api (apis/app_api/memory_spaces/):
- routes.py: /memory/spaces CRUD over MemorySpaceService — list (with
templates + accurate per-space role), create-from-template, get (index +
entry manifest), delete-or-leave, entry read/list/upsert/delete, index
read/update. Sync handlers (FastAPI threadpools the sync boto3 service).
- Gated by require_memory_spaces_user: 404 while MEMORY_SPACES_ENABLED off
(surface behaves as unmounted); cookie auth via get_current_user_from_session.
- Service errors translated NotFound->404, Permission->403, Error->400.
- models.py: camelCase request/response models.
- Mounted before the existing /memory (AgentCore Memory) router; paths are
non-overlapping (/memory/spaces vs /memory/{record_id}).
shared service:
- leave_space(): a member drops their own grant (the shared-in forget-me
case); owner cannot leave.
- list_spaces_for_user() now returns (space, role) so shared-in spaces carry
the member's real viewer/editor grant (only consumer is the new route).
Tests: 12 route tests (moto-backed real service, flag gate, CRUD, 403/404,
member-leaves-via-delete) + leave_space service tests. Full memory suite +
import boundaries green (69 passing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): Memory Space zip export — /memory/spaces/{id}/export (A3)
The "own your data" leg of Workstream A: a loss-free `.zip` download of a
space's raw markdown (§9). `MemorySpaceService.export_space` gathers the
corpus once (index + every entry's bytes) behind the viewer+ permission
gate, including the member grant list only for editor+ callers (mirrors
`list_members`). The app-api route builds the archive in a
`SpooledTemporaryFile` — spilling to disk beyond 8 MiB so a large space
never pins memory — and streams it back.
Zip mirrors the S3 layout (`{name}/MEMORY.md`, `entries/<type>/<slug>.md`,
`metadata.json`) so it is self-contained and re-importable later. Archive
path components are sanitized against zip-slip. Route tests cover layout,
verbatim frontmatter, owner-vs-viewer member disclosure, 403/404/flag-off,
and the hostile-slug case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): Memory Space sharing + optimistic manifest concurrency (A4)
The sharing leg of Workstream A, plus the concurrency guarantee that makes
multi-editor spaces safe.
Sharing surface (app-api, over the existing service grant methods):
- GET /memory/spaces/{id}/shares list grants (editor+)
- POST /memory/spaces/{id}/shares grant viewer|editor (owner)
- PATCH /memory/spaces/{id}/shares/{email} change a grant's role (owner)
- DELETE /memory/spaces/{id}/shares/{email} revoke (owner, idempotent)
New `MemorySpaceService.update_share` gives PATCH proper not-found semantics
and preserves the grant's original createdAt (distinct from share's upsert).
Optimistic manifest concurrency (the real design content):
- `MemorySpaceRepository.put_index(expected_version=…)` does a conditional
DynamoDB write on the manifest `version`, raising the repository-local
`OptimisticLockError` on a mismatch.
- `write_entry`/`delete_entry` route through a new `_mutate_index` helper: a
bounded read-modify-conditional-write retry loop. Because an entry write
touches a single slug, re-reading the fresh manifest and re-applying is safe;
it converges on transient races and raises `MemorySpaceConcurrencyError`
(→ 409) only on a sustained one. Behavior is unchanged for single-writer
spaces.
Tests: 6 route tests (share CRUD, member gains access, non-owner 403, viewer
can't list, PATCH-unknown 404, owner-role 422) + 7 service tests (update_share
role/origin/owner-gate + version-increments, stale-write rejected, retry
converges, gives-up-after-max). 76 memory + import-boundary tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): Memory Spaces SPA panel — list/detail/create/share/export (A5)
The user-facing "Memory" surface for the Memory Space primitive, under
frontend/ai.client/src/app/memory-spaces/. Makes A2–A4 visible to users.
- List page: owned + shared-in spaces as cards with role/template badges;
per-card open, share (owner), download .zip, delete/leave. Empty, loading,
error, and feature-unavailable states.
- Detail page: view/edit the MEMORY.md index (editor+) and the entry list;
entries open in a dialog to view (viewer) or edit/create (editor+); delete
per entry. Header carries share/download/delete-or-leave.
- Create-from-template dialog and a share dialog (add-by-email + per-row role
+ delta-on-save over the A4 /shares endpoints). Viewer access is read-only
throughout; the share dialog fails soft for non-owners.
- Signal facade (MemorySpaceService) + thin API service mirror the
assistants/schedules pattern. The nav entry rides a live accessible$ probe:
a 404 (MEMORY_SPACES_ENABLED off) hides it, matching showSchedules.
- Routes memory-spaces + memory-spaces/:id (authGuard); redesign-tokens and
@angular/cdk/dialog conventions throughout.
Facade spec (7 tests) green; dev build + tsc clean; sidenav specs still pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(memory): mock MemorySpaceService in sidenav spec (fix unhandled rejection)
The A5 sidenav now injects MemorySpaceService and probes `loadSpaces()` in the
auth effect. The sidenav spec mocked ScheduleService but not the new service,
so the authenticated-probe test constructed the real MemorySpaceService, which
fired a real XHR to /memory/spaces (status 0, no backend); `loadSpaces` then
re-threw, surfacing as a Vitest "unhandled rejection" at suite level.
Mock MemorySpaceService exactly as ScheduleService is mocked, and add matching
coverage: probe fires once authenticated (not while unauthenticated) and the
`showMemorySpaces` nav gate resolves null→false, false→false, true→true.
Full suite: 1422 passed, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(memory): wire memory-spaces table/bucket names onto app-api
app-api owns the Memory Spaces CRUD surface (`/memory/spaces/*`) but its
container environment only set MEMORY_SPACES_ENABLED — never the table or
bucket names the service reads (DYNAMODB_MEMORY_SPACES_TABLE_NAME /
S3_MEMORY_SPACES_BUCKET_NAME). Without them the repository falls back to the
default "memory-spaces" table name, which doesn't exist, so every read throws
a boto3 ResourceNotFoundException that the centralized handler maps to a
502 "Upstream service error." (inference-api already sets the identical trio,
but per the service-boundary rule it isn't the one serving these routes.)
Thread `refs.memorySpacesTable`/`.memorySpacesBucket` through AppApiSsmParams
and emit both names next to the flag in buildAppApiEnvironment. Names are
always wired (read lazily); only MEMORY_SPACES_ENABLED gates route mounting,
so flipping the switch on later needs no env change. New unit test guards the
wiring; tsc + 431 infra jest tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cdk): capture the "wire resource name to every compute" rule
Fold the lesson from the app-api env-wiring fix into the cdk-infrastructure
skill so the next construct-author sees it while wiring, not after a 502.
Adds a "Cross-Construct References" subsection: set a resource's name env var
on every compute that reads it (one doesn't imply the other), the silent-502
failure mode (default-name fallback → ResourceNotFoundException → generic 502,
invisible to synth/CI), and the env-map test guard + service-boundary caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory): deterministic consolidation health pass (A6)
The safe, non-LLM slice of Workstream A6. `MemorySpaceService.consolidate`
(editor+) + `POST /memory/spaces/{id}/consolidate` → a `ConsolidationReport`.
Auto-fixes only storage hygiene: orphaned content-addressed objects — keys
under a space's prefix that no manifest entry or the index pointer references
(leaks from crashed/raced writes) — are GC'd (new `MemorySpaceStore.list_keys`
drives it). Everything that needs a judgment call is *reported, not mutated*:
- duplicate content across slugs (same content hash) — which slug survives is
semantic, so it's flagged, never auto-merged;
- dead `[[slug]]` wikilinks in MEMORY.md — reported; opt-in `stripDeadLinks`
unlinks them (they point nowhere) while preserving the surrounding prose;
- over-cap entry counts (`MEMORY_SPACE_INDEX_CAP`, default 200) — flagged, never
auto-evicted.
This deliberately does not merge/evict/rewrite durable memory — that's deferred
to the LLM consolidation pass (Workstream B era), which extends this exact
`consolidate()` seam once agentic writes create real duplication/staleness to
act on. On-demand only for now; scheduler/threshold auto-run and SPA surfacing
are follow-ups.
Tests: 8 service (healthy report, orphan GC + skip, dup-report-no-merge,
dead-link report + strip-keeps-prose, over-cap flag, editor gate) + 4 route
(report shape, no-body, viewer 403, flag-off 404) + 3 store (list_keys prefix
scoping / empty / disabled). 104 memory + boundary tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(agent): Agent Designer spec — unified primitive-binding surface
Captures the strategy for the "Agent Designer" (Agent Harness Editor): a new
authoring surface that composes an Agent from RBAC-governed primitives
(instructions, model, KBs, tools, skills, Memory Spaces, + future), replacing
the term/feature "Assistant."
Locks the load-bearing decisions: own a primitive-agnostic Agent contract and
federate AgentCore Registry later rather than build on it (adopt-with-boundary
precedent); evolve the assistant store in place (no parallel table); a uniform
bindings[] model with the model as a governed single-select; RBAC = compose the
five existing per-primitive access checks (incl. ModelAccessService), not a new
system; design-time filter + run-time re-resolution per invoker with block-on-
missing v1; ship memory-consumption as a thin vertical slice before the full
Designer. Phasing 0–5 + later AWS federation; supersedes the memory spec's
"extend the Assistant" §B1 framing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): Agent contract + compat mapping in shared assistants models
Phase 1 (PR-1) of the Agent Designer. Pure library, zero behavior change:
legacy Assistants read unchanged and no caller passes the new fields yet.
- AgentModelConfig (D3 governed single-select; field is model_settings/
alias modelConfig to dodge pydantic's reserved model_config — R3)
- AgentBinding (open kind on read, KNOWN_BINDING_KINDS for request validation)
- optional model_settings + bindings on Assistant (additive)
- compat.effective_bindings/to_agent_view (D2): absent bindings synthesize a
knowledge_base binding reffing the assistant id (KB's only stable identity,
F4 deferred — R4); absent model maps to None, never fabricated (R1)
- Decimal-safe serialization for modelConfig.params floats
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): persist bindings + modelConfig with design-time validation
Phase 1 (PR-2). The Agent fields now round-trip through the rag-assistants
store and are validated at write time by composing existing RBAC checks (D4).
Legacy clients are unaffected: the SPA sends none of the new fields and the
AssistantResponse surface is unchanged.
- service.create_assistant/update_assistant thread bindings + model_settings;
to_ddb_safe on write / from_ddb on read so modelConfig.params floats survive
DynamoDB (Decimal); explicit [] replaces bindings, absent leaves them untouched
- app_api/agents/services/binding_validation.py composes model access
(ModelAccessService), memory resolve_permission (viewer+/editor+), the
implicit-KB rejection, and inert shape-only checks for tool/skill (D4/D5)
- assistants POST/PUT validate then pass through; validation raises 4xx outside
the create handler's generic except so it isn't masked as 500
- tests: validation matrix (incl. inert no-RBAC guarantee), persistence round
trip, legacy no-field read
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): /agents alias router behind AGENTS_API_ENABLED (dark)
Phase 1 (PR-3). A governed Agent read/write surface over the evolved assistant
store: same shared service functions and identity-based access gates as
/assistants, but returning the Agent shape (compat.to_agent_view -> AgentResponse)
so callers see modelConfig + bindings. Legacy ids valid unchanged.
- feature_flags.agents_enabled(): AGENTS_API_ENABLED, default OFF (memory-spaces
pattern) — surface 404s while off, ships incrementally, /assistants unaffected
- app_api/agents/routes.py: require_agents_enabled 404-gate; draft/create/list/
get/update/delete + 4 shares endpoints, delegating to apis.shared.assistants
service and reprojecting via to_agent_view; create/update run binding_validation
- AgentResponse/AgentsListResponse/AgentSharesResponse (agentId == assistantId)
- main.py mounts the router
- test-chat + document sub-routes deliberately excluded (would force a 2nd
architecture import-boundary exception); list is owner+shared (public/pagination
parity deferred to the Phase-4 Designer)
- tests: 404-gate, agentId/bindings projection, CRUD permission gating, shares
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): wire AGENTS_API_ENABLED through CDK + Phase 1 docs
Phase 1 (PR-4). Completes Phase 1 deployability: the /agents surface can now
be turned on per environment. No new AWS resources — the flag only gates
whether the routes 404 (the assistant store it reads is always present).
- config.ts: AgentsConfig { enabled }; CDK_AGENTS_API_ENABLED (default off,
empty-string-safe) or an `agents.enabled` cdk.json context, mirroring
memorySpaces exactly
- app-api-environment.ts: AGENTS_API_ENABLED env on app-api
- infra tests: default-off / opt-on assertion; mock-config default
- docs: agent-designer.md Phase-1 status (+ the two refinements and the
Oliver-dogfood-gated-on-Phase-3 note); CHANGELOG [Unreleased]
The live Oliver dogfood (D6) is deliberately NOT included: it needs Phase 3
harness resolution + Memory Spaces deployed to the target env before a
memory_space binding resolves at invocation. Tracked as the Phase 3 payoff.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): thread AGENTS_API_ENABLED to the inference runtime (Phase 3 PR-0)
Phase 3 harness resolution runs inside inference-api, so the runtime needs the
same flag the app-api surface got in #593. Default off, mirrors the app-api
wiring; without it the harness ignores Agent bindings entirely (today's behavior).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): resolve Agent modelConfig at invocation, per invoker (Phase 3 PR-A)
The Harness now re-resolves an Agent's governed modelConfig against the INVOKING
user (D5) and applies it to model selection. Absent modelConfig ⇒ the model
resolves exactly as today; gated on AGENTS_API_ENABLED (off in all envs still).
- agent_binding_resolver.py: resolve_agent_invocation() checks the pinned model
against AppRoleService.can_access_model for the invoker (R2 — same gate the
harness uses elsewhere), returns a model_override or raises
AgentBindingBlockedError. inference-api imports apis.shared only (boundary-safe)
- routes.py /invocations: resolve after assistant load, before the KB search;
on block, stream a conversational stream_error via stream_conversational_message
(D5 block-with-message, no silent downgrade). Override wins at model resolution;
agent params sit beneath request params, still flowing through admin bounds/locks
- tests: allowed→override, denied→block (checked vs invoker), no-modelConfig→
empty plan (no RBAC call); 57 existing inference/chat tests unchanged
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): Memory-Space hydration helper for prompt injection (Phase 3 PR-B)
Shared, sync helper that resolves a memory_space binding's alwaysLoad specs into
injectable text fragments — the read side of Workstream B.
- resolve_always_load(): MEMORY.md → index; latest:<type>/<prefix> → most-recent
matching manifest entry (defines that scheme, which had no resolver); bare slug
→ entry. Missing entries skipped (never fails a turn). Byte-budgeted with a
truncation marker pointing at memory_read (MEMORY_INJECTION_MAX_BYTES, ~24KB)
- render_memory_block(): delimited system-prompt block; empty for a fresh space
- reads go through MemorySpaceService (re-checks viewer+ internally) — no leak
- 11 unit tests against a fake service
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): inject bound Memory Space into the prompt, per invoker (Phase 3 PR-C)
The Harness now resolves an Agent's memory_space binding against the invoking user
and injects the space's alwaysLoad content (read-only) into the system prompt —
the first half of the Workstream B / Oliver payoff.
- agent_binding_resolver: _resolve_memory() checks the invoker's grant via
MemorySpaceService.resolve_permission (D4); blocks (D5) when the flag is off,
the space is gone, or a readwrite binding meets a below-editor invoker (no
silent read-only downgrade). Returns ResolvedMemoryBinding (v1: first binding)
- routes.py: after prompt assembly, hydrate via resolve_always_load (asyncio.to_
thread; MemorySpaceService re-checks viewer+) and append render_memory_block;
best-effort — a memory-read hiccup never fails the turn
- tests: memory grant matrix (none/flag-off/missing/read-viewer/readwrite-viewer-
block/readwrite-editor/invoker-identity); 78 inference+compat tests green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agents): rename app_api.agents package to avoid shadowing top-level agents
run-app-api.sh launches app-api with `cd src/apis/app_api && python main.py`,
putting that directory on sys.path[0]. The new apis/app_api/agents/ package
(Agent Designer surface, #591/#592) then shadowed the top-level `agents`
package, so `admin/quota/routes.py`'s `from agents.main_agent...` resolved into
it and crashed startup with `ModuleNotFoundError: No module named
'agents.main_agent'`. Tests never caught it — pytest runs from backend/ where
`agents` resolves correctly. Production is unaffected (the container runs
`uvicorn apis.app_api.main:app` from WORKDIR /app, so sys.path[0] is /app).
Rename the package apis/app_api/agents → apis/app_api/agent_designer (and its
test dir) so its name can't collide with the top-level `agents` package. Pure
rename: the /agents URL surface, router, and behavior are unchanged.
Verified: `import agents.main_agent.quota.repository` and the full app-api module
now load from src/apis/app_api; 34 agent/boundary tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): memory_* tools scoped to an Agent's bound Memory Space (Phase 3)
Completes the Workstream B write side: an Agent with a memory_space binding now
gets memory_list / memory_read (always) and memory_write (readwrite bindings
only) at invocation — Oliver can read AND write his space.
- agents/builtin_tools/memory_spaces/: closure-scoped factories capturing the
binding's space id + invoker identity, MemorySpaceService via asyncio.to_thread
(artifact-tools pattern). Every call re-checks the grant inside the service
(viewer+ read / editor+ write), so a revoked grant becomes an error tool-result
mid-session, never a leak
- routes.py _build_memory_tools(): appended to the extra_tools seam only when a
memory binding resolved; write tool gated on access==readwrite. extra_tools
agents are never cached → tools closed over user A can't be served to user B
- not gated on enabled_tools: the governing capability is the Agent's binding,
not the user's tool picker (same reasoning as artifact tools)
- tests: tool success/permission-error/not-found matrix + seam counts
(none=0, read=2, readwrite=3); 84 inference+tool tests green
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(memory): default Memory Spaces ON with a kill switch
Memory Spaces is a complete feature (CRUD + SPA panel + agent binding), so it
should ship enabled for every deployer/forker — opt-out, not opt-in — matching
the kbSync / scheduledRuns convention. The table + bucket are already
provisioned unconditionally in PlatformStack, so this only flips the runtime
MEMORY_SPACES_ENABLED env var; no new infra footprint.
- config.ts: memorySpaces.enabled default true (empty/unset workflow var = on,
only literal "false" disables); interface + block docs updated
- platform.yml: forward CDK_MEMORY_SPACES_ENABLED (kill switch) and
CDK_AGENTS_API_ENABLED (per-env enable for the still-default-off /agents
surface, so dev can turn it on for dogfooding without a code change)
- app-api-environment.ts: comment reflects default-on
- config.test.ts: 5 tests locking default-on + kill-switch + context override
Agent Designer (AGENTS_API_ENABLED) stays default OFF until the Phase-4 Designer
UI ships — a headless /agents surface helps no forker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): Phase 4 — Agent Designer UI + bindable catalog API
Ships the Agent Designer authoring surface (Phase 4) and its Phase-2
precursor, the bindable-primitives catalog. The pickers can't exist
without the catalog, so both land together.
Backend (Phase 2 catalog):
- GET /agents/bindable?kind=model|tool|skill|knowledge_base|memory_space
returns an RBAC-filtered palette, composing the 5 existing per-primitive
access services (D4); no new RBAC invented. Uniform BindableItem shape so
every picker consumes one contract. Route declared before /{agent_id} so
the literal path isn't captured. knowledge_base → empty (welded/synthesized);
skill/memory_space → empty when their feature flag is off. Behind
AGENTS_API_ENABLED.
- Fix binding_validation._validate_model: it resolved models via
get_managed_model() — a primary-key lookup on the internal UUID — but
modelConfig.modelId is the Bedrock model_id that the runtime resolver, RBAC
(permissions.models) and invocation all key on. A valid model would have
been rejected 400 on save the moment the picker set one. Now matches by
model_id, consistent with the whole chain.
Frontend (Phase 4 UI):
- New agents/ feature dir (separate from the Assistants editor): Agent +
Binding + BindableItem TS contracts, a thin AgentApiService, and an
AgentService signal facade with the accessible$ 404-probe idiom + a
per-kind bindable cache.
- Agents list page (model + binding-count badges) and an agent-form page:
persona/emoji/tags/starters, a required single-select model picker (D3),
tool/skill multi-select chips, and a memory-space picker with access
(read/read+write — write disabled unless editor+ on the space, per D5) and
an alwaysLoad MEMORY.md toggle. KB shown read-only. Sharing reuses the
assistants share dialog (agentId == assistantId).
- Routes agents / agents/new / agents/:id/edit, plus a sidenav "Agents"
entry gated on the accessible$ probe.
Tests: backend 1552 pass (9 new catalog + 4 new route + 3 updated
model-validation); SPA build + tsc clean, ng test 7 AgentService + 11
sidenav specs pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sidenav): stub AgentService probe to fix unhandled HTTP rejection
The sidenav constructor now probes agent accessibility (void
agentService.loadAgents()), but sidenav.spec.ts didn't provide a mock
AgentService, so the real service fired an unstubbed HTTP GET /agents that
rejected with status 0 — vitest fails the run on unhandled errors even
though all assertions passed. Provide a mock AgentService (accessible$
signal + no-op loadAgents) mirroring the schedule/memory stubs, plus
parity tests for the probe + showAgents gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent-designer): align model write-check with the bindable catalog
The catalog lists models via ModelAccessService.filter_accessible_models,
but design-time write validation used can_access_model — and the two
disagree. filter_accessible_models grants access whenever the model id is
in the user's AppRole permissions.models; can_access_model only honors that
membership when the model record ALSO carries a non-empty allowed_app_roles.
So a model granted purely via the user's AppRole (empty allowed_app_roles)
was listed by the picker but rejected on save with a 403 — and the runtime
resolver (membership-based) would actually have allowed it.
Validate the model with the same filter_accessible_models predicate the
catalog uses, so 'if the palette offers it, the write accepts it' holds by
construction. Adds a regression test for the empty-allowed_app_roles grant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sidenav): gate Memory Spaces + Agents to system-admin, add Preview badges
Match the Scheduled Runs treatment for the two other preview surfaces:
Memory Spaces and Agents now also require the system_admin AppRole
(showX() && isAdmin()) in addition to their accessibility probe. Adds a
small amber 'Preview' badge to all three nav entries (Agents, Memory
Spaces, Scheduled Runs) so their preview status is visible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): resolve tool bindings at invocation (replace + per-invoker RBAC)
An Agent's `tool` bindings were stored by the Designer but inert at run time — the
free-select tool picker fully drove the toolset regardless of what the Agent bound.
This resolves them, mirroring the shipped `modelConfig` override:
- Run-time (inference-api): `resolve_agent_invocation` now returns `plan.tools`
(`ResolvedTools`). When an Agent binds tools they *replace* the request's
`enabled_tools` for the turn; each bound tool is re-checked against the INVOKING
user via `AppRoleService.can_access_tool` (the same AppRole gate the harness uses
for model, R2) and a missing tool blocks the turn with a message (D5). No tool
binding ⇒ `plan.tools is None` ⇒ the request drives the toolset exactly as today.
Wired at the existing `extra_tools`/`get_agent` seam via `effective_enabled_tools`
(also feeds the spreadsheet/artifact tool gates + attachment guidance/inventory).
- Design-time (app-api): `tool` dropped from `_INERT_KINDS`; a bound tool must be in
the author's palette (`ToolCatalogService.get_user_accessible_tools`, the same
source the picker fetches — "if the palette offers it, the write accepts it", cf.
the model check). The palette is resolved once per write.
`skill` bindings stay inert here (their run-time fold interacts with agent_type/skill
resolution — a follow-up slice).
Tests: 6 resolver cases (override, dedupe, block-on-missing, per-invoker, none→passthrough)
+ 5 validation cases (accessible/inaccessible/empty-ref/fetch-once/lazy). Full backend
suite green (4621 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(topnav): surface active assistant in the top nav
Move the assistant/agent indicator out of the chat-input footer and into
the top nav, beside the session title, so an attached assistant is visible
throughout the conversation.
- Add a compact 'variant' to app-assistant-indicator: a subtle name-only
pill (emoji + name) that opens the same actions menu (New session / Edit /
Share) on click. The full card style is preserved behind variant="card".
- Add a menuPlacement input so the actions dropdown opens downward in the
top nav instead of clipping off-screen.
- Thread the assistant/owner/loading state and action outputs from the chat
container into app-topnav; render the pill (with a loading shimmer) to the
right of the title.
- Remove the now-orphaned footer indicator and loading skeletons from the
full-page chat container (embedded preview footer left intact).
- Assistant card: move conversation starters into a collapsible accordion
(expanded by default) to keep the card compact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): resolve skill bindings at invocation (replace + force skill-mode)
Completes the tool/skill runtime-resolution gap (tools landed in #601). An Agent's
`skill` bindings were stored by the Designer but inert at run time. This resolves them,
mirroring the tool/model overrides:
- Run-time (inference-api): `resolve_agent_invocation` now returns `plan.skills`
(`ResolvedSkills`). When an Agent binds skills they *replace* the request's skills for
the turn AND the route forces `agent_type="skill"` so the SkillAgent discloses exactly
the bound set. Each bound skill is re-checked against the INVOKING user via
`AppRoleService.can_access_skill`; a missing skill — or the Skills feature being
disabled in this environment — blocks the turn with a message (D5). No skill binding ⇒
`plan.skills is None` ⇒ the request's agent_type/enabled_skills drive the turn as today.
Wired by reassigning `effective_agent_type`/`effective_skill_ids` before the main-turn
get_agent, so the values flow into the construction snapshot and a bound-skill agent
resumes on the same skills_hash (resume-safe, same mechanism the tool slice relies on).
- Design-time (app-api): `skill` dropped from inert (no inert kinds remain). A bound skill
is flag-gated (`skills_enabled()`) and must be in the author's palette
(`resolve_accessible_skill_ids`, the same source the picker fetches — cf. the tool
check); the palette is resolved once per write and only when skills are enabled.
Tests: +6 resolver (override, dedupe, flag-off block, block-on-missing, per-invoker, none
→passthrough) + 6 validation (accessible/inaccessible/empty-ref/flag-off/fetch-once/lazy).
Full backend suite green (4631 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): reflect governed agent bindings in the chat-input (lock pickers)
The backend governs an Agent's model/tool/skill bindings at invocation (#601, #602) —
the agent's set wins regardless of what the client sends. The chat-input still showed
the model/tool/skill pickers as free-select, which was dishonest (a change the backend
ignores). This locks each picker to the active Agent's bindings, per primitive.
- Session page (`session.page.ts`): inject AgentService/ToolService/SkillService; fetch
the governed Agent alongside the assistant (agentId == assistantId) in `loadAssistant`;
apply per-primitive locks from `modelConfig`/`bindings`, and release them when navigating
to plain chat. Best-effort: the /agents surface may be disabled (404) or the assistant
may be a legacy assistant with no bindings — every failure leaves the pickers free-select.
- ModelService/ToolService/SkillService: add a small agent-lock API (`lockToAgent*` /
`clearAgentLock` + `agentLocked`/`agentModelLocked`). While locked, `enabledToolIds`/
`enabledSkillIds` return the bound set (replace semantics, matching the backend), toggles
no-op, and `isToolShownEnabled`/`isSkillShownEnabled` render the bound set honestly.
- UI: model-dropdown shows a locked read-only chip ("set by this agent"); model-settings
shows a "Set by agent" model row and a "This agent uses a fixed set of tools/skills"
banner, with tool/skill/sub-tool toggles disabled + greyed while locked.
This is UI honesty, not enforcement — the backend remains the authority. Per-primitive:
an agent that binds a model but no tools locks only the model; the rest stay free-select.
Tests: +5 tool-lock, +5 skill-lock, +4 model-lock service specs (ng test, 51 pass);
`tsc` clean; production build (AOT template check) clean.
Known limitations (documented for follow-up): a model race if the pinned model isn't in
the user's loaded set yet (dropdown disables but may show the fallback name until models
load); the skill-lock banner only shows in skills chat-mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent-designer): release chat-input picker locks on new conversation
The agent-binding picker locks live in root singleton services (Model/Tool/Skill
Service) that outlive the session component. Clicking "New chat" navigates to `/`,
which recreates the session component with fresh assistant()/agent() signals (both
null). The lock-release lived inside the `if (loadedAssistant || … || agent())`
guard, which is false on that fresh component — so the stale locks from the previous
agent conversation were never released, leaving the model + tools pickers stuck.
Move `clearAgentBindingLocks()` out of the guard so it always runs when there is no
assistant in the URL. Idempotent — a no-op when nothing is locked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): show only the bound tools/skills when an agent locks the settings
When an Agent dictates a fixed toolset/skillset, the settings panel listed every
accessible tool/skill with the bound ones toggled on and the rest greyed off — a long,
noisy list. Filter to show ONLY the bound (enabled) tools/skills so the panel reflects
exactly what the agent uses.
- ToolService.visibleTools / SkillService.visibleSkills: agent-locked → filter to the
bound ids; otherwise the full accessible list.
- model-settings template iterates the visible* lists.
Tests: +1 tool-lock, +1 skill-lock spec (ng test green); tsc + AOT build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(agent-designer): default AGENTS_API_ENABLED on with a kill switch
The Agent Designer is complete (contract → surface → resolution → Designer UI →
binding reflection), so flip the feature flag from opt-in to default-on, matching the
house style for shipped features (scheduled_runs / memorySpaces).
- backend `agents_enabled()`: empty-string-safe default-on — unset/empty ⇒ enabled,
only the literal "false" disables (was `== "true"`, default off).
- CDK `config.agents.enabled`: mirror the memorySpaces/scheduledRuns ternary
(`!== 'false'` + context fallback `?? true`), so an unset/empty GitHub Actions var
can't silently disable it.
- Tests: add the Agents API default-on/empty/kill-switch/context suite to config.test.ts
(mirrors Memory Spaces); rename the app-api-environment threading test (no longer
"default off").
The `/agents/*` API now ships everywhere; the SPA nav stays preview-gated (system-admin
+ "Preview" badge) until Assistants are deprecated, so this doesn't broaden user-facing
exposure — it just stops the API 404ing per-environment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): manage an agent's knowledge base from the Agent Designer
Extract the assistant editor's inline "Knowledge base" section into a
standalone, reusable KnowledgeBaseSectionComponent and use it in both the
assistant form and the Agent Designer — replacing the agent form's read-only
"managed automatically" card with the live document/web-crawl/connector flow.
This closes the last Agent migration blocker. The gap was frontend-only: the
document upload/ingestion/retrieval pipeline already keys on the record id and
agentId == assistantId, so /assistants/{id}/documents backs an agent unchanged.
No backend or data-model changes (Option 1, not the deferred F4 first-class KB
primitive).
The component owns record identity via a createDraft callback so the first
content-adding action can mint a draft in create mode; a permissionResolved
input gates the edit-only sync-policy calls so a viewer never 403s on the
default owner guess. The assistant form keeps createDraftAssistant as its
callback (shedding ~1000 lines); the agent form adds createDraftAgent and drops
the read-only kbBinding path.
Verified: ng build clean, ng test 1449 specs green (incl. assistant-form spec).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(scheduled-runs): freeze dispatcher clock to de-flake cadence rearm test
test_next_run_at_uses_schedule_cadence asserted the daily-9am re-arm delta
fell in (1h, 48h), which fails when CI runs in the hour before 9am Boise
(the next daily run is legitimately <1h away). Freeze dispatcher._now to a
fixed instant and assert next_run_at equals compute_next_run_at recomputed
from the same instant, making the test time-of-day independent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): govern model params + live editor preview
Model-params governance:
- binding_validation._validate_model_params rejects params that are
unsupported / locked / out-of-[min,max] / out-of-allowed against the
model's admin supported_params (belt-and-suspenders to the runtime
merge; author-facing 400 instead of a silent clamp). +9 tests.
- Data-driven Parameters subsection under the model picker reading
meta.supportedParams (numeric inputs, enum selects, locked read-only);
empty params omit `params` (today's exact resolution).
Live side-by-side preview in the agent editor:
- New AgentPreviewComponent reuses PreviewChatService and streams the
SAVED agent through the real /chat/stream invocation path, so all
bindings (model/params/tools/skills/memory) resolve server-side.
Capability strip + dirty banner make the resolved context and the
save-to-apply semantics explicit.
- Agents send a minimal request body (message/session_id/agent id) and
opt out of the assistant preview's system_prompt + owner-tools
injection, which fought the bindings and blew the 8KB system_prompt
cap for long personas (422). PreviewChatService gains a backward-
compatible opts flag; assistant preview behavior unchanged.
- Two-column editor shell mirroring the assistant editor.
Verified: backend 59 pass (9 new), ng build clean, 16 SPA specs
(7 agents + 9 preview-chat).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agent-designer): lock preview model picker; trim preview nav
The Agent Designer preview reused the main chat-input, whose model
dropdown reads the root ModelService — so it showed the user's global
model (e.g. Sonnet 5) and let them switch it, even though the harness
resolves the model from the agent's binding server-side. Wire the
preview to lock that picker to the agent's model via the same
lockToAgentModel mechanism the session page uses for a real agent
conversation, released on destroy (and idempotently on the next plain
chat via the session page's self-heal effect).
Also hide the Memory Spaces and Scheduled Runs side-nav entries for now
(routes/pages and their capability probes are unchanged, so re-enabling
is just re-adding the template blocks). Agents stays system-admin only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(memory-spaces): route namespaced entry slugs via :path converter
Entry slugs are namespaced with a slash (e.g. `people/brian-bolt`), but the
app-api entry routes declared a plain `{slug}` param whose converter stops at
`/`. Uvicorn percent-decodes `%2F`→`/` before routing, so `/entries/people/
brian-bolt` never matched `/entries/{slug}` and returned 404 on view/edit/delete.
Switch the GET/PUT/DELETE entry routes to the `{slug:path}` converter so the
embedded slash is captured and the slug arrives matching the manifest. Adds a
route test exercising upsert→read→delete with a slashed slug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(memory-spaces): let the agent read/write MEMORY.md via reserved slug
MEMORY.md is the space's human-readable index — a standalone S3 object outside
the entries manifest, injected into the agent's context each session via
hydration. It never appears in `memory_list`, and the agent had no tool to read
it back or keep it in sync with the entries it writes, so the machine-readable
manifest and the human-readable index could silently drift.
Route the reserved `"MEMORY.md"` slug (case-insensitive) through the existing
service methods: `memory_read("MEMORY.md")` → `read_index` (viewer+),
`memory_write("MEMORY.md", body)` → `update_index` (editor+, body only). No new
tool surface; matches the literal hydration already uses. The slug is reserved —
the agent cannot create an ordinary entry named MEMORY.md. Write stays gated
identically to entry writes (only bound when the binding grants readwrite;
service re-checks editor+). Docstrings + spec §4/§5 updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(schedules): target Agents instead of Assistants on scheduled runs
The scheduled-run form's target selector now lists Agents (the Agent Designer
primitive that supersedes the Assistant) instead of Assistants. Same underlying
record — agentId == assistantId — so the wire field stays `assistantId` and no
backend change is needed for the swap.
Because an Agent's `tool` bindings replace the run's `enabled_tools` at
invocation (agent_binding_resolver / routes.py effective_enabled_tools), the
manual tool picker is now hidden whenever an Agent is selected — showing it
would let the user pick tools that get silently discarded. The picker (and its
snapshot semantics) remains only for the "Default agent" case. Submit drops any
stale snapshot when an Agent is targeted.
Also fixes "Run now" to target the selected Agent via ragAssistantId (the
/runs/now backend already accepts it) — previously it ignored the target, so
the attended test surface didn't match what the schedule would actually run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(kaizen): weekly research scan 2026-07-10
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(deps): upgrade Strands to 1.47.0 and add aws-bedrock-token-generator
Bumps strands-agents 1.40.0 -> 1.47.0 (and the [bidi] extra to match) and
adds aws-bedrock-token-generator==1.1.0 (bounded >=1.1.0,<2.0.0 by strands'
openai extra). strands-agents-tools stays at 0.5.2 (resolver-confirmed
compatible).
Unblocks Bedrock Mantle work that needs the newer SDK:
- OpenAIResponsesModel (Responses API) for models that don't support Chat
Completions (e.g. openai.gpt-5.x on Mantle).
- bedrock_mantle_config, which mints the Mantle bearer token via
aws-bedrock-token-generator and derives the base URL + model-family base
path (openai.gpt-5.* -> /openai/v1, else -> /v1).
Full backend suite green on 1.47.0 (2306 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(scheduled-runs): remove RBAC gate causing prod 403 "Access Denied"
Regular users hit a 403 "You do not have access to scheduled runs" toast on
page load. The `/schedules` and `/runs/*` surfaces were gated by the
`scheduled-runs` RBAC capability, granted only to a beta cohort's AppRole
(admins passed via the `*` wildcard). The sidenav ran a background
`loadSchedules()` probe on every load, and the global errorInterceptor popped
the toast on the 403 before the schedule service's graceful catch ran.
The feature doesn't need admin/beta gating — keep it low-key and reachable
only by direct URL for now:
- Drop the capability check from both `require_scheduled_runs_user` gates;
only the `SCHEDULED_RUNS_ENABLED` kill switch remains (404 when off). Runs
still execute with the caller's own RBAC-allowed tools, so this widens who
can reach the surface, not what any one caller can do.
- Remove the vestigial sidenav schedules probe and dead
showSchedules/navigateToSchedules wiring (the template never rendered a
"Scheduled runs" link).
- Update route + sidenav tests accordingly.
`apis/shared/rbac/capabilities.py` is now unreferenced; left in place as
generic RBAC infra so re-gating is a two-line revert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: consolidate release workflow into one auto-invoked steering doc + skill
Fold the versioning and release-notes guidance into a single 'cutting a release' guide covering the branch workflow, SemVer bump + version sync, change identification across the divergent main/develop histories, writing both release docs, the squash-merge PR into main, and the required backmerge into develop.
- Add .kiro/steering/cutting-a-release.md (inclusion: auto — name + description, intent-triggered)
- Add .claude/skills/cutting-a-release/ (SKILL.md auto-invoked via description) with references/{release-notes-format,changelog-format}.md for progressive disclosure
- Remove superseded .kiro/steering/{versioning,release-notes}.md and .claude/skills/{versioning,release-notes}/
- Repoint .github/copilot-instructions.md at the consolidated skill/steering
* feat(models): Mantle Responses API + per-model region; drop endpoint-path knob
Refactors the admin "mantle" provider onto Strands' bedrock_mantle_config so
the SDK owns the base URL, model-family base path, and bearer-token minting —
removing hand-rolled inference plumbing. Adds the two things the library can't
infer as declarative per-model fields:
- apiMode (chat | responses): selects OpenAIModel vs OpenAIResponsesModel.
Some Mantle models (e.g. openai.gpt-5.x) only serve the Responses API and
reject Chat Completions, which the endpoint-path knob could never satisfy.
- region: optional override into bedrock_mantle_config["region"], driving both
the Mantle endpoint host and the SigV4 region the token is signed for — so a
model can pin inference to its host region (e.g. gpt-5.x in us-east-1)
independent of where the app runs.
mantleEndpointPath is kept as an accept-but-ignore deprecated schema field
(no stored record breaks) and removed from the UI + runtime. The Responses API
uses different native param names, so to_mantle_config selects a Responses map
(max_output_tokens, nested reasoning.effort) by mode.
Runtime fields (mantle_api_mode/mantle_region) thread through model_config,
the agent factory, base_agent, the paused-turn snapshot, stream_coordinator,
and the chat service/routes. get_mantle_base_url/generate_bedrock_bearer_token
are retained for the admin model-browse list (not inference).
Gemma 4 (google.gemma-4-31b) is temporarily un-curated: it needs the
/openai/v1 base path but the SDK only routes openai.gpt-5.* there, and
bedrock_mantle_config forbids a base_url override. Re-add once the
"google.gemma-" family prefix lands upstream in strands-agents/sdk-python.
Backend suite green (2342). Frontend typecheck + manage-models specs green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(api-converse): serve /chat/api-converse from app-api, not via inference proxy
The API-key converse endpoint was broken in cloud. app-api proxied
POST /chat/api-converse to `{INFERENCE_API_URL}/chat/api-converse`, but
inference-api now runs inside an AgentCore Runtime whose data plane only
serves POST /invocations and GET /ping — any other path returns
UnknownOperationException (404) before reaching the container. It worked
locally only because localhost:8001 bypasses the runtime gateway.
Relocate the handler onto app-api as a self-contained route (validate key
-> RBAC -> bedrock-runtime.converse -> cost accounting), reusing the shared
services it already depends on. app-api reaches Bedrock directly via its
task role, so there is no inference-api hop and no INFERENCE_API_URL
dependency. Delete the proxy, the now-dead inference-api route, and its
DTOs (moved to app_api/chat/models.py). Repoint the converse tests at the
app-api module.
Verified: 263 backend tests pass, import-boundary test clean, and a real
un-mocked smoke against a Bedrock model returns 200 (stream + non-stream).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(app-api): grant Bedrock streaming + inference-profile invoke
The relocated /chat/api-converse handler calls Bedrock Converse from
app-api, so the task role's invoke grant must cover what the catalog's
model IDs need. Expand the BedrockInvokeModel statement to add
bedrock:InvokeModelWithResponseStream (the stream=true path) and broaden
resources to all-region foundation models plus the account-level
inference-profile ARN, since the catalog uses `us.*` cross-region
inference profiles. Mirrors inference-api's BedrockModelInvocation grant.
Verified: infra tsc clean, 442 infra jest tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(settings): point API-key snippets at /api/chat/api-converse
After the BFF refactor, CloudFront only routes /api/* to the backend;
other paths hit the SPA origin, which rejects POST with a CloudFront 403.
The generated curl/Python/JS examples emitted the bare origin, producing
`<origin>/chat/api-converse`. Resolve a relative/empty appApiUrl against
the current origin so snippets target `<origin>/api/chat/api-converse`;
leave an already-absolute value (local dev's http://localhost:8000)
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(api-converse): route Bedrock Mantle models via a shared builder
The API-key /chat/api-converse handler was Bedrock-only; provider="mantle"
models (e.g. openai.gpt-5.4) 400'd because it always called
bedrock-runtime.converse. Add a Mantle path so the full model catalog works.
Extract the Mantle model construction (class-pick + bedrock_mantle_config)
and its param maps + MantleApiMode enum out of agents/main_agent/core into a
new apis/shared/models/mantle.py, so the agent factory and the API-key
handler share ONE implementation (app-api can't import agents/). The factory
now delegates to build_mantle_model.
The handler resolves the requested model's provider from the catalog and
branches: bedrock -> boto3 converse (unchanged); mantle -> the shared builder
+ the bare Strands model's .stream(), which yields the same Converse-shaped
events the Bedrock path already emits — so SSE translation and usage/cost
accounting are shared (cost is now tagged with the real provider). Unknown /
lookup-failure ids fail safe to the Bedrock path.
Verified: shared builder + factory-delegation + handler mantle-path unit
tests; and real dev-ai smokes — chat-mode Mantle and Responses-API Mantle
(openai.gpt-5.4) both return 200 (stream + non-stream) against the live
endpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(app-api): grant bedrock-mantle:CreateInference for api-converse
The api-converse Mantle path invokes a Mantle model directly from app-api, so
the task role needs bedrock-mantle:CreateInference (Mantle's own IAM
namespace) — without it, mantle requests AccessDeny. Fold it into the
existing project-scoped Mantle statement (was browse-only Get*/List*),
renamed BedrockMantleInference to mirror the runtime role's grant.
Verified: infra tsc clean, integration jest green (24 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(identity): MCP user identity forwarding via access-token enrichment
Add an opt-in Cognito Pre-Token-Generation v2 Lambda that copies configured
user-pool attributes into namespaced claims on the ACCESS token, so
personalized MCP tools can identify the caller. The access token is the only
token forwarded end-to-end to MCP servers, so enrichment needs no changes to
the SPA -> app-api -> inference-api -> MCP forwarding path.
Shipped disabled by default (opt-in): a fork that configures nothing gets zero
resources and the token is forwarded as before. Enabling requires the Cognito
Essentials feature plan (pinned on the pool) plus two GitHub Actions variables
(CDK_MCP_TOKEN_ENRICHMENT_ENABLED + CDK_MCP_TOKEN_ENRICHMENT_CLAIMS), keeping
the committed cdk.context.json inert.
- config: McpIdentityConfig (enabled + accessTokenClaims); claim map settable
via JSON env var or context; parseJsonRecordEnv helper.
- handler: stdlib-only, fail-open Pre-Token-Gen v2 trigger (returns event
unchanged on any error so login is never blocked).
- construct: real-code Lambda (fromAsset) attached via addTrigger V2_0;
pool featurePlan pinned to ESSENTIALS.
- wired conditionally into PlatformStack; platform.yml job-level env.
- docs: spec updated (open questions resolved) + implementation summary,
incl. the mcp-servers follow-on handoff.
Ref: docs/specs/MCP_USER_IDENTITY_FORWARDING_SPEC.md
* fix(app-api): grant bedrock-agentcore:CreateTokenVault for OAuth provider create
Admin "add OAuth provider" (POST /admin/oauth-providers/) returned a 502
Bad Gateway. dev-ai app-api logs showed the real cause: an
AccessDeniedException on bedrock-agentcore:CreateTokenVault against
token-vault/default. AgentCore's CreateOauth2CredentialProvider ensures
the default token vault exists on the first provider create, which
requires CreateTokenVault (+ GetTokenVault) on the caller. The app-api
task role had the ...Oauth2CredentialProvider actions but not the
TokenVault ones. The shared error handler maps an uncaught AWS
ClientError to HTTP 502, so the missing permission surfaced as a 502
rather than a 403.
Add CreateTokenVault + GetTokenVault to the AgentCoreWorkloadIdentityAccess
statement. The resource scope (token-vault/*) already covered
token-vault/default; only the actions were missing.
Requires a platform.yml (CDK) redeploy to take effect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(scripts): make sync-version.sh portable across GNU and BSD tools
The version-sync script only ran inside the dev container / CI (GNU coreutils);
on macOS (BSD sed/grep) it errored out and silently left the manifests
un-synced, so a release cut locally had to hand-edit every manifest.
Replace the three GNU-only constructs with POSIX equivalents:
- `grep -oP ... \K` (Perl regex) -> `sed -n 's/.../\1/p'` / awk field split
- `sed -i "expr"` (GNU in-place) -> sed_inplace helper (temp file + mv)
- `sed "0,/re/s/..."` (GNU-only address) -> awk first-match replace
Behavior is unchanged on GNU; the script now runs identically on macOS.
Verified both --check and the write path (incl. shields.io `--` hyphen
doubling and SemVer->PEP 440 lock conversion) round-trip on BSD tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin): make admin sidebar nav sticky on desktop
Pin the admin layout aside below the sticky top bar so the section nav
stays in view while the content area scrolls. Uses lg:self-start so the
aside shrinks to its content (flex items stretch to full height by
default, which defeats position:sticky), plus a max-height + overflow so
a long nav scrolls internally. Mobile dropdown is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): redesign 404 page to match auth screens
Rework the not-found page onto the same design system as the login and
first-boot pages: the primary-derived lava-lamp parallax backdrop (six
depth-tiered morphing blobs), the masked graph-paper grid overlay, and
the frosted-glass card. The oversized 404 sits above the card where the
auth pages place the logo, so all three screens read as one system.
Preserves existing behavior (sidenav hide/show, Return Home, Go Back)
and respects prefers-reduced-motion. Classes are nf-prefixed and
component-scoped via view encapsulation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): make shell scroll container real so sticky nav engages
The admin aside's lg:sticky never engaged because its nearest scrolling
ancestor was the app shell's `flex-1 overflow-y-auto` div, which had no
bounded height — it grew to content and the window scrolled instead, so
sticky bound to a box that never moved. Pin <main> to h-dvh so that div
becomes a genuine scroll container; the admin aside and top bar now stick.
Also apply the admin bar's frosted-glass treatment (bg-*/opacity +
backdrop-blur-sm) to the session topnav so the two surfaces match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(specs): quota cooldown windows + platform ceiling spec and committee one-pager
Replaces the hard monthly quota cutoff with a three-layer model:
anchored 5-hour cooldown windows (Claude-style, exact reset times),
a hard admin-adjustable platform-wide monthly ceiling as the fiscal
guarantee, and the per-user monthly limit demoted to a generous
anti-runaway backstop with degrade-to-economy-model as the target
behavior. Backstop horizon (monthly vs weekly) is a per-tier choice.
Includes an admin pilot tuning playbook with an observe-only phase,
a user-facing quota status endpoint, recommended opening numbers,
and a 7-PR implementation breakdown. The one-pager is the
committee-facing rationale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): guarantee JIT compiler in vitest runs to stop PlatformLocation flake
The unit-test builder keeps Angular packages external, so vitest evaluates
raw fesm2022 chunks whose partial declarations (ɵɵngDeclareInjectable/
ɵɵngDeclareFactory) compile eagerly and require @angular/compiler. Its
presence was incidental — loaded transitively via @angular/core/testing in
the builder's init-testbed setup — so specs with no static Angular imports
(app.spec.ts dynamic-imports './app') could evaluate an unlinked
@angular/common chunk first and fail with "The injectable
'PlatformLocation' needs to be compiled using the JIT compiler, but
'@angular/compiler' is not available" (angular/angular-cli#31993).
- add src/test-setup.ts importing @angular/compiler, wired via the test
target's setupFiles and included in tsconfig.spec.json
- add src/test-setup.spec.ts guarding the invariant deterministically
- bump the first shared-view.page spec to 15s: it pays the one-time
dynamic page-chunk import, which can exceed 5s under full-suite load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): size chat scroll space to the response, adapt to shell scroll container
Replace the fixed viewport-tall bottom spacer in the message list with a
min-height on the last turn group (user message + its assistant
responses). The response streams into the reserved space instead of
pushing a static spacer further down: a short response leaves exactly
the room needed to pin the user message at the top, and a response
taller than the viewport leaves zero dead scroll below it. Turn groups
are keyed by their first message id so a finished turn's DOM (including
live MCP App iframes) never remounts when the next turn starts, and the
end-of-conversation sections (loader, consent/approval prompts,
compaction, orphan artifacts) render inside the reserved space so they
stay visible next to the response.
Also adapt the session page to the real shell scroll container
introduced by #634 (frosted sticky nav): the window no longer scrolls,
which had silently broken submit scroll-to-message and scroll
save/restore. scrollToMessage now uses scrollIntoView with a
scroll-mt-20 header offset, and save/restore reads the shell
container's scrollTop via a stable #app-scroll-container hook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(settings): make user settings sidebar nav sticky on desktop
Mirror the admin layout change (#632): pin the settings aside below the
sticky top bar so the section nav stays in view while the content area
scrolls. Uses lg:self-start so the aside shrinks to its content (grid
items stretch to full row height by default, which defeats
position:sticky), plus a max-height + overflow so a long nav scrolls
internally. Mobile dropdown is untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(admin-tools): discover OAuth-gated MCP servers with the admin's vaulted token
The admin tool "Discover" flow refused OAuth-gated MCP servers outright, so
servers like the GitHub remote MCP server (api.githubcopilot.com/mcp/) could
not be discovered — discovery either 400'd on auth_type=oauth2 or connected
unauthenticated and got a 401 from the server (wrapped to a 400).
Discovery now accepts the OAuth provider id and connects using the admin's own
vaulted 3LO token for that provider, fetched via AgentCore Identity
(get_token_for_user) and injected as a bearer — mirroring how the agent loop
attaches the end-user's provider token at runtime, and reusing the exact path
connector_status already uses. This validates the admin's own connection and
lists the tools their token can see (providers such as GitHub scope-filter the
tool list to the token's grants). It fetches the admin's token only; it cannot
mint an arbitrary end-user's token.
Backend:
- Add requires_oauth_provider (alias requiresOauthProvider) to MCPDiscoverRequest.
- Handler loads the provider, fetches the admin's vaulted token, injects it as
oauth_token into create_external_mcp_client. requires_consent -> 409, unknown
provider / conflict with forward_auth / oauth2-without-provider -> 400.
Frontend:
- Send requiresOauthProvider in the discover payload (the form control already
existed) and the OAuth2CallbackUrl header (bare /oauth-complete, no query
strin…
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.
Reconciles
developwithmainafter the v1.9.0 release squash-merge (#707), makingmaina genuine ancestor ofdevelopagain.Clean auto-merge — no conflicts; only the release-artifact files changed (
VERSION,CHANGELOG.md,RELEASE_NOTES.md,README.md, manifests, lockfiles).sync-version.sh --checkpasses andorigin/mainis verified as an ancestor of this branch.🤖 Generated with Claude Code